一、异常处理
1、下面for循环20个线程,到11,12号的时候执行失败,这里我也用了try catch来捕获异常。
private void button11_Click(object sender, EventArgs e){TaskFactory taskFactory = new TaskFactory();List<Task> taskList = new List<Task>();try{for (int i = 0; i < 20; i++){string name = string.Format($"Click_{i}");Action<object> act = t =>{Thread.Sleep(2000);if (t.ToString().Equals($"Click_11")){throw new Exception(string.Format($"{t} 执行失败"));}if (t.ToString().Equals($"Click_12")){throw new Exception(string.Format($"{t} 执行失败"));}Console.WriteLine("{0} 执行成功", t);};taskList.Add(taskFactory.StartNew(act, name));}}catch (AggregateException aex){foreach (var item in aex.InnerExceptions){Console.WriteLine(item.Message);}}catch (Exception ex){Console.WriteLine(ex);}}
打印出来发现并没有捕获到异常
那么我再新增一句:
Task.WaitAll(taskList.ToArray());
这样我们就可以成功捕获到异常了。 同时,我们也可以通过AggregateException,捕获到我们异常的数据。
最开始我们抓不到异常,是因为系统跑出了try catch,我们抓不到。
而WaitAll可以抓到多线程里面的所有异常
但是产生了一个新的问题,WaitAll会卡界面
线程里面的action不允许出现异常,需要自己处理好
二、线程取消
多个线程并发,某个失败后,希望别的线程停下来。
task外部无法中止,Thread.Abort不靠谱,因为线程是OS的资源,无法掌控啥时候取消
线程自己停止自己——公共的访问变量——修改它——线程不断的检测它(会有延迟)
CancellationTokenSource标志任务是否取消 Cancel 表示取消 IsCancellationRequested表示是否取消。
Token启动Task的时候传入,那么如果Cancel 了,这个任务就会放弃启动,抛出一个异常
private void button12_Click(object sender, EventArgs e){TaskFactory taskFactory = new TaskFactory();List<Task> taskList = new List<Task>();CancellationTokenSource cts = new CancellationTokenSource(); //bool值for (int i = 0; i < 20; i++){string name = $"Click_{i}";Action<object> act = t =>{try{Thread.Sleep(2000);if (t.ToString().Equals($"Click_11")){throw new Exception(string.Format($"{t} 执行失败"));}if (t.ToString().Equals($"Click_12")){throw new Exception(string.Format($"{t} 执行失败"));}//除了11和12抛异常,其他的我们检查一下。//如果已经取消了,那么我们放弃执行if (cts.IsCancellationRequested) //检查信号量{Console.WriteLine($"{t} 放弃执行");return;}//如果还没取消了,那么我们正确执行else{Console.WriteLine($"{t} 执行成功");}}catch (Exception ex){cts.Cancel();Console.WriteLine(ex.Message);}};taskList.Add(taskFactory.StartNew(act, name, cts.Token));}Task.WaitAll(taskList.ToArray());}
三、多线程的临时变量
1、闭包问题
private void button13_Click(object sender, EventArgs e){for (int i = 0; i < 5; i++) {int k = i;Task.Run(() => {Thread.Sleep(100);Console.WriteLine(k);});}}
四、多线程的线程安全问题
抛出问题:
private void button14_Click(object sender, EventArgs e){TaskFactory taskFactory = new TaskFactory();List<Task> taskList = new List<Task>();int totalCount = 0;List<int> intList = new List<int>();for (int i = 0; i < 10000; i++){int newi = i;taskList.Add(taskFactory.StartNew(() => {totalCount += 1;intList.Add(newi);}));}Task.WaitAll(taskList.ToArray());Console.WriteLine(totalCount);Console.WriteLine(intList.Count);}
这里我声明了一个int类型的临时变量和一个List<int>类型的临时变量,让他们嵌套在多线程内进行累加。
可以发现打印出来的数目并不一致,并且都小于10000?
这就是线程安全的问题,是多个线程同时操作同一变量导致的。
对于共有变量:都能访问的局部变量/全局变量/数据库的值/硬盘文件
我们尝试加上锁(Lock)(Lock可以算是一种语法糖)。
private static readonly object btnThreadCore_Click_Lock = new object();private void button14_Click(object sender, EventArgs e){TaskFactory taskFactory = new TaskFactory();List<Task> taskList = new List<Task>();int totalCount = 0;List<int> intList = new List<int>();for (int i = 0; i < 10000; i++){int newi = i;taskList.Add(taskFactory.StartNew(() => {lock (btnThreadCore_Click_Lock){totalCount += 1;intList.Add(newi);}}));}Task.WaitAll(taskList.ToArray());Console.WriteLine(totalCount);Console.WriteLine(intList.Count);}
发现打印出来的数量都正常。
因为lock后的方法块,任意时刻只有一个线程可以进入
1、Lock介绍:
介绍:Lock等同于Monitor.Enter(btnThreadCore_Click_Lock);
离开等同于调用了Monitor.Exit();
限制:Lock只能锁引用类型(占用引用链接),不要用string,因为享元
微软提供的标准写法:
private static readonly object btnThreadCore_Click_Lock = new object();
Lock 最好锁private的,防止外面也去lock
static 全场唯一 ,避免不同实例锁的不同
readonly 只读,不要改动
object 表示引用
lock(this)每次实例化都是不同的锁,同一个实例时相同的锁
但是这个实例别人也能访问到,别人也能锁定
缺点:
Lock解决,因为只有一个线程可以进去,没有并发,所以解决了问题,但是牺牲了性能,所以要尽量缩小lock的范围
2、安全队列(ConcurrentQueue):
最后还是需要一个线程完成操作
3、最好的方法就是不要冲突——数据拆分,避免冲突!!