那么什么时候能用多线程? 任务能并发的时候
多线程能干嘛?提升速度/优化用户体验
网站首页:A数据库 B接口 C分布式服务 D搜索引擎,适合多线程并发,都完成后才能返回给用户,需要等待WaitAll
列表页:核心数据可能来自数据库/接口服务/分布式搜索引擎/缓存,多线程并发请求,哪个先完成就用哪个结果,其他的就不管了
现实实例
多人合作开发---多线程--提升效率/性能
1 { 2 TaskFactory taskFactory = new TaskFactory(); 3 List<Task> taskList = new List<Task>(); 4 taskList.Add(taskFactory.StartNew(o=> Coding("A", " Portal"), "A")); 5 taskList.Add(taskFactory.StartNew(o=> Coding("B", " DBA"), "B")); 6 taskList.Add(taskFactory.StartNew(o=> Coding("C", " Client"), "C")); 7 taskList.Add(taskFactory.StartNew(o=> Coding("D", "Service"), "D")); 8 taskList.Add(taskFactory.StartNew(o=> Coding("E", " Wechat"), "E")); 9 10 //谁第一个完成,获取一个红包奖励 11 taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"{t.AsyncState}开发完成,获取个红包奖励{Thread.CurrentThread.ManagedThreadId.ToString("00")}")); 12 //实战作业完成后,一起庆祝一下 13 taskList.Add(taskFactory.ContinueWhenAll(taskList.ToArray(), rArray => Console.WriteLine($"开发都完成,一起庆祝一下{Thread.CurrentThread.ManagedThreadId.ToString("00")}"))); 14 //ContinueWhenAny ContinueWhenAll 非阻塞式的回调;而且使用的线程可能是新线程,也可能是刚完成任务的线程,唯一不可能是主线程 15 16 17 //阻塞当前线程,等着任意一个任务完成 18 Task.WaitAny(taskList.ToArray());//也可以限时等待 19 Console.WriteLine("准备环境开始部署"); 20 //需要能够等待全部线程完成任务再继续 阻塞当前线程,等着全部任务完成 21 Task.WaitAll(taskList.ToArray()); 22 Console.WriteLine("5个模块全部完成后,集中调试"); 23 24 //Task.WaitAny WaitAll都是阻塞当前线程,等任务完成后执行操作 25 //阻塞卡界面,是为了并发以及顺序控制 26 }
1 /// <summary> 2 /// 模拟Coding过程 3 /// </summary> 4 /// <param name="name"></param> 5 /// <param name="projectName"></param> 6 private static string Coding(string name, string projectName) 7 { 8 Console.WriteLine($"****************Coding Start {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 9 long lResult = 0; 10 for (int i = 0; i < 1_000_000_000; i++) 11 { 12 lResult += i; 13 } 14 Console.WriteLine($"****************Coding End {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************"); 15 return name; 16 }
多线程异常处理
1 #region 多线程异常处理 2 { 3 try 4 { 5 6 List<Task> taskList = new List<Task>(); 7 for (int i = 0; i < 100; i++) 8 { 9 string name = $"btnThreadCore_Click_{i}"; 10 taskList.Add(Task.Run(() => 11 { 12 if (name.Equals("btnThreadCore_Click_11")) 13 { 14 throw new Exception("btnThreadCore_Click_11异常"); 15 } 16 else if (name.Equals("btnThreadCore_Click_12")) 17 { 18 throw new Exception("btnThreadCore_Click_12异常"); 19 } 20 else if (name.Equals("btnThreadCore_Click_38")) 21 { 22 throw new Exception("btnThreadCore_Click_38异常"); 23 } 24 Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); 25 })); 26 } 27 //多线程里面抛出的异常,会终结当前线程;但是不会影响别的线程; 28 //那线程异常哪里去了? 被吞了, 29 //假如我想获取异常信息,还需要通知别的线程 30 Task.WaitAll(taskList.ToArray());//1 可以捕获到线程的异常 31 } 32 catch (AggregateException aex)//2 需要try-catch-AggregateException 33 { 34 foreach (var exception in aex.InnerExceptions) 35 { 36 Console.WriteLine(exception.Message); 37 } 38 } 39 catch (Exception ex)//可以多catch 先具体再全部 40 { 41 Console.WriteLine(ex); 42 } 43 //线程异常后经常是需要通知别的线程,而不是等到WaitAll,问题就是要线程取消 44 //工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作 45 } 46 #endregion
多线程里面抛出的异常,会终结当前线程;但是不会影响别的线程;线程异常哪里去了? 被吞了
多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息 ,通知别的线程
线程取消
1 { 2 CancellationTokenSource cts = new CancellationTokenSource(); 3 var token = cts.Token; cts.Cancel(); 4 CancellationTokenSource cts2 = new CancellationTokenSource(); 5 var token2 = cts2.Token; 6 List<Task> taskList = new List<Task>(); 7 for (int i = 0; i < 10; i++) 8 { 9 int k = i; 10 switch (i%5) 11 { 12 case 0: 13 taskList.Add(Task.Run(() => { Console.WriteLine($"i={i},k={k},i%5=0"); }));break; 14 case 1: 15 taskList.Add(Task.Run(() => { Console.WriteLine($"i={i},k={k},i%5=1"); },token)); break; 16 case 2: 17 taskList.Add(Task.Run(() => { Console.WriteLine($"i={i},k={k},i%5=2"); }, token2)); break; 18 case 3: 19 taskList.Add(Task.Run(() => { Console.WriteLine($"i={i},k={k},i%5=3"); })); break; 20 case 4: 21 taskList.Add(Task.Run(() => { Console.WriteLine($"i={i},k={k},i%5=4"); 22 throw new Exception("throw new Exception"); 23 })); break; 24 } 25 } 26 //Thread.Sleep(500); 27 cts2.Cancel(); 28 try 29 { 30 Task.WaitAll(taskList.ToArray()); 31 }catch(AggregateException ae) 32 { 33 foreach (var item in ae.InnerExceptions) 34 { 35 Console.WriteLine($"{item.GetType().Name}:{item.Message}"); 36 } 37 } 38 Console.WriteLine("**********************************"); 39 foreach (var item in taskList) 40 { 41 Console.WriteLine($"Id:{item.Id},Status:{item.Status}"); 42 if (item.Exception != null) 43 { 44 foreach (var ex in item.Exception.InnerExceptions) 45 { 46 Console.WriteLine($"{ex.GetType().Name}:{ex.Message}"); 47 } 48 } 49 } 50 }
运行上面的代码,有四个任务被取消,取消注释,则有两个任务被取消
线程安全
如果你的代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的
线程安全问题一般都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要多线程都能访问和修改
Lock
1、Lock解决多线程冲突
Lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着
推荐锁是private static readonly object
A 不能是Null,可以编译不能运行;
B 不推荐lock(this),外面如果也要用实例,就冲突了
1 public class LockHelper 2 { 3 public void Show() 4 { 5 LockTest test = new LockTest(); 6 Console.WriteLine(DateTime.Now); 7 Task.Delay(10).ContinueWith(t => 8 { 9 lock (test) 10 { 11 Console.WriteLine($"*********Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}********"); 12 Thread.Sleep(5000); 13 Console.WriteLine($"*********End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}********"); 14 } 15 }); 16 test.LockThis(); 17 } 18 } 19 public class LockTest 20 { 21 private int lockthis; 22 public void LockThis() 23 { 24 lock (this) 25 //递归调用,lock this 会不会死锁? 不会死锁! 26 //这里是同一个线程,这个引用就是被这个线程所占据 27 { 28 Thread.Sleep(1000); 29 this.lockthis++; 30 if (this.lockthis < 10) 31 this.LockThis(); 32 else 33 Console.WriteLine($"This is {nameof(LockThis)}:{this.lockthis} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); 34 } 35 } 36 }
这里LockThis自身递归调用不会死锁,这个引用被当前线程占用,但当另外的实例要使用时就冲突了,必须等待LockThis执行完成后,释放当前实例,外面的实例才能被调用
C 不应该是string; string在内存分配上是重用的,会冲突
1 { 2 LockTest test = new LockTest(); 3 Console.WriteLine(DateTime.Now); 4 string lockString = "lockString"; 5 Task.Delay(1000).ContinueWith(t => 6 { 7 lock (lockString) 8 { 9 Console.WriteLine($"****lockString Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}********"); 10 Thread.Sleep(5000); 11 Console.WriteLine($"****lockString End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}********"); 12 } 13 }); 14 test.LockString(); 15 } 16 public class LockTest 17 { 18 private int lockthis; 19 public void LockThis() 20 { 21 lock (this) 22 //递归调用,lock this 会不会死锁? 不会死锁! 23 //这里是同一个线程,这个引用就是被这个线程所占据 24 { 25 Thread.Sleep(1000); 26 this.lockthis++; 27 if (this.lockthis < 10) 28 this.LockThis(); 29 else 30 Console.WriteLine($"This is {nameof(LockThis)}:{this.lockthis} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); 31 } 32 } 33 34 private string lockString= "lockString"; 35 public void LockString() 36 { 37 lock (lockString) 38 { 39 Thread.Sleep(2000); 40 Console.WriteLine($"This is {nameof(LockString)}:{this.lockString} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); 41 Thread.Sleep(2000); 42 } 43 } 44 }
String类型在内存分配上按享元模式设计的,某个字符串被占用,其他线程就必须等待字符串释放后才能使用
D Lock里面的代码不要太多,这里是单线程的
2、线程安全集合
System.Collections.Concurrent.ConcurrentQueue<T>
3、 数据分拆,避免多线程操作同一个数据;又安全又高效
1 private int _sync = 0; 2 private int _async = 0; 3 private List<int> listInt = new List<int>(); 4 private static readonly object lockObject = new object(); 5 public void LockObject() 6 { 7 for (int i = 0; i < 1000; i++) 8 { 9 this._sync++; 10 } 11 for (int i = 0; i < 1000; i++) 12 { 13 Task.Run(() => this._async++); 14 } 15 for (int i = 0; i < 1000; i++) 16 { 17 int k = i; 18 Task.Run(() => this.listInt.Add(k)); 19 } 20 Thread.Sleep(5 * 1000); 21 Console.WriteLine($"_sync={this._sync} _async={this._async} listInt={this.listInt.Count}"); 22 }
运行上面的代码发现_sync=1000 _async与listInt集合个数都少于1000
1 public void LockObject() 2 { 3 for (int i = 0; i < 1000; i++) 4 { 5 this._sync++; 6 } 7 for (int i = 0; i < 1000; i++) 8 { 9 Task.Run(() => { 10 lock (lockObject) 11 { 12 this._async++; 13 } 14 }); 15 } 16 for (int i = 0; i < 1000; i++) 17 { 18 int k = i; 19 Task.Run(() => { 20 lock (lockObject) 21 { 22 this.listInt.Add(k); 23 } 24 }); 25 } 26 Thread.Sleep(5 * 1000); 27 Console.WriteLine($"_sync={this._sync} _async={this._async} listInt={this.listInt.Count}"); 28 }
使用lock包装后 _async与listInt集合个数都为1000, 使用lock后 只有一个线程才能进入lock方法块内,相当于把程序又变回了单线程
微软文档:
lock:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/lock-statement
CancellationTokenSource:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.cancellationtokensource?view=netframework-4.8
CancellationToken:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.cancellationtoken?view=netframework-4.8