1.进程、线程、多线程、计算机概念
【进程】:一个运行程序 【线程】:依托于进程,一个进程可以包含多个线程
【多线程】:多个执行流同时运行,主要分为两种:其一CPU运算太快啦,分时间片——上下文切换(加载环境—计算—保存环境) 微观角度讲,一个核同一时刻只能执行一个线程;宏观的讲是多线程并发。其二,多CPU多核,可以独立工作,4核8线程——核指物理的核,线程指虚拟核
多线程三大特点:不卡主线程、速度快、无序性 【Thread】:C#语言对线程对象的封装
2.同步与异步 【同步】:完成计算之后,再进入下一行,会阻塞 【异步】:不会等待方法的完成,直接进入下一行,不会阻塞
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> */
11.小结
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)的正确用法
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
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.3 Task中用CancellationTokenSource取消任务
14.3.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.3.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.3.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.3.4 ThrowIfCancellationRequested
ThrowIfCancellationRequested 比 IsCancellationRequested 多了throw。。。 如果一个任务被取消,我希望代码抛出一个异常。。。 if(IsCancellationRequested) throw new Exception("adasdaf"); == 等价操作 == throwIfCancellationRequested();
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.await/async
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; }
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