以下是学习笔记:
回顾:
Thread线程和ThreadPool线程池
Thread:我们可以开启一个线程。但是请大家记住:线程开启会在空间和时间上有不小的开销。所以,不能随便开。
ThreadPool:会根据你的CPU的核心数开启一个最合适的线程数量。如果你操作中,非常耗时,就不要用线程池,如果耗时十几分钟,那就不合适线程池了。
Task=>Thread + ThreadPool结合 ,使用多线程,尽量使用Task
1,Task和各种任务阻塞、延续及其线程锁Lock
#region Task使用【1】多线程任务的开启3种方式 //【1】通过new的方式创建一个Task对象,并启动 static void Method1_1() { Task task1 = new Task(() => { //在这个地方编写我们需要的逻辑... Console.WriteLine($"new一个新的Task启动的子线程Id={Thread.CurrentThread.ManagedThreadId}"); }); task1.Start(); } //【2】使用Task的Run()方法 static void Method1_2() { Task task2 = Task.Run(() => { //在这个地方编写我们需要的逻辑... Console.WriteLine($"使用Task的Run()方法开启的子线程Id={Thread.CurrentThread.ManagedThreadId}"); }); } //1和2对比 //1,灵活开启线程,想什么时候开启就什么时候开启 //2, 马上开启线程 //【3】使用TaskFactory启动(类似于ThreadPool) static void Method1_3() { Task task3 = Task.Factory.StartNew(() => { //在这个地方编写我们需要的逻辑... Console.WriteLine($"使用TaskFactory开启的子线程Id={Thread.CurrentThread.ManagedThreadId}"); }); } #endregion #region Task使用【2】Task的阻塞方式和任务延续 //【1】回顾之前使用Thread多个子线程执行时阻塞的方法 static void Method2() { Thread thread1 = new Thread(() => { Thread.Sleep(2000); Console.WriteLine("Child Thread (1)......"); }); Thread thread2 = new Thread(() => { Thread.Sleep(1000); Console.WriteLine("Child Thread (2)......"); }); thread1.Start(); thread2.Start(); //... thread1.Join();//让调用线程阻塞 thread2.Join(); //如果有很多的thread,是不是也得有很多的Join?还有,我们只希望其中一个执行完以后,后面的其他线程就能执行,这个也做不了! Console.WriteLine("This is Main Thread!"); } //【2】Task各种【阻塞】方式(3个) static void Method3() { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task2.Start(); ////第1种方式:挨个等待和前面一样 //task1.Wait(); //task2.Wait(); ////第2种方式:等待所有的任务完成 【推荐】 Task.WaitAll(task1, task2); //第3种方式:等待任何一个完成即可 【推荐】 //Task.WaitAny(task1, task2); Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString()); /* 第2中方式结果: Task1子线程Id=4 21:46:58 Task2子线程Id=3 21:46:59 主线程开始运行!Time=21:46:59 第3种方式结果 Task1子线程Id = 3 21:41:34 主线程开始运行!Time = 21:41:34 Task2子线程Id = 4 21:41:35 */ } //Task任务的延续:WhenAll 希望前面所有任务执行完毕后,再继续执行后面的线程,和前面相比,既有阻塞,又有延续。 static void Method4() { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task2.Start(); //线程的延续(主线程不等待,子线程依次执行,如果你需要主线程也按照子线程的顺序来,请你自己把主线程的任务放到延续任务中就可以) //线运行主线程,然后task1和task2都执行完,再执行task3 Task.WhenAll(task1, task2).ContinueWith(task3 => { //在这里可以编写你需要的业务... Console.WriteLine($"Task3子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString()); /* 主线程开始运行!Time = 21:44:46 Task1子线程Id = 3 21:44:47 Task2子线程Id = 4 21:44:48 Task3子线程Id = 3 21:44:48 */ } //Task的延续:WhenAny static void Method5() { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); task2.Start(); //线程的延续(主线程不等待,子线程任何一个执行完毕,就会执行后面的线程) Task.WhenAny(task1, task2).ContinueWith(task3 => { //在这里可以编写你需要的业务... Console.WriteLine($"Task3子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }); Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString()); /* 主线程开始运行!Time=21:48:51 Task1子线程Id=3 21:48:52 Task3子线程Id=6 21:48:52 Task2子线程Id=4 21:48:53 */ } #endregion #region Task使用【3】Task常见枚举 TaskCreationOptions(父子任务运行、长时间运行的任务处理) //请大家通过Task的构造方法,观察TaskCreationOptions这个枚举的类型,自己通过F12查看 static void Method6() { Task parentTask = new Task(() => { Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }, TaskCreationOptions.AttachedToParent); Task task2 = new Task(() => { Thread.Sleep(3000); Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }, TaskCreationOptions.AttachedToParent); task1.Start(); task2.Start(); }); parentTask.Start(); parentTask.Wait();//等待附加的子任务全部完成。相当于Task.WaitAll(taks1,task2); //TaskCreationOptions.AttachedToParent如果这个枚举参数不添加,主线程会直接运行,不等待 Console.WriteLine("主线程开始执行!Time= " + DateTime.Now.ToLongTimeString()); /* Task1子线程Id=4 21:52:17 Task2子线程Id=5 21:52:19 主线程开始执行!Time= 21:52:19 */ } //长时间的任务运行,需要采取的方法 static void Method7() { Task task1 = new Task(() => { Thread.Sleep(2000); Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); }, TaskCreationOptions.LongRunning); //LongRunning:如果你明确知道这个任务是长时间运行的,建议你加上。 //当然你使用Thread也是可以的。但是不要使用ThreadPool,因为长时间占用不归还线程,系统会强制开启新的线程,会一定程度影响性能 task1.Start(); task1.Wait(); Console.WriteLine("主线程开始执行!Time= " + DateTime.Now.ToLongTimeString()); /* Task1子线程Id=3 21:57:42 主线程开始执行!Time= 21:57:42 */ } #endregion #region Task使用【4】Task中的取消功能:使用的是CacellationTokenSoure解决多任务中协作取消和超时取消方法 //【1】Task任务的取消和判断 static void Method8() { //创建取消信号源对象 CancellationTokenSource cts = new CancellationTokenSource(); Task task = Task.Factory.StartNew(() => { int i = 0; while (!cts.IsCancellationRequested) //判断任务是否被取消 { Thread.Sleep(200); i++; Console.WriteLine( $"执行次数:{i},子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); } }, cts.Token); //我们在这个地方模拟一个事件产生,如果发生某个错误,就取消线程 Thread.Sleep(2000); cts.Cancel(); //取消任务,只要传递这样一个信号就可以 /* 执行次数:1,子线程Id=3 22:06:18 执行次数:2,子线程Id=3 22:06:18 执行次数:3,子线程Id=3 22:06:18 执行次数:4,子线程Id=3 22:06:18 执行次数:5,子线程Id=3 22:06:19 执行次数:6,子线程Id=3 22:06:19 执行次数:7,子线程Id=3 22:06:19 执行次数:8,子线程Id=3 22:06:19 执行次数:9,子线程Id=3 22:06:19 执行次数:10,子线程Id=3 22:06:20 */ } //【2】Task任务取消:同时我们也希望做一些清理的工作,也就是取消这个动作会触发一个任务。 static void Method9() { CancellationTokenSource cts = new CancellationTokenSource(); Task task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested) { Thread.Sleep(500); Console.WriteLine($"子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); } }, cts.Token); //注册一个委托:这个委托将在任务取消的时候调用 cts.Token.Register(() => { //在这个地方可以编写自己要处理的逻辑... Console.WriteLine($"任务取消,开始清理工作......{DateTime.Now.ToLongTimeString()}"); Thread.Sleep(2000); Console.WriteLine($"任务取消,清理工作结束......{DateTime.Now.ToLongTimeString()}"); }); //这个地方肯定是有其他的逻辑来控制取消 Thread.Sleep(3000);//模拟其他的耗时工作 cts.Cancel();//取消任务 /* 子线程Id=3 22:12:52 子线程Id=3 22:12:53 子线程Id=3 22:12:53 子线程Id=3 22:12:54 子线程Id=3 22:12:54 任务取消,开始清理工作......22:12:55 子线程Id=3 22:12:55 任务取消,清理工作结束......22:12:57 */ } //【3】Task任务延时自动取消:比如我们请求一个远程接口,如果长时间没有返回数据,我们可以做一个时间限制,超时可以取消任务(比如微信红包退回) static void Method10() { CancellationTokenSource cts = new CancellationTokenSource(); // CancellationTokenSource cts = new CancellationTokenSource(3000); Task task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested) { Thread.Sleep(300); Console.WriteLine($"子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}"); } }, cts.Token); //注册一个委托:这个委托将在任务取消的时候调用 cts.Token.Register(() => { //在这个地方可以编写自己要处理的逻辑... Console.WriteLine($"任务取消,开始清理工作......{DateTime.Now.ToLongTimeString()}"); Thread.Sleep(2000); Console.WriteLine($"任务取消,清理工作结束......{DateTime.Now.ToLongTimeString()}"); }); cts.CancelAfter(3000); //3秒后自动取消 /* 子线程Id=3 22:16:49 子线程Id=3 22:16:50 子线程Id=3 22:16:50 子线程Id=3 22:16:50 子线程Id=3 22:16:50 子线程Id=3 22:16:51 子线程Id=3 22:16:51 子线程Id=3 22:16:51 子线程Id=3 22:16:52 任务取消,开始清理工作......22:16:52 子线程Id=3 22:16:52 任务取消,清理工作结束......22:16:54 */ } #endregion #region Task使用【5】Task中专门的异常处理:AggregateException //AggregateException:是一个异常集合,因为Task中可能抛出异常,所以我们需要新的类型来收集异常对象 static void Method11() { var task = Task.Factory.StartNew(() => { var childTask1 = Task.Factory.StartNew(() => { //实际开发中这个地方写你处理的业务,可能会发生异常.... //自己模拟一个异常 throw new Exception("my god!Exception from childTask1 happend!"); }, TaskCreationOptions.AttachedToParent); var childTask2 = Task.Factory.StartNew(() => { throw new Exception("my god!Exception from childTask2 happend!"); }, TaskCreationOptions.AttachedToParent); }); try { try { task.Wait(); //1.异常抛出的时机(等待task执行完毕,这里是等到异常抛出) } catch (AggregateException ex) //2.异常所在位置 { foreach (var item in ex.InnerExceptions) { Console.WriteLine(item.InnerException.Message + " " + item.GetType().Name); } //3.异常集合,如果你想往上抛,需要使用Handle方法处理一下 ex.Handle(p => { if (p.InnerException.Message == "my god!Exception from childTask1 happend!") return true;//就结束了,不往上抛了 else return false; //返回false表示往上继续抛出异常 }); } } catch (Exception ex) { Console.WriteLine("-----------------------------------------------------"); Console.WriteLine(ex.InnerException.InnerException.Message); } /* my god!Exception from childTask2 happend! AggregateException my god!Exception from childTask1 happend! AggregateException ----------------------------------------------------- my god!Exception from childTask2 happend! */ } #endregion #region 监视锁:Lock 限制线程个数的一把锁 //为什么要用锁?在多线程中,尤其是静态资源的访问,必然会有竞争 private static int nums = 0; private static object myLock = new object(); static void Method12() { for (int i = 0; i < 5; i++) { //开启5线程调用一个nums Task.Factory.StartNew(() => { //TestMethod1();//不加锁的结果顺序是乱的,1,3,2,4,6,9,,,,500 TestMethod2();//加锁的结果顺序是对的,因为把资源给锁住了,1,2,3,4,5,6,,,,500 }); } } static void TestMethod1() { for (int i = 0; i < 100; i++) { nums++; Console.WriteLine(nums); } } static void TestMethod2() { for (int i = 0; i < 100; i++) { lock (myLock) { nums++; Console.WriteLine(nums); } } } //Lock是Monitor语法糖,本质是解决资源的锁定问题 //我们锁住的资源一定是让线程可访问到的,所以不能是局部变量。 //锁住的资源千万不要是值类型。 //lock也不能锁住string类型。 } #endregion
2,Task中的跨线程访问控件和UI耗时任务卡顿的解决方法
//普通方法 private void btnUpdate_Click(object sender, EventArgs e) { Task task = new Task(() => { this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!"; }); //task.Start(); //这样使用会报错 //使用下面的方式解决报错的问题 task.Start(TaskScheduler.FromCurrentSynchronizationContext());//使用任务调度器 } //针对UI耗时的情况,单独重载其实并不是很好 private void btnUpdate_Click1(object sender, EventArgs e) { Task task = new Task(() => { //模拟耗时(这个地方会卡主) Thread.Sleep(5000);//界面会卡5秒钟,多线程不是万能,多线程并不是解决卡界面的。 this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!"; }); //task.Start(); //这样使用会报错 //使用下面的方式解决报错的问题 task.Start(TaskScheduler.FromCurrentSynchronizationContext()); } //以后耗时任务都可以用这个方法 //针对耗时任务,我们可以使用新的方法 private void btnUpdate_Click2(object sender, EventArgs e) { this.btnUpdate.Enabled = false; this.lblInfo.Text = "数据更新中,请等待......"; Task task =Task.Factory.StartNew(() => { Thread.Sleep(5000); //有耗时的任务,我们可以放到ThreadPool中 }); //在ContinueWith中更新我们的数据 task.ContinueWith(t => { this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!"; this.btnUpdate.Enabled = true; },TaskScheduler.FromCurrentSynchronizationContext()); //更新操作到同步的上下文中 }