本文是.NET异步和多线程系列第四章,主要介绍的是多线程异常处理、线程取消、多线程的临时变量问题、线程安全和锁lock等。html
多线程里面抛出的异常,会终结当前线程,可是不会影响别的线程。那线程异常哪里去了? 被吞了。c++
假如想获取异常信息,这时候要怎么办呢?下面来看下其中的一种写法(不推荐):数据库
/// <summary> /// 1 多线程异常处理和线程取消 /// 2 多线程的临时变量 /// 3 线程安全和锁lock /// </summary> private void btnThreadCore_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnThreadCore_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); #region 多线程异常处理 { try { List<Task> taskList = new List<Task>(); for (int i = 0; i < 100; i++) { string name = $"btnThreadCore_Click_{i}"; taskList.Add(Task.Run(() => { if (name.Equals("btnThreadCore_Click_11")) { throw new Exception("btnThreadCore_Click_11异常"); } else if (name.Equals("btnThreadCore_Click_12")) { throw new Exception("btnThreadCore_Click_12异常"); } else if (name.Equals("btnThreadCore_Click_38")) { throw new Exception("btnThreadCore_Click_38异常"); } Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); })); } //多线程里面抛出的异常,会终结当前线程,可是不会影响别的线程。 //那线程异常哪里去了? 被吞了。 //假如我想获取异常信息,还须要通知别的线程 Task.WaitAll(taskList.ToArray()); //1 能够捕获到线程的异常 } catch (AggregateException aex) //2 须要try-catch-AggregateException { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } } catch (Exception ex) //能够多catch 先具体再所有 { Console.WriteLine(ex); } //线程异常后常常是须要通知别的线程,而不是等到WaitAll,问题就是要线程取消? //工做中常规建议:多线程的委托里面不容许异常,包一层try-catch,而后记录下来异常信息,完成须要的操做。 } #endregion 多线程异常处理 Console.WriteLine($"****************btnThreadCore_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} " + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
上面的这种写法每每太极端了,一会儿捕获了全部的异常。在真实工做中,线程异常后一般是须要通知别的线程(进行线程取消),而不是等到WaitAll。安全
工做中常规建议:多线程的委托里面不容许异常,包一层try-catch,而后记录下来异常信息,完成须要的操做。具体的咱们往下继续看。多线程
多线程并发任务,某个失败后,但愿通知别的线程都停下来,要如何实现呢?闭包
Thread.Abort--终止线程;向当前线程抛一个异常而后终结任务;线程属于OS资源,可能不会当即停下来。很是不建议这样子去作,该方法如今也被微软给废弃了。并发
既然Task不能外部终止任务,那只能本身终止本身(上帝才能战胜本身),下面咱们来看下具体的代码:(推荐)dom
#region 线程取消 { //多线程并发任务,某个失败后,但愿通知别的线程都停下来,要如何实现呢? //Thread.Abort--终止线程;向当前线程抛一个异常而后终结任务;线程属于OS资源,可能不会当即停下来。很是不建议这样子去作,该方法如今也被微软给废弃了。 //Task不能外部终止任务,只能本身终止本身(上帝才能战胜本身) //cts有个bool属性IsCancellationRequested 初始化是false //调用Cancel方法后变成true(不能再变回去),能够重复Cancel try { CancellationTokenSource cts = new CancellationTokenSource(); List<Task> taskList = new List<Task>(); for (int i = 0; i < 50; i++) { string name = $"btnThreadCore_Click_{i}"; taskList.Add(Task.Run(() => { try { if (!cts.IsCancellationRequested) Console.WriteLine($"This is {name} 开始 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(new Random().Next(50, 100)); if (name.Equals("btnThreadCore_Click_11")) { throw new Exception("btnThreadCore_Click_11异常"); } else if (name.Equals("btnThreadCore_Click_12")) { throw new Exception("btnThreadCore_Click_12异常"); } else if (name.Equals("btnThreadCore_Click_13")) { cts.Cancel(); } if (!cts.IsCancellationRequested) { Console.WriteLine($"This is {name}成功结束 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); } else { Console.WriteLine($"This is {name}中途中止 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); return; } } catch (Exception ex) { Console.WriteLine(ex.Message); cts.Cancel(); } }, cts.Token)); //加参数cts.Token目的是:在Cancel时尚未启动的任务,就不启动了。 //可是全部没有启动的任务都会抛出一个异常cts.Token.ThrowIfCancellationRequested } //1 准备cts 2 try-catch-cancel 3 Action要随时判断IsCancellationRequested //尽快中止,确定有延迟,在判断环节才会结束 Task.WaitAll(taskList.ToArray()); //若是线程还没启动,能不能就别启动了?加参数cts.Token //1 启动线程传递Token 2 异常抓取 //在Cancel时尚未启动的任务,就不启动了;也是抛异常,cts.Token.ThrowIfCancellationRequested } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } } catch (Exception ex) { Console.WriteLine(ex.Message); } } #endregion 线程取消
CancellationTokenSource有个bool属性IsCancellationRequested,初始化是false,调用Cancel方法后变成true(不能再变回去),能够重复Cancel。cts是线程安全的。异步
值得一提的是,使用Task.Run启动线程的时候还传了一个cts.Token的参数,目的是:调用Cancel方法后尚未启动的任务,就不启动了,实现原理是全部没有启动的任务都会抛出一个System.Threading.Tasks.TaskCanceledException类型的异常,异常描述为“已取消一个任务”,抛出异常后任务天然也就终止了。通常状况下咱们不会主动的去捕获这种异常,this
那若是想看到这种异常信息的话能够经过Task.WaitAll(taskList.ToArray())加上try{...}catch (AggregateException aex){...}这种方式去捕获该类型的异常。
PS:能够发现上面的这段代码在线程内部的地方加了一个异常捕获,工做中常规建议:多线程的委托里面不容许异常,包一层try-catch,而后记录下来异常信息,完成须要的操做。
注意:此处的线程中止也只能说是尽快中止,确定有延迟,在判断环节才会结束。
#region 多线程的临时变量问题 { //多线程的临时变量问题,线程是非阻塞的,延迟启动的;线程执行的时候,i已是5了。 for (int i = 0; i < 5; i++) { Task.Run(() => { //此处i都是5 Console.WriteLine($"This is btnThreadCore_Click_{i} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); } //k是闭包里面的变量,每次循环都有一个独立的k //5个k变量 1个i变量 for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { Console.WriteLine($"This is btnThreadCore_Click_{i}_{k} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); } } #endregion 多线程的临时变量问题
运行结果以下:
线程安全:若是你的代码在进程中有多个线程同时运行这一段,若是每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。
线程安全问题通常都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要是多线程都能访问和修改的就有多是非线程安全。
非线程安全是由于多个线程相同操做,出现了覆盖,那要怎么解决?
方案1:使用lock解决多线程冲突(如今通常不推荐使用这个,会限制并发)
lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着。
推荐锁是private static readonly object lockObj = new object();
首先咱们来看下lock的标准写法:
//字段 private static readonly object lockObj = new object(); private int iNumSync = 0; private int iNumAsync = 0; //非线程安全 private int iNumLockAsync = 0; private List<int> iListAsync = new List<int>();
{ for (int i = 0; i < 10000; i++) { this.iNumSync++; //单线程 } for (int i = 0; i < 10000; i++) { Task.Run(() => { this.iNumAsync++; //非线程安全 }); } for (int i = 0; i < 10000; i++) { Task.Run(() => { //lock的标准写法 //推荐锁是private static readonly object lockObj = new object(); lock (lockObj) //任意时刻只有一个线程能进入方法块,这不就变成了单线程,限制了并发 { this.iNumLockAsync++; } }); } for (int i = 0; i < 10000; i++) { int k = i; Task.Run(() => this.iListAsync.Add(k)); //非线程安全 } Thread.Sleep(5 * 1000); Console.WriteLine($"iNumSync={this.iNumSync} iNumAsync={this.iNumAsync} iNumLockAsync={iNumLockAsync} listNum={this.iListAsync.Count}"); //结果:iNumSync=1000 、 iNumAsync=1到1000之间 、 iNumLockAsync=1000 、 this.iListAsync.Count=1到1000之间 }
运行结果以下:
使用lock虽然能够解决线程安全问题,可是同时也限制了并发。
使用lock的注意点:
A 不能是lock(null),能够编译但不能运行;
B 不推荐lock(this),外面若是也要用实例,就冲突了;
C 不该该是lock(string字符串),string在内存分配上是重用的,会冲突;
D lock里面的代码不要太多,这里是单线程的;
下面咱们来看些例子:
为何不推荐lock(this)?
public class Test { private int iDoTestNum = 0; private string name = "浪子天涯"; /// <summary> /// 锁this会和外部锁对象实例冲突 /// </summary> public void DoTest() { //递归调用,lock (this) 会不会死锁? 正确答案是不会死锁! //这里是同一个线程,这个引用就是被这个线程所占据。 lock (this) { Thread.Sleep(500); this.iDoTestNum++; if (this.iDoTestNum < 10) { Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}"); this.DoTest(); } else { Console.WriteLine("28号,课程结束!!"); } } } /// <summary> /// 这次锁字符串会和外部锁值相同的字符串冲突 /// 这是由于相同的字符串会被指向同一块引用,这就至关于锁同一个引用,即同一个锁 /// </summary> public void DoTestString() { //这次不会死锁 //这里是同一个线程,这个引用就是被这个线程所占据。 lock (this.name) { Thread.Sleep(500); this.iDoTestNum++; if (this.iDoTestNum < 10) { Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}"); this.DoTestString(); } else { Console.WriteLine("28号,课程结束!!"); } } } }
#region 线程安全和锁lock { //线程安全:若是你的代码在进程中有多个线程同时运行这一段,若是每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。 //线程安全问题通常都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要是多线程都能访问和修改的就有多是非线程安全。 //非线程安全是由于多个线程相同操做,出现了覆盖,那要怎么解决? //一、使用lock解决多线程冲突 //lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着。 //推荐锁是private static readonly object lockObj = new object(); //A 不能是lock(null),能够编译但不能运行; //B 不推荐lock(this),外面若是也要用实例,就冲突了; //C 不该该是lock(string字符串),string在内存分配上是重用的,会冲突; //D lock里面的代码不要太多,这里是单线程的; Test test = new Test(); Task.Delay(1000).ContinueWith(t => { lock (test) //和Test内部的lock(this)是同一个锁,故这次尽管是子线程也要排队等待 { Console.WriteLine("*********lock(this) Start*********"); Thread.Sleep(2000); Console.WriteLine("*********lock(this) End*********"); } }); test.DoTest(); } #endregion 线程安全和锁lock
运行结果以下:
仔细观察会发现Task子线程的任务会等到test.DoTest()的任务执行完后才会执行,这是为何呢?
有些人可能就会有疑问了,此处锁this和锁test实例看上去应该是2把锁,互不影响才对啊,那为何又会冲突呢?
实际上此处的this和test是同一个实例,那么锁的固然也是同一个引用,故至关因而同一把锁。
那又为何不该该锁string字符串呢?
咱们在上面的例子上作一些调整以下所示:
#region 线程安全和锁lock { //线程安全:若是你的代码在进程中有多个线程同时运行这一段,若是每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。 //线程安全问题通常都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要是多线程都能访问和修改的就有多是非线程安全。 //非线程安全是由于多个线程相同操做,出现了覆盖,那要怎么解决? //一、使用lock解决多线程冲突 //lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着。 //推荐锁是private static readonly object lockObj = new object(); //A 不能是lock(null),能够编译但不能运行; //B 不推荐lock(this),外面若是也要用实例,就冲突了; //C 不该该是lock(string字符串),string在内存分配上是重用的,会冲突; //D lock里面的代码不要太多,这里是单线程的; { // Test test = new Test(); // Task.Delay(1000).ContinueWith(t => // { // lock (test) //和Test内部的lock(this)是同一个锁,故这次尽管是子线程也要排队等待 // { // Console.WriteLine("*********lock(this) Start*********"); // Thread.Sleep(2000); // Console.WriteLine("*********lock(this) End*********"); // } // }); // test.DoTest(); } { Test test = new Test(); string student = "浪子天涯"; Task.Delay(1000).ContinueWith(t => { lock (student) { Console.WriteLine("*********lock(string) Start*********"); Thread.Sleep(2000); Console.WriteLine("*********lock(string) End*********"); } }); test.DoTestString(); } } #endregion 线程安全和锁lock
运行结果以下:
仔细观察会发现这和lock(this)的效果是同样的,那这又是为何呢?
这是因为C#内存分配致使的,相同的字符串会被指向同一块引用空间,那么此处的锁this.name变量和锁student变量就至关于锁同一个引用,故至关因而同一把锁。
方案2:线程安全集合
使用System.Collections.Concurrent.ConcurrentQueue<int>等相关操做,System.Collections.Concurrent命名空间下的相关操做是线程安全的。
方案3:数据分拆,避免多线程操做同一个数据,又安全又高效(推荐)
在真实工做中遇到线程不安全的状况,若是有办法使用数据分拆来解决则推荐使用数据分拆,数据分拆没法解决的时候再考虑使用锁。
Demo源码:
连接:https://pan.baidu.com/s/1Eaet92HhGoK9sHjXhz_VsA 提取码:7st0
此文由博主精心撰写转载请保留此原文连接:https://www.cnblogs.com/xyh9039/p/13592042.html