C#多线程学习
1 使用委托创建线程
我们先使用委托来创建一个线程,关于委托入门学习可以看我的另一篇文章。
第一步是创建一个无返回值的方法
public static void DoSomething(string name)
{
Console.WriteLine("DoSomething begin {0} {1} {2}", name, Thread.CurrentThread.ManagedThreadId.ToString("00"),
DateTime.Now.ToString("HHmmss:fff"));
long result = 0;
for (int i = 0; i < 10000000; i++)
result += i;
Thread.Sleep(2000);
Console.WriteLine("DoSomething end {0} {1} {2}", name, Thread.CurrentThread.ManagedThreadId.ToString("00"),
DateTime.Now.ToString("HHmmss:fff"));
}
记得对Thead的引用
using System.Threading.Tasks;
using System.Threading;
然后使用Action委托来指向这个方法。
Action<string> action = new Action<string>(DoThread.DoSomething);
再声明一个多线程的回调方法。
//声明异步调用的回调函数 AsyncState为在BeginInvoke时第三个参数
AsyncCallback callback = new AsyncCallback(ary =>
{
Console.WriteLine(ary.AsyncState + "计算完成" + Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
接下来我们就可以用BeginInvoke来启动这个一个线程来执行这个委托
//异步调用 参数为 给委托的实际参数 回调函数 给回调函数使用的内容
IAsyncResult res = action.BeginInvoke("123", callback, "ABC");
其中BeginInvoke()的第三个参数是用来给回调函数的参数的AsyncState方法使用。
接下来系统就会启动一个新线程来执行委托方法,主线程可以去执行其他任务,如果希望阻塞主线程来等替代这个新线程,可以使用IAsyncResult的WaitOne()方法。
res.AsyncWaitHandle.WaitOne(2000);
上面我们使用的是无返回值的委托,接下来使用Func<>来指向有返回值的委托。
Func<int> func = new Func<int>(() =>
{
Thread.Sleep(2000);
return DateTime.Now.Year;
});
IAsyncResult iarA = func.BeginInvoke(ar =>
{
int funcRes = func.EndInvoke(ar);
Console.WriteLine("func委托运行结果" + funcRes.ToString());
}, null);
我们可以看到, 使用委托的EndInvoke()可以获取到委托运行的结果。
2 使用TaskFactory创建多线程
另一个更加方便的方法就是使用TaskFactory来创建多线程,具体就是使用TaskFactory的StartNew()来创建并启动一个线程。
List<Task> taskList = new List<Task>();
//接下来开始多线程执行
TaskFactory factory = new TaskFactory();
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("A"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("B"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("C"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("D"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("E"); }));
3 线程的顺序控制
接下来介绍几个用来控制线程流程的方法。
如果想让主线程等待所有其他线程结束后再执行其它任务,可以使用WaitAll(),若是只等其中的一个可以用WaitAny()
//waitAll是说有线程都结束才继续执行
//WaitAny是有一个线程结束主线程就开始继续执行
Task.WaitAll(taskList.ToArray());
这样做的缺点是主线程会卡住,我们可以新创建一个线程来等待之前的那些线程。其中ContinueWhenAll是在所有线程都结束后执行回调函数,而ContinueWhenAny是只要有一个线程完成后就会执行回调函数。
//将这个新线程也加入到集合中 Task.WaitAll就会等待它结束
taskList.Add(factory.ContinueWhenAll(taskList.ToArray(), ask => { Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00") + " Continue完成"); }));
如果在启动线程时,希望传递一个参数给回调函数,那可以将这个参数值作为StartNew()的参数。其中StartNew()的第一个参数为Action,在这个Action中调用该线程要执行的函数。
taskList.Add(taskFactory.StartNew(o => DoSonmeThingLong(para), para));
然后在回调函数中,通过iar的AsyncState属性来获取刚才的参数。
//创建了一个新的线程去等待其它线程
taskList.Add(taskFactory.ContinueWhenAny(taskList.ToArray(), ar =>
{
Console.WriteLine("至少有一个线程已结束 " + ar.AsyncState);
}));
 通过下来的例子我们来看一下ContinueWhenAny和ContinueWhenAll的效果,他们是非阻塞式的。
for (int i = 0; i < 5; i++)
{
int j = i + 1;
string para = "任务" + j.ToString();
//第一个参数为一个无参数的Action 所以要通过一个无参的action去调用DoSonmeThingLong
taskList.Add(taskFactory.StartNew(o => DoSonmeThingLong(para), para));
// taskList.Add(taskFactory.StartNew(() => { DoSonmeThingLong("任务" + j.ToString()); }));
}
//创建了一个新的线程去等待其它线程
taskList.Add(taskFactory.ContinueWhenAny(taskList.ToArray(), ar =>
{
Console.WriteLine("至少有一个线程已结束 " + ar.AsyncState);
}));
//等待所有线程都完成
taskFactory.ContinueWhenAll(taskList.ToArray(), ar =>
{
Console.WriteLine("所有线程都已结束\n");
});
Console.WriteLine("主线程结束 {0}", Thread.CurrentThread.ManagedThreadId.ToString("00"));
}
4 控制线程数量
假如当前有1000个任务可以并发地去执行,那我们就可以用多线程去执行他们,但是这样系统可能就会创建非常多的线程去执行它们,这样就会占用大量的系统资源,所以我们需要控制线程的数量。
具体的做法就是,在当前线程的数量已到达我们的预设值事,就不创建新线程了,除非已有的线程完成一个后,才会再创建一个线程。这样就保证了线程的总数量不会超过我们的预设值。这里就会用到我们上面提到的ContinueWhenAny()。
List<int> list = new List<int>();
for (int i = 0; i < 100; i++)
{
list.Add(i);
}
//实例化一个委托
Action<int> action = (k) =>
{
Console.WriteLine(k.ToString() + " " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
};
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
//控制线程数量
foreach (int i in list)
{
int j = i;
//通过委托启动线程
taskList.Add(taskFactory.StartNew(() => action.Invoke(j)));
if (taskList.Count > 10)
{
//有线程已经结束
Task.WaitAny(taskList.ToArray());
taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
Console.WriteLine("当前线程数量 " + taskList.Count);
}
}
5 多线程的异常处理
在多线程中处理异常时,并不能仅仅在主程序中去捕获异常,因为可能在程序运行结束后,其它线程才开始。所以要在每个线程执行的行为里面去捕获异常,即在Action中去捕获异常。
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
for (int i = 0; i < 20; i++)
{
string name = "thread " + i.ToString();
Action<object> action = t =>
{
try
{
if (t.Equals("thread 11") || t.Equals("thread 12"))
throw new Exception(t + " 执行失败");
Console.WriteLine("正在执行 " + t + " " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
Thread.Sleep(2000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
};
taskList.Add(taskFactory.StartNew(action, name));
}
6 多线程的取消
当多个线程相继启动去执行时,当有一个线程中发生了异常时,其他线程应停止运行,未启动的线程应取消启动。
我们可以用一个信号量来CancellationTokenSource来保存当前进程的运行情况。每当一个线程开始启动时,就先判断下信号量是否正常,若信号量是异常的就停止该线程。同时应在启动这些线程的线程中捕获异常, 这样就能获取线程未启动的消息,所以要使用Task.WaitAll(taskList.ToArray())来阻塞这个线程。
先来创建一个函数,用来运行上述过程。
private void TestThreadCancel()
{
#region 多线程异常处理 取消
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
//保存多线程运行是否正常的信号量
CancellationTokenSource cts = new CancellationTokenSource();
try
{
for (int i = 0; i < 20; i++)
{
string name = "thread " + i.ToString();
Action<object> action = t =>
{
try
{
if (t.Equals("thread 11") || t.Equals("thread 12"))
throw new Exception(t + " 执行失败");
if (cts.IsCancellationRequested)
{
Console.WriteLine("取消执行 {0} {1} ", t, Thread.CurrentThread.ManagedThreadId.ToString("00"));
return;
}
Console.WriteLine("正在执行 {0} {1} ", t, Thread.CurrentThread.ManagedThreadId.ToString("00"));
Thread.Sleep(2000);
}
catch (Exception ex)
{
cts.Cancel();
Console.WriteLine(ex.Message);
}
};
taskList.Add(taskFactory.StartNew(action, name, cts.Token));
}
//需要在所有系线程执行完毕后来捕获AggregateException 所以要阻塞
Task.WaitAll(taskList.ToArray());
}
//捕获多线程信号量异常
catch (AggregateException aex)
{
foreach (var item in aex.InnerExceptions)
{
Console.WriteLine(item.Message);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
#endregion
}
为了不阻塞主线程,我们并不能直接运行该函数,而是启动一个新线程来运行它。
Action action = TestThreadCancel;
action.BeginInvoke(null, null);
7 多线程的安全问题
在运行多个线程时,如果它们都要访问同一个变量,那就会出现问题,我们先演示一个错误的写法。
int iNumber = 0;
//多个线程访问共享变量 存在值覆盖的问题
List<Task> taskList = new List<Task>();
for (int i = 0; i < 10000; i++)
{
int k = i;
taskList.Add(taskfactory.StartNew(() =>
{
iNumber++;
}));
}
正是因为多线程访问共享变量,存在了值得覆盖问题。所以,我们需要用的锁来保护共享变量。
首先声明一个静态的私有的引用型变量
private static readonly Object lock_object = new object();
然后将上述代码稍加改动即可
int iNumber = 0;
//多个线程访问共享变量 存在值覆盖的问题
List<Task> taskList = new List<Task>();
for (int i = 0; i < 10000; i++)
{
int k = i;
taskList.Add(taskfactory.StartNew(() =>
{
//lock要减少作用范围
lock (lock_test)
{
iNumber++;
}
}));
}
如果使用lock(this)就需要特别注意,如果只有一个多线程对象的实例在运行,那么共享变量是安全的,如果有多个实例对象的运行,那么this只能指代某一个实例,那么共享变量还是会被非法访问。
//对于该对象的不同实例去调用时 如果使用Lock(this) 则不互斥 只在某个实例内部互斥
//应为在不同额实例中 不同的this不同变量
TaskSafe taskSafe1 = new TaskSafe();
TaskSafe taskSafe2 = new TaskSafe();
taskSafe1.LockTest("taskSafe1");
taskSafe2.LockTest("taskSafe2");
可以看到这些线程并没有互斥地运行,所以推荐使用私有的静态引用型变零来代替this。
另一个比较让人疑惑的是,在lock()的代码段中,使用递归是否会发生死锁。我们就一起来看一下。
//此时虽然有递归 但是在进入递归时 this是同一个 并且线程也是同一个
//锁是为了防止不同的进程同时访问一个变量
//但此时只有一个线程 并且this也是这个线程创建的
private int iNumber = 0;
//递归锁
public void Lock_this()
{
iNumber++;
lock (this)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00") + " Start");
Thread.Sleep(2000);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00") + " End");
if (iNumber < 10)
Lock_this(); //this不会发生变化
else
Console.WriteLine("End ");
}
}
事实证明并没有发生死锁,因为锁是为了避免多个线程同时访问某个共享变量,但在上述的递归运行过程中,一直都是同一个进程,所以不会发生死锁。
以上就是我为大家介绍的多线程部分的相关知识,如果大家想了解更多相关内容,我会继续更新下去。