1.进程、线程、多线程、计算机概念
【进程】:一个程序运行时占用的全部计算资源的总和 【线程】:程序执行流最小单位,依托于进程,一个进程可以包含多个线程
【多线程】:多个执行流同时运行,主要分为两种:其一CPU运算太快啦,分时间片——上下文切换(加载环境—计算—保存环境) 微观角度讲,一个核同一时刻只能执行一个线程;宏观的讲是多线程并发。其二,多CPU多核,可以独立工作,4核8线程——核指物理的核,线程指虚拟核
多线程三大特点:不卡主线程、速度快、无序性 【Thread】:C#语言对线程对象的封装
2.同步与异步 【同步】:完成计算之后,再进入下一行,会阻塞 【异步】:不会等待方法的完成,直接进入下一行,不会阻塞
Action<string> action = this.DoSomethingLong; //带一个string参数的耗时方法 action.Invoke("btnAsync_Click_1"); //同步 action("btnAsync_Click_2"); //同步 action.BeginInvoke("btnAsync_Click_3", null, null); //异步
3.不同C#版本线程的进化升级
C#1.0 | Thread | 线程等待、回调、前后台线程,可以扩展Thread封装回调,各种API函数很多,可设置线程优先级,各种API包括:Start(),Suspend(),Resume(),Abort(),Thread.ResetAbort(),Join(), |
C#2.0 | ThreadPool | 线程池使用,设置线程池,等待可用ManualResetEvent,砍掉了很多API,可以设置线程池的最大最小数量 |
C#3.0 | Task |
4.并发编程
并发编程包括【多线程】、【并行处理】、【异步编程】、【响应式编程】
5.Task启动
5.1为什么要有task
Task = Thread + ThreadPool
Thread:容易造成时间+空间开销,而且使用不当,容易造成线程过多,导致时间片切换...
ThreadPool:控制能力比较弱,做thread的延续、阻塞、取消、超时等功能较弱,ThreadPool的控制权在CLR而不在编程者...
Task看起来更像是一个Thread...但是在ThreadPool的基础上进行的封装
.net4.0之后微软极力推荐使用task进行异步运算
引用using System.Threading C#3.0才有、基于ThreadPool
WinDbg: .loadby sos clr !threads
private void DoSomethingLong(string name) { Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1000000000; i++) { lResult += i; } Console.WriteLine($"****************DoSomethingLong {name} End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************"); }
1 Task.Run(() => this.DoSomethingLong("btnTask_Click1")); 2 Task.Run(() => this.DoSomethingLong("btnTask_Click2"));
TaskFactory taskFactory = Task.Factory;//4.0 taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click3"));
new Task(() => this.DoSomethingLong("btnTask_Click4")).Start();
以上三种启动方式并无区别,都是返回一个Task
Run(Action)不具有参数,没有返回值
Task.Run(() => { string name = "无名方法"; Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1000000000; i++) { lResult += i; } Console.WriteLine($"****************DoSomethingLong {name} End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************"); });
Console.WriteLine($"这是Task1单击事件Start 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); Task.Run(()=> { Console.WriteLine($"这是Tsak.Run(Action)无名方法Start 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); Random random = new Random(); Thread.Sleep(random.Next(1000,5000)); Console.WriteLine($"这是Tsak.Run(Action)无名方法End 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); }); Console.WriteLine($"这是Task1单击事件End 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
6.多线程阻塞-----Task.WaitAll
设置一场景,睡觉之后要吃饭
6.1不阻塞多线程,就会出现还没睡醒就吃饭的情况
List<Task> listTask = new List<Task>(); listTask.Add(Task.Run(()=> SleepMethod("杨三少"))); listTask.Add(Task.Run(() => SleepMethod("牛大帅"))); listTask.Add(Task.Run(() => SleepMethod("猪宝宝"))); Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
6.2阻塞多线程,使用Task.WaitAll(Task[]),会阻塞当前线程,等着全部任务都完成后,才进入下一行
List<Task> listTask = new List<Task>(); listTask.Add(Task.Run(()=> SleepMethod("杨三少"))); listTask.Add(Task.Run(() => SleepMethod("牛大帅"))); listTask.Add(Task.Run(() => SleepMethod("猪宝宝"))); Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
6.2.1使用Task.WaitAll如果想不阻塞线程,可以使用Task套用Task
Task.Run(() => {//Task套Task,可以不阻塞 List<Task> listTask = new List<Task>(); listTask.Add(Task.Run(() => DoSomethingLong("杨三少"))); listTask.Add(Task.Run(() => DoSomethingLong("牛大帅"))); listTask.Add(Task.Run(() => DoSomethingLong("猪宝宝"))); Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 Console.WriteLine($"全部都醒了 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); });
6.3设置最多等待时长,设置最长等待时间,可以看到,杨三少还未睡醒就开吃,谁让杨三少睡的太久了呢,哈哈
List<Task> listTask = new List<Task>(); listTask.Add(Task.Run(()=> SleepMethod("杨三少"))); listTask.Add(Task.Run(() => SleepMethod("牛大帅"))); listTask.Add(Task.Run(() => SleepMethod("猪宝宝"))); //Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 Task.WaitAll(listTask.ToArray(),2500);//最多等待2.5秒,超过2.5秒就不再进行等待 Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
6.4等待汇总
7.多线程阻塞-----Task.WaitAny
List<Task> listTask = new List<Task>(); listTask.Add(Task.Run(()=> SleepMethod("杨三少"))); listTask.Add(Task.Run(() => SleepMethod("牛大帅"))); listTask.Add(Task.Run(() => SleepMethod("猪宝宝"))); //Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 //Task.WaitAll(listTask.ToArray(),2500);//最多等待2.5秒,超过2.5秒就不再进行等待 Task.WaitAny(listTask.ToArray());//阻塞线程,只要只要有一个宝宝睡醒就开吃,“早起的宝宝有饭吃” Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
“这次是杨三少醒的最早,所以他就可以先吃辣”
8.多线程不阻塞----Task.WhenAll(Task[]).ContinueWith() Task.WhenAny(Task[]).ContinueWith()
List<Task> listTask = new List<Task>(); listTask.Add(Task.Run(()=> DoSomethingLong("杨三少"))); listTask.Add(Task.Run(() => DoSomethingLong("牛大帅"))); listTask.Add(Task.Run(() => DoSomethingLong("猪宝宝"))); //Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 //Task.WaitAll(listTask.ToArray(),2500);//最多等待2.5秒,超过2.5秒就不再进行等待 //Task.WaitAny(listTask.ToArray());//阻塞线程,只要只要有一个宝宝睡醒就开吃,“早起的宝宝有饭吃” Task.WhenAny(listTask.ToArray()).ContinueWith(t=> { Console.WriteLine($"第一个已经醒了 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); }); Task.WhenAll(listTask.ToArray()).ContinueWith(t=> { Console.WriteLine($"全部都醒了 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); });
ContinueWith类似于一个回调式的,ContinueWith里面的t就是前面的Task,也就是使用ContinueWith相当于一个回调,当执行了这个Task之后会自动执行ContinueWith里的后续方法。
9.仅用11个线程完成10000个任务
List<int> list = new List<int>(); for (int i = 0; i < 10000; i++) { list.Add(i); } //完成10000个任务 但是只要11个线程 Action<int> action = i => { Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00")); Thread.Sleep(new Random(i).Next(100, 300)); }; List<Task> taskList = new List<Task>(); foreach (var i in list) { int k = i; taskList.Add(Task.Run(() => action.Invoke(k))); if (taskList.Count > 10) { Task.WaitAny(taskList.ToArray());//只要有一个完成,重新整理集合 taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList(); } } Task.WhenAll(taskList.ToArray());
10.使用TaskFactory可以知道已完成线程的标识
TaskFactory taskFactory = new TaskFactory(); List<Task> taskList = new List<Task>(); taskList.Add(taskFactory.StartNew(o => DoSomethingLong("一一一"), "一一一")); taskList.Add(taskFactory.StartNew(o => DoSomethingLong("二二二"), "二二二")); taskList.Add(taskFactory.StartNew(o => DoSomethingLong("三三三"), "三三三")); taskFactory.ContinueWhenAny(taskList.ToArray(), t => { Console.WriteLine($"【{t.AsyncState}】任务已完成 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); taskFactory.ContinueWhenAll(taskList.ToArray(),t=> { Console.WriteLine($"所有任务已完成,第一个是{t[0].AsyncState} 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); });
如果不用TaskFactory,直接用Task,可以通过做个子类的方式来搞。
11.Task中两种常见的枚举
11.1 Task生成时的枚举 public Task(Action action, TaskCreationOptions creationOptions);
11.1.1 AttachedToParent :建立了父子关系,父任务想要继续执行,必须等待子任务执行完毕
/* task是父task,task1和task2是子task * 如果不加AttachedToParent,三个线程的执行没有先后顺序 * 如果加AttachedToParent,必须等到task1和task2执行完毕后再执行task */ Task task = new Task(() => { Task task1 = new Task(() => { Thread.Sleep(100); Debug.WriteLine("task1"); }, TaskCreationOptions.AttachedToParent); Task task2 = new Task(() => { Thread.Sleep(10); Debug.WriteLine("task2"); }, TaskCreationOptions.AttachedToParent); task1.Start(); task2.Start(); }); task.Start(); task.Wait(); //task.WaitAll(task1,task2); Debug.WriteLine("我是主线程!!!!");
11.1.2 DenyChildAttach: 不让子任务附加到父任务上去,即使子任务加了AttachedToParent ,父任务如果加上DenyChildAttach的话也无济于事,还是各自执行各自的,没有先后顺序
/* task是父任务,task1和task2是子任务 * 如果不加AttachedToParent,三个线程的执行没有先后顺序 * 如果加AttachedToParent,必须等到task1和task2执行完毕后再执行task * 如果父任务加了DenyChildAttach,还是各自执行各自的 */ Task task = new Task(() => { Task task1 = new Task(() => { Thread.Sleep(100); Debug.WriteLine("task1"); }, TaskCreationOptions.AttachedToParent); Task task2 = new Task(() => { Thread.Sleep(10); Debug.WriteLine("task2"); }, TaskCreationOptions.AttachedToParent); task1.Start(); task2.Start(); },TaskCreationOptions.DenyChildAttach); task.Start(); task.Wait(); //task.WaitAll(task1,task2); Debug.WriteLine("我是主线程!!!!");
11.1.3 HideScheduler: 子任务默认不使用父类的Task的Scheduler,而是使用默认的
11.1.4 LongRunning:如果你明知道是长时间运行的任务,建议你使用此选项,建议使用 “Thread” 而不是“threadPool(租赁公司)"
如果长期租用不还给threadPool,threadPool为了满足市场需求,会新开一些线程,满足当前使用,如果此时租用线程被归还,这会导致ThreadPool的线程过多,销毁和调度都是一个很大的麻烦
if ((task.Options & TaskCreationOptions.LongRunning) != 0) { Thread thread = new Thread(s_longRunningThreadWork); thread.IsBackground = true; thread.Start(task); } else { bool forceGlobal = (task.Options & TaskCreationOptions.PreferFairness) != TaskCreationOptions.None; ThreadPool.UnsafeQueueCustomWorkItem(task, forceGlobal); }
11.1.5 PreferFairness: 给你的感觉就是一个”queue队列“的感觉,如果指定PreferFairness会将Task放入到ThreadPool的全局队列中,让work thread进行争抢,默认情况会放到task的一个本地队列中
11.2 任务延续中使用的枚举 public Task ContinueWith(Action<Task> continuationAction, TaskContinuationOptions continuationOptions); 用在ContinuWith中
11.2.1 不使用取消和任务延续枚举,任务依次执行,分别是task1,task2,task3
Task task1 = new Task(() => { Thread.Sleep(1000); Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); }); var task2 = task1.ContinueWith(t => { Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); }); var task3 = task2.ContinueWith(t => { Debug.WriteLine("task3 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); }); task1.Start(); /* 输出 task1 tid=10, dt=02/26/2021 10:11:09 task2 tid=11, dt=02/26/2021 10:11:09 task3 tid=10, dt=02/26/2021 10:11:09 */
11.2.2 添加取消,取消task2,未指定任务延续枚举
添加取消,不加枚举的任务延续
11.2.3 TaskContinuationOptions.LazyCancellation,需要等待task1执行完成之后再判断source.token的状态
CancellationTokenSource source = new CancellationTokenSource(); //定义一个任务取消对象 source.Cancel(); //执行任务取消 Task task1 = new Task(() => { Thread.Sleep(1000); Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); }); var task2 = task1.ContinueWith(t => { Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); },source.Token,TaskContinuationOptions.LazyCancellation,TaskScheduler.Current); //即使task1被cancel,延续链依旧没断,只是task2不会执行,先执行task1,再执行task3 var task3 = task2.ContinueWith(t => { Thread.Sleep(100); Debug.WriteLine("task3 tid={0}, dt={1} task2状态:{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"),task2.Status); }); task1.Start(); /* 输出 task1 tid=10, dt=10:36:12.363 task3 tid=11, dt=10:36:12.468 task2状态:Canceled */ /* 说明 需要等待task1执行完成之后再判断source.token的状态 这样的话,就形成了一条链: task1 -> task2 -> task3... */
11.2.4 TaskContinuationOptions.ExecuteSynchronously,这个枚举就是希望执行前面那个task的thread也在执行本延续任务,可以防止线程切换
Task task1 = new Task(() => { Thread.Sleep(1000); Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); }); var task2 = task1.ContinueWith(t => { Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); },TaskContinuationOptions.ExecuteSynchronously); //task2用task1的线程去执行 task1.Start(); /* 输出 task1 tid=6, dt=10:47:25.189 task2 tid=6, dt=10:47:25.192 */ /* 说明 * task2 也希望使用 task1的线程去执行,这样可以防止线程切换。。。 */
11.2.5 TaskContinuationOptions.OnlyOnRanToCompletion 表示延续任务必须在前面task完成状态才能执行
Task task1 = new Task(() => { Thread.Sleep(1000); Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); throw new Exception("抛出异常"); }); var task2 = task1.ContinueWith(t => { Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); },TaskContinuationOptions.OnlyOnRanToCompletion); //仅在task1正常完成时执行task2,如果task1异常了,task2就不会运行 task1.Start();
11.2.6 TaskContinuationOptions.NotOnRanToCompletion 表示延续任务必须在前面task非完成状态才能执行,其余类似
NotOnRanToCompletion | 延续任务必须在前面task【非完成状态】才能执行 |
NotOnFaulted | 延续任务必须在前面task【非报错状态】才能执行 |
OnlyOnCanceled | 延续任务必须在前面task【已取消状态】才能执行 |
NotOnCanceled | 延续任务必须在前面task【没有取消状态】才能执行 |
OnlyOnFaulted | 延续任务必须在前面task【报错状态】才能执行 |
OnlyOnRanToCompletion | 延续任务必须在前面task【已完成状态】才能执行 |
12.Task有返回值
12.1 Task<TResult> 继承于Task
Task<int> task1 = Task.Factory.StartNew(() => { //... Thread.Sleep(1000); return 1; }); Debug.WriteLine(task1.Result);
12.2 ContinueWith<TResult>
Task<int> task1 = Task.Factory.StartNew(() => { //做一些逻辑运算 return 1; }); var task2 = task1.ContinueWith<string>(t => { int num = t.Result; var sum = num + 10; return sum.ToString(); }); Debug.WriteLine(task2.Result);
12.3 WhenAll<TResult> WhenAny<TResult>
Task<int> task1 = Task.Factory.StartNew(() => { //做一些逻辑运算 return 1; }); Task<int> task2 = Task.Factory.StartNew(() => { //做一些逻辑运算 return 2; }); Task.WhenAll<int>(new Task<int>[2] { task1, task2 }).ContinueWith<int>(t => { int sum = 0; foreach (var item in t.Result) { sum += item; } Debug.WriteLine(sum); return sum; }); /* 输出 3 * 同时用到了Continue<TResult>和WhenAll<TResult> */
12.4小结
Task.WaitAll(Task[]) | 阻塞,等待Task[]数组里的全部线程任务都完成后方可运行到下一步 |
Task.WaitAny(Task[]) | 阻塞,等待Task[]数组里的某一线程任务完成,方可运行到下一步 |
Task.WhenAll(Task[]).Continue | 不阻塞,等待Task[]数组里的全部线程任务都完成后执行回调 |
Task.WhenAny(Task[]).Continue | 不阻塞,等待Task[]数组里的某一线程任务完成后执行回调 |
12.Thread.Sleep(2000)与Task.Delay(2000)
Thread.Sleep(2000);//等待2s Task.Delay(2000);//延迟2s
12.1Thread.Sleep(2000)
Console.WriteLine($"这是Task1单击事件Start 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Thread.Sleep(2000);//等待 stopwatch.Stop(); Console.WriteLine(stopwatch.ElapsedMilliseconds); Console.WriteLine($"这是Task1单击事件End 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
12.2Task.Delay(2000)直接替换Thread.Sleep(2000)
Console.WriteLine($"这是Task1单击事件Start 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Task.Delay(2000);//延迟 stopwatch.Stop(); Console.WriteLine(stopwatch.ElapsedMilliseconds); Console.WriteLine($"这是Task1单击事件End 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
12.3Task.Delay(2000)的正确用法
当运行到某一处时,希望两秒后执行一段代码,但又不想阻塞后面的代码,可以用Task.Delay(2000).ContinueWith()
Console.WriteLine($"这是Task1单击事件Start 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Task.Delay(2000).ContinueWith(t => {//Task.Delay()的返回值仍然是Task, Task.ContinueWith() stopwatch.Stop(); Console.WriteLine(stopwatch.ElapsedMilliseconds); }); Console.WriteLine($"这是Task1单击事件End 当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
13.多线程中的异常处理
13.1 主线程的异常捕捉不到子线程的异常信息。所以新开辟的子线程要写属于自己的try...catch...
子线程如果不阻塞,主线程是捕捉不到。但最最好还是在子线程里书写自己的异常捕捉机制。
Console.WriteLine($"这是主线程单击事件Start 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); List<Task> listTask = new List<Task>(); try { for (int i = 0; i < 5; i++) { Action<string> action = t => { try { Thread.Sleep(100); if (t.Equals("for2")) { Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 执行异常"); throw new Exception($"这是{t},刨出异常"); } if (t.Equals("for3")) { Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 执行异常"); throw new Exception($"这是{t},刨出异常"); } Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 执行成功"); } catch (Exception ex) { Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 异常信息:{ex.Message}"); } }; string k = $"for{i}"; listTask.Add(Task.Run(() => action.Invoke(k))); } Task.WaitAll(listTask.ToArray()); } catch (Exception ex) { Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 异常信息:{ex.Message}"); } Console.WriteLine($"这是主线程单击事件End 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
13.2 AggregateException集合
当wait、TResult时会抛出异常;可以一个一个遍历异常,Handle遍历异常数组,需要上抛return false,不需要上抛return true
try { try { task.Wait(); } catch (AggregateException ex) { foreach (var item in ex.InnerExceptions) { Console.WriteLine($"{item.InnerException.Message} {item.GetType().Name}"); } ex.Handle(x => { if (x.InnerException.Message == "这是task1异常") { return true; //不往上抛 } else { return false; //往上抛 } }); } } catch (AggregateException ex) { foreach (var item in ex.InnerExceptions) {//输出 这是task2异常 Console.WriteLine($"{item.InnerException.Message} {item.GetType().Name}"); } }
14.线程取消
14.1 应用场景是多个线程并发执行,某个失败后,希望通知别的线程,都停下来;Task是外部无法中止的,Thread.Abort不要考虑因为不靠谱(原因是线程是OS的资源,无法掌控啥时候取消),在这里需要线程自己停止自己,需要用到公共的访问变量,所有的线程都会不断的去访问它,当某个线程执行异常时修改此公共变量(当然这中间有一定的延迟)。
CancellationTokenSource变量去标志任务是否取消,Cancel()方法用于取消,IsCancellationRequested属性作为每个线程执行的条件;其Token在启动Task的时候传入,如果某一时刻执行Cancel()方法,这个任务会自动放弃,并抛出一个异常。
Console.WriteLine($"这是主线程单击事件Start 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); List<Task> listTask = new List<Task>(); CancellationTokenSource cts = new CancellationTokenSource(); try { for (int i = 0; i < 5; i++) { Action<string> action = t => { try { Thread.Sleep(100); if (t.Equals("for2")) { Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 执行失败"); throw new Exception($"这是{t},刨出异常"); } if (t.Equals("for3")) { Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 执行失败"); throw new Exception($"这是{t},刨出异常"); } if (cts.IsCancellationRequested) { Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 放弃执行"); } else { Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 执行成功"); } } catch (Exception ex) { cts.Cancel(); Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 异常信息:{ex.Message}"); } }; string k = $"for{i}"; listTask.Add(Task.Run(() => action.Invoke(k),cts.Token)); } Task.WaitAll(listTask.ToArray()); } catch (Exception ex) { Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 异常信息:{ex.Message}"); } Console.WriteLine($"这是主线程单击事件End 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
14.2 Thread中的取消
var isStop = false; //定义一个公有变量 var thread = new Thread(() => { while (!isStop) { Thread.Sleep(100); Debug.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId); } }); thread.Start(); Thread.Sleep(1000); isStop = true; //多个线程操作公有变量,不用锁的话,存在Bug风险
14.3Thread中的中断
14.4 Task中用CancellationTokenSource取消任务
14.4.1 取消时做一些事情,希望有一个函数能够被触发,这个触发可以做一些资源的清理,又或者是更新数据库信息
CancellationTokenSource cts = new CancellationTokenSource(); cts.Token.Register(() => { //如果当前的token被取消,此函数将会被执行 Debug.WriteLine("当前source已经被取消,现在可以做资源清理了。。。。"); }); var task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested) { Thread.Sleep(100); Debug.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId); } }, cts.Token); Thread.Sleep(1000); cts.Cancel();
14.4.2 延时取消,可以用CancelAfter,也可以在构造函数中指定延时时长
CancellationTokenSource cts = new CancellationTokenSource(); var task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested) { Thread.Sleep(100); Debug.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId); } }, cts.Token); cts.CancelAfter(new TimeSpan(0, 0, 0, 1)); //延时1秒取消
14.4.3 取消组合,是and的逻辑关系
CancellationTokenSource source1 = new CancellationTokenSource(); //现在要让source1取消 source1.Cancel(); CancellationTokenSource source2 = new CancellationTokenSource(); var combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token); Debug.WriteLine("s1={0} s2={1} s3={2}", source1.IsCancellationRequested, source2.IsCancellationRequested, combineSource.IsCancellationRequested); /* 输出 s1=True s2=False s3=True */ /* 说明 * s3 = s1 && s2 */
14.4.4 ThrowIfCancellationRequested
ThrowIfCancellationRequested 比 IsCancellationRequested 多了throw。。。 如果一个任务被取消,我希望代码抛出一个异常。。。 if(IsCancellationRequested) throw new Exception("adasdaf"); == 等价操作 == throwIfCancellationRequested();
14.4.5 异步任务的超时机制
15.多线程中的临时变量
15.1直接引用for循环中的i
Console.WriteLine($"这是主线程单击事件Start 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); for (int i = 0; i < 5; i++) { Task.Run(() => { Console.WriteLine($"这是for循环,i={i}"); }); } Console.WriteLine($"这是主线程单击事件End 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
为什么都等于5,线程并没有阻塞,所以5次循环下来之后i就是5啦。
15.2直接引用循环外的局部变量
Console.WriteLine($"这是主线程单击事件Start 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); int k = 0; for (int i = 0; i < 5; i++) { k = i; new Action(() => { Console.WriteLine($"这是for循环,i={k}"); }).BeginInvoke(null, null); } Console.WriteLine($"这是主线程单击事件End 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
为什么都等于4,线程没有阻塞,其实最主要的原因是声明了一次k,全程只有一个k。
15.3在循环内每次声明赋值
Console.WriteLine($"这是主线程单击事件Start 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); for (int i = 0; i < 5; i++) { int k = i; new Action(() => { Console.WriteLine($"这是for循环,i={k}"); }).BeginInvoke(null, null); } Console.WriteLine($"这是主线程单击事件End 当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")} 当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
16.线程安全
共有变量(共享资源 )指:都能访问的局部变量、全局变量、数据库中的一个值、磁盘文件等,如果多个线程都去操作一个共有变量,可能会出现意外的结果,这就是多线程并不安全
16.1多线程并不安全
List<Task> listTask = new List<Task>(); int sum = 0; for (int i = 0; i < 10000; i++) { listTask.Add(Task.Run(() => { sum += 1; })); } Task.WhenAll(listTask.ToArray()).ContinueWith((t) => { Console.WriteLine($"最终结果:{sum}"); });
可见结果并不是意料之中的10000.
16.2加lock
//private:定义为私有,防止外界也去lock static:全场唯一 readonly:不要随意赋值 object表示引用类型 private static readonly object objLock = new object(); private void button1_Click(object sender, EventArgs e) { List<Task> listTask = new List<Task>(); int sum = 0; for (int i = 0; i < 10000; i++) { listTask.Add(Task.Run(() => { lock (objLock) { //lock后的方法块,任意时刻只有一个线程可以进入 //只能锁引用类型,相当于占用这个引用链接 //string虽然也是引用类型,但不能用string,因为什么享元模式 sum += 1; } })); } Task.WhenAll(listTask.ToArray()).ContinueWith((t) => { Console.WriteLine($"最终结果:{sum}"); }); }
可见结果是意料之中的10000
16.3有关lock的深思
lock虽然能解决线程安全的问题,同一时刻只能有一个线程可以运行lock后的程序块不并发,但是牺牲了性能,所以有两个建议:
其一、尽可能得缩小lock的范围;其二、尽量不要定义共有变量,可以通过数据拆分来避免冲突
17.async与await
await与async通常成对出现,async放在方法名前(private后面),await放在task对象前。如果只在方法名前加一个async,没有任何意义;如果只在方法内task前面加一个await,会报错。在await task后面的语句,类似于一个回调,方法运行到await task时会自动返回返回主线程继续运行,要等待子线程运行结束后才会继续运行这个回调,并且这个回调的线程是不确定的,可能是主线程,可能是子线程,也可能是其他线程。
不用用void作为async方法的返回类型,如果没有返回值,也要Task,如果有返回值,要返回Task<T>;仅限于编写事件处理程序需要返回void
17.1 类似于用一种同步的方法写异步。举例说在主线程中运行了一个task,并且希望在这个子线程task执行之后再运行另一个事件task1,那么就需要task.ContinueWith(task1),在这里task.ContinueWith(task1)就等于await task;task1,其实说白了就是ContinueWith=await
/* 使用swait/async可以把异步当成同步来使用 * 这个示例演示一个没有返回值的多线程任务,模拟人一天的动作 * 其执行结果的先后顺序是【按钮单击开始】【NoReturn方法Start】【开始吃饭】【吃饭结束】【开始工作】【工作结束】【工作结束】【开始睡觉】【NoReturn方法End】【按钮单击结束】 * 这个示例可能并无实际的意义,但只是用来模拟这种await/async的使用方法 * 不过请注意使用async的方法虽说没有返回值(并没有具体的return task),但其结果返回的是Task */ private async void button1_Click(object sender, EventArgs e) { Console.WriteLine($"ClickEventStart 【按钮单击开始】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); await NoReturn_AwaitAsync(); Console.WriteLine($"ClickEventEnd 【按钮单击结束】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); } private async static Task NoReturn_AwaitAsync() { Console.WriteLine($"【NoReturn方法Start】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); await Task.Run(()=> { Console.WriteLine($"NoReturn方法await之前 【开始吃饭】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(500); Console.WriteLine($"NoReturn方法await之后 【吃饭结束】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); await Task.Run(() => { Console.WriteLine($"NoReturn方法await之前 【开始工作】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(500); Console.WriteLine($"NoReturn方法await之后 【工作结束】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); Console.WriteLine($"NoReturn方法await之后 【开始睡觉】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Console.WriteLine($"【NoReturn方法End】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }
同样的方法从await/async换成Task.ContinueWith来一步步回调则显得代码很冗余
private static void NoReturn_Task_ContinueWith() { Console.WriteLine($"【NoReturn方法Start】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Task.Run(() => { Console.WriteLine($"NoReturn方法await之前 【开始吃饭】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(500); Console.WriteLine($"NoReturn方法await之后 【吃饭结束】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }).ContinueWith(t=> { Task.Run(() => { Console.WriteLine($"NoReturn方法await之前 【开始工作】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(500); Console.WriteLine($"NoReturn方法await之后 【工作结束】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }).ContinueWith(t1=> { Console.WriteLine($"NoReturn方法await之后 【开始睡觉】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Console.WriteLine($"【NoReturn方法End】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); }); }
17.2 task有返回值的场景
private void button1_Click(object sender, EventArgs e) { Console.WriteLine($"ClickEventStart 【按钮单击开始】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Task<int> task = Task.Run(() => DoSomething("杨三少")); int result = task.Result;//这一步子线程会阻塞主线程,因为主线程需要调用子线程的执行结果 Console.WriteLine($"子线程执行结果为{result}"); Console.WriteLine($"ClickEventEnd 【按钮单击结束】 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); } private int DoSomething(string name) { Console.WriteLine($"Task【{name}】 线程ID:{Thread.CurrentThread.ManagedThreadId} 是否在线程池中:{Thread.CurrentThread.IsThreadPoolThread}"); return 22; }
17.3 这两个关键词适合专用于处理一些文件IO,【潜规则】使用的是ThreadPool、IOStream
网络IO、文件IO都有一些异步方法 例如MemoryStream、FileStream、WebRequest
如果返回值是task,都可以使用await进行等待
而Task 则是最大限度的压榨 workThread
async static Task<string> GetString() { using (FileStream fs = new FileStream(Environment.CurrentDirectory + "//1.txt", FileMode.Open)) { var bytes = new byte[fs.Length]; var len = await fs.ReadAsync(bytes, 0, bytes.Length); var str = Encoding.Default.GetString(bytes, 0, bytes.Length); return str; } return ""; } //调用 Task<string> info = GetString(); Console.WriteLine(info.Result); //打印hello world
17.4 使用async和await的优缺点
优点:代码简洁,把异步的代码形式写成了同步方式; 提到开发效率
缺点:如果你用同步的思维去理解,容易出问题,返回值对不上;我们在编译器层面看到的代码,不见得是真的代码
17.5 async和await深层剖析
static async Task<string> Hello() { //主线程执行,底层还会调用一个 AwaitUnsafeOnCompleted 委托给线程池 Console.WriteLine("hello world"); //在工作线程中执行 var x = await Task.Run(() => { Console.WriteLine("i'm middle"); return "i'm ok"; }); Console.WriteLine("我是结尾哦:{0}", x); return x; } //调用 var info = Hello().Result; Console.WriteLine(info); Console.Read();
[AsyncStateMachine(typeof(<Hello>d__1))] [DebuggerStepThrough] private static Task<string> Hello() { <Hello>d__1 stateMachine = new <Hello>d__1(); stateMachine.<>t__builder = AsyncTaskMethodBuilder<string>.Create(); stateMachine.<>1__state = -1; stateMachine.<>t__builder.Start(ref stateMachine); return stateMachine.<>t__builder.Task; } //调用 string info = Hello().Result; Console.WriteLine(info); Console.Read();
//同等代码,内部新建一个状态机类 public class MyStateMachine : IAsyncStateMachine //异步方法的状态机接口 { public AsyncTaskMethodBuilder<string> t_builder; //异步方法生成器 public int state; //状态 初始-1,执行异步方法,完事后赋0,执行后续操作 private MyStateMachine machine = null; private TaskAwaiter<string> myawaiter; string result = string.Empty; public MyStateMachine() { } public void MoveNext() { try { switch (state) { case -1: Console.WriteLine("hello world"); var waiter = Task.Run(() => { Console.WriteLine("i'm middle"); return "i'm ok"; }).GetAwaiter(); state = 0; //设置下一个状态 myawaiter = waiter; machine = this; //丢给线程池执行了。。。 t_builder.AwaitUnsafeOnCompleted(ref waiter, ref machine); break; case 0: var j = myawaiter.GetResult(); Console.WriteLine("我是结尾哦:{0}", j); t_builder.SetResult(j); break; } } catch (Exception ex) { t_builder.SetException(ex); //设置t_builder的异常 } } public void SetStateMachine(IAsyncStateMachine stateMachine) { } }
static Task<string> Hello() { MyStateMachine machine = new MyStateMachine(); machine.t_builder = AsyncTaskMethodBuilder<string>.Create(); //包装器,相当于TaskCompletionSource<string> machine.state = -1; var t_builder = machine.t_builder; t_builder.Start(ref machine); return machine.t_builder.Task; //真正返回的还是Task<string> }
17.6 小结
AsyncTaskMethodBuilder 扮演了一个TaskcomplationSource一个角色,就是做Task的包装器
state:扮演者状态机状态的角色
AwaitUnsafeOnCompleted 这个函数是丢给线程池去执行的,当某一时刻执行结束,会调用Movenext
异步IO处理的流程 压榨IOthread:work thread: 是应用程序主动使用;IO thread: 是clr反向通知的。。
17.7 十月的寒流对异步编程的解释
1 async Task Main() 2 { 3 Helper.PrintThreadId("Before"); 4 await FooAsync(); 5 Helper.PrintThreadId("After"); 6 } 7 8 async Task FooAsync() 9 { 10 Helper.PrintThreadId("Before"); 11 await Task.Delay(1000); 12 int result = await GetValueAsync(); 13 result.Dump("int返回值"); 14 Helper.PrintThreadId("After"); 15 } 16 17 async Task<int> GetValueAsync() 18 { 19 await Task.Delay(1000); 20 return 42; 21 } 22 23 class Helper 24 { 25 private static int index = 1; 26 public static void PrintThreadId(string? message = null, [CallerMemberName] string? name = null) 27 { 28 var title = $"{index}:{name}"; 29 if (!string.IsNullOrEmpty(message)) 30 { 31 title += $"@{message}"; 32 } 33 Environment.CurrentManagedThreadId.Dump(title); 34 Interlocked.Increment(ref index); 35 } 36 } 37 38 // You can define other methods, fields, classes and namespaces here
18.定时器
18.1 ThreadPool的定时器
Debug.WriteLine("main, datetime={0}", DateTime.Now); ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(false), new WaitOrTimerCallback((obj, b) => {//为false第一次不会马上执行,为true第一次会马上执行 //做逻辑判断,判断是否在否以时刻执行。。。 Debug.WriteLine("obj={0},tid={1}, datetime={2}", obj, Thread.CurrentThread.ManagedThreadId, DateTime.Now); }), "hello world", 1000, false);
18.2 Timer
System.Threading(优先使用这个Timer,最底层的,其他的都是对它的封装) System.Timers System.Windows.Forms System.Web.UI
18.3 超级强大的Quartz.NET
19. 锁机制 【为什么要用锁,涉及到多线程同时对共享资源的访问和调用安全,同一时刻某一共享变量只能被一个线程使用,否则会共享资源混乱】
19.1 用户模式锁 【通过一些cpu指令或者一个死循环】 达到thread等待和休眠
19.1.1 易变结构 volatile
volatile修饰符通常用于由多个线程访问但不实用lock语句对访问进行序列化的字段,促进线程安全,让线程按顺序操作
//这段代码release版本在我的电脑上并没异常,可能微软修复这个Bug bool isStop = false; var t = new Thread(() => { var isSuccess = false; while (!isStop) { isSuccess = !isSuccess; } }); t.Start(); Thread.Sleep(1000); isStop = true; t.Join(); Console.WriteLine("主线程执行结束!"); Console.ReadLine();
//volatile关键字:1.不可以底层对代码进行优化;2.我的read和write都是从memrory中读取,读取的都是最新的 private static volatile bool isStop = false; static void Main(string[] args) { //bool isStop = false; var t = new Thread(() => { var isSuccess = false; while (!isStop) { isSuccess = !isSuccess; } }); t.Start(); Thread.Sleep(1000); isStop = true; t.Join(); Console.WriteLine("主线程执行结束!"); Console.ReadLine(); }
19.1.2 互锁结构 Interlocked 【只能做简单的计算】
int sum = 5; Interlocked.Increment(ref sum); //sum=6 Interlocked.Decrement(ref sum); //sum=5 Interlocked.Add(ref sum, 10); //sum=15 Interlocked.Exchange(ref sum, 1); //sum=1 Interlocked.CompareExchange(ref sum, 100, 1); //第一个参数和带三个参数相等,会把第二个参数赋给第一个参数,否则不变 sum=100 Interlocked.CompareExchange(ref sum, 20, 99); //第一个参数和带三个参数不相等,第一个参数不变 sum=100
19.1.3 旋转锁 SpinLock 用户模式和内核模式,能把内容留到用户模式的话就不要将其推送到内核模式
private static SpinLock spin = new SpinLock(); static void Main(string[] args) { for (int i = 0; i < 5; i++) { Task.Factory.StartNew(() => { Run(); }); } Console.Read(); } static int nums = 0; static void Run() { try { for (int i = 0; i < 100; i++) { var b = false; spin.TryEnter(ref b); //获取锁 不使用锁,顺序会乱 Console.WriteLine(nums++); } } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { spin.Exit(); //退出锁 } }
19.2 内核模式锁 【调用win32底层的代码,来实现thread的各种操作】 eg Thread.Sleep()
不要轻易使用内核模式锁,比较重代价太大,能用混合锁就不要用内核模式锁
19.2.1 事件锁【开关锁,使用true、false的变量进行控制】
自动事件锁:AutoResetEvent 手动事件锁:ManualResetEvent Set() Reset() WaitOne()
19.2.2 信号量 Semaphore
Semaphore seLock = new Semaphore(1, 10); //1表示可以同时授予的信号量的初始请求数 10表示可以同时授予的信号量的最大请求数 seLock.WaitOne(); //阻塞当前线程 seLock.Release(); //退出信号量并返回前一个计数
static Semaphore semaphore = new Semaphore(1, 10); static void Main(string[] args) { semaphore.Release(1); //初始化指定一个线程,现在又加了一个线程,所以是两个线程一起跑 for (int i = 0; i < 20; i++) { Task.Run(() => Run()); } Thread.Sleep(3000); semaphore.Release(5); //继续释放线程个数,允许5+2=7个线程同时跑 Console.Read(); } static void Run() { semaphore.WaitOne(); Thread.Sleep(1000); Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId} 当前时间:{DateTime.Now}"); semaphore.Release(); }
19.2.3 互斥锁 mutex
Mutex mutex = new Mutex(); mutex.WaitOne(); //阻塞当前线程 //。。。 mutex.ReleaseMutex(); //释放一次
19.2.4 读写锁 ReaderWriteLock
//多个线程可以一起读, 只能让要给线程去写 //模拟:多个线程读,一个线程写,那么写的线程是否会阻止读取的线程 答:会 //一般读写 8/2 开 不管写入还是读取时间太久,都会导致另一方超时 static ReaderWriterLock rwlock = new ReaderWriterLock(); static void Main(string[] args) { //比如开启5个task读 for (int i = 0; i < 5; i++) { Task.Factory.StartNew(() => { Read(); }); } //一个线程写 Task.Factory.StartNew(() => { Write(); }); Console.Read(); } /// <summary> /// 线程读 /// </summary> static void Read() { while (true) { Thread.Sleep(10); rwlock.AcquireReaderLock(int.MaxValue); //设置读超时 Thread.Sleep(10); Console.WriteLine("当前 t={0} 进行读取 {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); rwlock.ReleaseReaderLock(); //释放锁 } } /// <summary> /// 线程写 /// </summary> static void Write() { while (true) { //3s进行一次写操作 Thread.Sleep(3000); rwlock.AcquireWriterLock(int.MaxValue); //设置写超时 Thread.Sleep(3000); Console.WriteLine("当前 t={0} 进行写入。。。。。。{1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); rwlock.ReleaseWriterLock(); //释放锁 } }
19.2.5 限制线程数 CountdownEvent
//CountdownEvent 限制线程数的一个机制 //搭建一个使用多个线程从一张表中读取数据的场景 //三张表 Orders Products Users //每个表都是用使用多线程去读取 eg Orders(10个线程) Products(5个线程) Users(2个线程) //如果不使用CountdownEvent 使用ContinueWith + TaskCreationOptions.AttachedToParent也是可以的 static CountdownEvent cdeLock = new CountdownEvent(10); //默认10个线程 static void Main(string[] args) { //加载Orders搞定 cdeLock.Reset(10); //重置当前的threadcount上限 for (int i = 0; i < cdeLock.InitialCount; i++) { Task.Factory.StartNew(() => { LoadOrders(); }); } cdeLock.Wait(); //相当于我们的Task.WaitAll Console.WriteLine("所有的Orders都加载完毕。。。。。。。。。。。。。。。。。。。。。"); //加载Product搞定 cdeLock.Reset(5); for (int i = 0; i < cdeLock.InitialCount; i++) { Task.Factory.StartNew(() => { LoadProducts(); }); } cdeLock.Wait(); Console.WriteLine("所有的Products都加载完毕。。。。。。。。。。。。。。。。。。。。。"); //加载Users搞定 cdeLock.Reset(2); for (int i = 0; i < cdeLock.InitialCount; i++) { Task.Factory.StartNew(() => { LoadUsers(); }); } cdeLock.Wait(); Console.WriteLine("所有的Users都加载完毕。。。。。。。。。。。。。。。。。。。。。"); Console.WriteLine("所有的表数据都执行结束了。。。恭喜恭喜。。。。"); Console.Read(); } /// <summary> /// 10 threads /// </summary> static void LoadOrders() { //...业务逻辑 Console.WriteLine("当前Orders正在加载中。。。{0}", Thread.CurrentThread.ManagedThreadId); cdeLock.Signal(); //将当前的threadcount--操作 } /// <summary> /// 5 threads /// </summary> static void LoadProducts() { //...业务逻辑 Console.WriteLine("当前Products正在加载中。。。{0}", Thread.CurrentThread.ManagedThreadId); cdeLock.Signal(); } /// <summary> /// 2 threads /// </summary> static void LoadUsers() { //...业务逻辑 Console.WriteLine("当前Users正在加载中。。。{0}", Thread.CurrentThread.ManagedThreadId); cdeLock.Signal(); }
19.3 混合锁 Slim后缀 用户模式锁+内核模式锁
19.3.1 Thread.Sleep(1) 让线程休眠1ms; Thread.Sleep(0) 让线程放弃当前的时间片,让本线程更高或者同等线程得到时间片运行; Thread.Yield() 让线程立即放弃当前的时间片,可以让更低级别的线程得到运行,当其他thread时间片用完,本thread再度唤醒 Yield < Sleep(0) < Sleep(1) 一个时间片=30ms
19.3.2 内核模式锁和混合锁的比较
混合锁:先在用户模式下内旋,如果超过一定的阈值,会切换到内核锁;在内旋的情况下,我们会看到大量的Sleep(0),Sleep(1),Yield等语法
内核模式锁 | 混合锁 | ||
Semaphore | 继承自WaitHandle | SemaphoreSlim | 继承自IDisposable |
ManualResetEvent | 继承自EventWaitHandle-->>WaitHandle | ManualResetEventSlim | 继承自IDisposable |
ReaderWriterLock | 继承自CriticalFinalizerObject | ReaderWriterLockSlim | 继承自IDisposable |
性能低:调用了win32 API | 性能高,并且Wait()方法有多个重载,支持任务取消 |
19.4 监视锁 lock和Monitor
19.4.1 Monitor 限定线程个数的一把锁 Enter锁住某一个资源;Exit退出某一个资源
static object lockMe = new object(); //锁的引用类型 static int nums = 0; static void Main(string[] args) { for (int i = 0; i < 5; i++) { Task.Factory.StartNew(() => { Run(); }); } Console.Read(); } static void Run() { for (int i = 0; i < 100; i++) { var b = false; try //为了严谨性,当时用Monitor时需要加Try...catch... { Monitor.Enter(lockMe, ref b); //b用于指示到底有没有真正获取到锁,如果获取失败,也就不用Exit Console.WriteLine(nums++); //多个线程都对num++,如果不加锁,将是乱序 } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { if (b) Monitor.Exit(lockMe); } } }
19.4.2 lock语法糖 凡是简化我们编程的方式,基本上都叫语法糖;语法糖是编译器层面的,底层的IL,还是使用Monitor
因为众多的锁机制中,唯独只有Monitor有专用的语法糖,所以说非常受重视,本质就是利用堆上的同步块实现资源锁定
static void Run() { for (int i = 0; i < 100; i++) { lock (lockMe) { Console.WriteLine(nums++); //多个线程都对num++,如果不加锁,将是乱序 } }
.method private hidebysig static void Run () cil managed { // Method begins at RVA 0x20a0 // Code size 72 (0x48) .maxstack 3 .locals init ( [0] int32 i, [1] object, [2] bool, [3] bool ) IL_0000: nop IL_0001: ldc.i4.0 IL_0002: stloc.0 IL_0003: br.s IL_003e // loop start (head: IL_003e) IL_0005: nop IL_0006: ldsfld object _021_lock和Monitor.Program::lockMe IL_000b: stloc.1 IL_000c: ldc.i4.0 IL_000d: stloc.2 .try { IL_000e: ldloc.1 IL_000f: ldloca.s 2 IL_0011: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) IL_0016: nop IL_0017: nop IL_0018: ldsfld int32 _021_lock和Monitor.Program::nums IL_001d: dup IL_001e: ldc.i4.1 IL_001f: add IL_0020: stsfld int32 _021_lock和Monitor.Program::nums IL_0025: call void [mscorlib]System.Console::WriteLine(int32) IL_002a: nop IL_002b: nop IL_002c: leave.s IL_0039 } // end .try finally { IL_002e: ldloc.2 IL_002f: brfalse.s IL_0038 IL_0031: ldloc.1 IL_0032: call void [mscorlib]System.Threading.Monitor::Exit(object) IL_0037: nop IL_0038: endfinally } // end handler IL_0039: nop IL_003a: ldloc.0 IL_003b: ldc.i4.1 IL_003c: add IL_003d: stloc.0 IL_003e: ldloc.0 IL_003f: ldc.i4.s 100 IL_0041: clt IL_0043: stloc.3 IL_0044: ldloc.3 IL_0045: brtrue.s IL_0005 // end loop IL_0047: ret } // end of method Program::Run
19.4.3 小结
Enter中添加的对象,相当于把对象的同步块索引和CLR的同步块数组进行关联
Exit中释放的资源,相当于把对象的同步快索引和CLR的同步块数组进行了解绑
要注意:锁住的引用类型一定是可访问的线程必须能够访问到的;锁住的资源千万不要使用值类型,因为值类型是拷贝的;锁住的资源的作用域必须足够大
20.Parallel并行处理
并行时,可指定当前有几个线程参与计算;不让所有的thread参与计算,不让cpu跑的太凶猛了
最后会使用ParallelForReplicatingTask 进行处理;
不要在Parallel.For中使用break或者stop,或许会给你引入一些不必要的bug;因为大家都是并行执行的,所以别的线程是刹不住车的
Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); for (int i = 0; i < 100; i++) { Console.WriteLine("串行:" + i); } stopwatch.Stop(); Console.WriteLine("串行耗时:" + stopwatch.ElapsedMilliseconds); //20ms stopwatch.Restart(); ConcurrentStack<int> stack = new ConcurrentStack<int>(); //线程安全的栈 Parallel.For(0, 100, (item) => { Console.WriteLine("并:" + item); stack.Push(item); }); stopwatch.Stop(); Console.WriteLine("并行耗时:" + stopwatch.ElapsedMilliseconds + "\r\n" + string.Join(",", stack)); //耗时180ms
//使用parallel.For计算1到99的和 var totalNums = 0; //类似聚合函数 Parallel.For<int>(1, 100, () => { return 0; }, (current, loop, total) => { total += (int)current; return total; }, (total) => { Interlocked.Add(ref totalNums, total); }); Console.WriteLine(totalNums);
Dictionary<int, int> dic = new Dictionary<int, int>() { {1,100}, {2,200 }, {3,300 } }; Parallel.ForEach(dic, (item) => { Console.WriteLine(item.Key); //1 2 3 }); Parallel.Invoke(() => { Console.WriteLine("我是并行计算1 " + Thread.CurrentThread.ManagedThreadId); //Invoke中的是不同的线程 }, () => { Console.WriteLine("我是并行计算2 " + Thread.CurrentThread.ManagedThreadId); });
Parallel的For处理的是数组,Foreach处理的是集合运算(非数组)。Parallel核心就是分区,每个线程处理一个区域 。
// // 摘要: // 执行具有 64 位索引和线程本地数据的 for(在 Visual Basic 中为 For)循环,其中可能会并行运行迭代,而且可以监视和操作循环的状态。 // // 参数: // fromInclusive: // 开始索引(含)。 // // toExclusive: // 结束索引(不含)。 // // localInit: // 用于返回每个任务的本地数据的初始状态的函数委托。 // // body: // 将为每个迭代调用一次的委托。 // // localFinally: // 用于对每个任务的本地状态执行一个最终操作的委托。 // // 类型参数: // TLocal: // 线程本地数据的类型。 // // 返回结果: // 包含有关已完成的循环部分的信息的结构。 // // 异常: // T:System.ArgumentNullException: // body 参数为 null。 或 - localInit 参数为 null。 或 - localFinally 参数为 null。 // // T:System.AggregateException: // 包含在所有线程上引发的全部单个异常的异常。 public static ParallelLoopResult For<TLocal>(long fromInclusive, long toExclusive, Func<TLocal> localInit, Func<long, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);
21.PLinq
底层基于Task的一些编程模型,让我们快速进行并行计算;最灵活的东西莫过于自己去写业务逻辑。。封装的越厉害,灵活性越差,性能自然也越差
WithDegreeOfParallelism(Environment.ProcessorCount-1)
//普通的linq var nums = Enumerable.Range(0, 100); var query = from n in nums select new { thread = Thread.CurrentThread.ManagedThreadId, num = n }; foreach (var item in query) { Console.WriteLine(item); }
AsParallel() | 可将串行的代码转换成并行 |
AsOrdered() | 将并行结果还是按照 未排序的样子进行排序 |
WithDegreeOfParallelism | (Environment.ProcessorCount-1) |
WithCancellation | 如果执行之前被取消,那就不要执行了 |
WithExecutionMode |
此参数可以告诉系统当前是否强制并行 |
21.1 AsParallel()
//并行查询AsParallel() var nums = Enumerable.Range(0, 100); var query = from n in nums.AsParallel() select new { thread = Thread.CurrentThread.ManagedThreadId, num = n }; foreach (var item in query) { Console.WriteLine(item); }
21.2 AsOrdered()
[10,1,2,3,4] => 并行计算.AsOrdered => [10,1,2,3,4]
[10,1,2,3,4] => orderby => [1,2,3,4,10]
22.TaskSchedule Task调度器
Task任务的执行都需要经过Schedule,Task的核心是Schedule调度器,Schedule需要把任务安排到线程或者线程池中
Task调度器有两种,默认是ThreadPoolTaskSchedule线程池,还有一个是SynchronizationContextTaskSchedule同步上下文调度器
22.1 ThreadPoolTaskSchedule 默认Task的调度形式
//ThreadPoolTaskSchedule核心源码 protected internal override void QueueTask(Task task) { //如果创建方式LongRunning,就以Thread的方式执行,否则以ThreadPool的方式执行 if ((task.Options & TaskCreationOptions.LongRunning) != 0) { Thread thread = new Thread(s_longRunningThreadWork); thread.IsBackground = true; thread.Start(task); } else { bool forceGlobal = (task.Options & TaskCreationOptions.PreferFairness) != 0; ThreadPool.UnsafeQueueCustomWorkItem(task, forceGlobal); } }
22.2 SynchronizationContextTaskScheduler 同步上下文Task调度器
在winform、wpf中,如果给一个控件赋值,都是在调用invoke方法
下属例子讲解如何在winform中使用同步上下文任务调度器在工作线程中更新控件
22.3 自定义TaskSchedule
//自定义TaskSchedule让其默认用Thread方式执行 public class PerThreadTaskSchedule : TaskScheduler { protected override IEnumerable<Task> GetScheduledTasks() { return Enumerable.Empty<Task>(); } protected override void QueueTask(Task task) { var thread = new Thread(() => { TryExecuteTask(task); }); thread.Start(); } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return true; } }
23.多线程模型:同步编程模型SPM 异步编程模型APM 基于事情的编程模型EAP 基于Task的编程模型TAP
微软大力推广Task,APM和EAP都能包装成Task使用,微软想用Task来统治异步编程领域,解决大一统的问题
23.1 异步编程模型APM xxxbegin xxxend 这两个配对的经典方法,委托给线程池执行
例如FileStream (ReadBegin和ReadEnd) 配对方法 以及 Action委托都可以异步执行
{ //使用Task包装AMP using (FileStream fs = new FileStream(Environment.CurrentDirectory + "//1.txt", FileMode.Open)) { var bytes = new byte[fs.Length]; var task = Task.Factory.FromAsync(fs.BeginRead, fs.EndRead, bytes, 0, bytes.Length, string.Empty); var nums = task.Result; Console.WriteLine(nums); } } { //不使用Task包装AMP using (FileStream fs = new FileStream(Environment.CurrentDirectory + "//1.txt", FileMode.Open)) { var bytes = new byte[fs.Length]; fs.BeginRead(bytes, 0, bytes.Length, (aysc) => { var nums = fs.EndRead(aysc); Console.WriteLine(nums); }, string.Empty); } } //使用Task包装的两个好处:代码量比较小、使用task更方便,更强大
23.2 基于事件的编程模型(EAP) xxxAsync事件模型,eg WebClient
//使用task包装EAP public static Task<byte[]> GetTaskAsync(string url) { //TaskCompletionSource Task包装器 TaskCompletionSource<byte[]> source = new TaskCompletionSource<byte[]>(); WebClient client = new WebClient(); //WebClient.DownloadDataCompleted事件 client.DownloadDataCompleted += (sender, e) => { try { //如果下载完成了,将当前的byte[]给task包装器 source.TrySetResult(e.Result); } catch (Exception ex) { source.TrySetException(ex); } }; client.DownloadDataAsync(new Uri(url)); return source.Task; } //调用 var task = GetTaskAsync("http://www.baidu.com"); byte[] data = task.Result;
24. 四大并发集合类 Concurrent :同时发生的
24.1 ConcurrentBag<T> 利用线程槽来分摊Bag中的所有数据;所有数据都是防止在多个插入线程的槽位中。。每个线程一个子集
ThreadLocal 是什么意思??? 每个线程有一个自己的备份(线程不可见) 1. 每一个线程分配一个“链表” 这个链表可以任务是list(ThreadLocalList) 当你Add操作的时候,locals里面有一份新增的数据,【只有本线程看得见】 同时head和next也是有数据的。。。。为什么有??因为我们的算法有一个“偷盗” 的行为。。。 TryTake: 获取数据 如果有三个线程做Add操作,那么三个线程的数据槽中都有一份子集数据。。。 t1: 1,2,3 locals t2: 1,3,2 locals t3: 2,3,4 locals 这个时候,如果你在t3线程中执行了三个TryTake。。 t1: 1,2,3 locals t2: 1,3,2 locals t3: empty locals 如果这个时候我在t3线程上进行tryTake,怎么办??? 这个时候就到Bag的下一级的ttl head 和 next中去找。。。。【steal 偷盗的时候使用的】 for (threadLocalList = this.m_headList; threadLocalList != null; threadLocalList = threadLocalList.m_nextList) { list.Add(threadLocalList.m_version); if (threadLocalList.m_head != null && this.TrySteal(threadLocalList, out result, take)) { return true; } } 总结:ConcurrentBag 就是利用线程槽来分摊Bag中的所有数据。 ConcurrentBag的所有数据都是防止在多个插入线程的槽位中。。每个线程一个子集。。。 链表的头插法。 static void Main(string[] args) { ConcurrentBag<int> bag = new ConcurrentBag<int>(); bag.Add(1); bag.Add(2); var result = 0; bag.TryTake(out result); }
24.2 ConcurrentStack<T> ConcurrentQueue<T> ConcurrentDictionary<TKey, TValue>
同步版本 | 线程安全版本 |
Stack 数组 | ConcurrentStack 链表,使用Interlocked来实现,而没有使用内核锁 |
Queue 数组 | ConcurrentQueue 链表 |
Dictionary | ConcurrentDictionary TryAdd TryGetValue TryRemove |
如果不想用Concurrent线程安全版本,可以用 同步+lock/其他锁机制
25. 三大救火Bug:CPU过高、死锁、内存爆满
25.1 CPU过高 死循环,肯定是否一个线程得不到休眠长时间运行容易导致CPU过高
static void Main(string[] args) { Run(); Console.Read(); } static void Run() { var task = Task.Factory.StartNew(() => { var i = true; //这个地方是一个非常复杂的逻辑。导致死循环 while (true) { i = !i; } }); }
步骤:1.生成release x64 2.在“任务管理器”中生成一个dump文件 3.需要用x64 的windbg
25.2 死锁
private static object lockMe = new object(); static void Main(string[] args) { Run(); Console.Read(); } static void Run() { lock (lockMe) //第一次锁 { var task = Task.Run(() => { Console.WriteLine("----- start ---- "); Thread.Sleep(1000); Run2(); Console.WriteLine("------ end -----"); }); task.Wait(); } } static void Run2() { lock (lockMe) //死锁 第二次锁 { Console.WriteLine("我是 run2.。。。。"); } }
1. ~*e!clrstack 查看所有线程的堆栈;2. !threads 查看当前的托管线程;3. !syncblk 当前哪一个线程持有锁
25.3 内存爆满
static StringBuilder sb = new StringBuilder(); //用于存放内存的对象 static void Main(string[] args) { for (int i = 0; i < 10000000; i++) { sb.Append("hello world"); } Console.WriteLine("执行完毕"); Console.Read(); }
!dumpheap -stat 查看clr的托管堆中的各个类型的占用情况
!DumpHeap /d -mt 00007ffb445467d0 //查看当前的方法表 这个方法表很长
!DumpObj /d 00000152aed26be8 //查看当前char[]的内容
!gcroot 00000152aed81208 //查看当前地址的根root