.NETCore 多线程学习笔记(多线程、线程结束顺序掌控、线程相对平均分配) | 您所在的位置:网站首页 › 多线程的执行顺序 › .NETCore 多线程学习笔记(多线程、线程结束顺序掌控、线程相对平均分配) |
参考资料:多线程MutiThread最佳实践专题-1-1_哔哩哔哩_bilibili 跟着视频学习的,下面是自己的注释笔记和实验结果 写了个窗体来学习线程的 多线程、线程掌控、线程分配 下面会用到这个方法 /// /// 仅仅只是一个耗时方法 /// /// private void DoSomethingLong(string name) { Debug.WriteLine($"*****DoSomethingLong start {name} {Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToString("hh:mm:ss:fff")}*****"); long lResult = 0; for (int i = 0; i < 1_000_000_000; i++) { lResult += i; } Debug.WriteLine($"*****DoSomethingLong end {name} {Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToString("hh:mm:ss:fff")} {lResult}*****"); } 单线程只是个简单的例子,用于与下面多线程来做对比和参考 /// /// 单线程 /// /// /// private void button1_Click(object sender, EventArgs e) { Debug.WriteLine(" "); Debug.WriteLine($"*****button1_Click 同步方法 start {Thread.CurrentThread.ManagedThreadId}*****"); //当前线程的id,任意操作的相应都需要线程,方便开发的时候对线程的分析 for (int i = 0; i < 5; i++) { string name = $"button1_Click_{i}"; //DoSomethingLong(name); Action action = () => DoSomethingLong(name); //这是一个委托 action.Invoke();//这仍然是单线程的,用于举例学习 } Debug.WriteLine($"*****button1_Click 同步方法 end {Thread.CurrentThread.ManagedThreadId}*****"); Debug.WriteLine(" "); } 输出: 可以看到是按照顺序输出了五遍,并且相隔一两秒。 多线程/// /// 多线程 /// 一、.NET Framework1.0 2.0 3.0 3.5 4.0 4.5 4.8 -->.NET Core 1.0 2.0 2.2 3.0 3.1 -->.NET5-->.NET6 /// 经过了上面这些.net的一些大版本发展史,有很多的异步多线程的写法 /// 例:Thread/ThreadPool/BeginInvoke(netcore中已移除)/Task/Parallel/AsyncAwait /// 公认最佳实践:Task /// 二、十个线程是单线程的10倍速度吗?why? /// 不到十倍,两种情况:1、CPU不行 2、调度机制、线程切换,如10个线程,cpu分片,线程调度,上下文切换都是需要成本的 /// 注意:线程不是越多越好。太多可能挂掉,但是Task不会出现,因为他是基于线程池的,但是太多了也会降低性能。 /// 那多少合适呢?控制在:开发的时候控制CPU核数*3 个线程以内(网络的经验之谈。扩展:线程池默认是2048个线程,如果你要Task开2048个是开不了的,但是Thread是可以的,那自然就挂了) /// 多线程虽快,但请勿贪杯 /// 三、多线程是无序的,因为线程是计算机资源(不像c#可控)。启动时无序的,因为CPU调度策略,结束也是无序的。 /// 如果需求要求必须是有序的。 使用线程sleep就可以了吗?答案:不一定,因为程序执行的时间超出睡眠时间就出现问题,所以不可取。 /// 有多线程有序的解决方案吗?有!在button3_Click /// /// /// private void button2_Click(object sender, EventArgs e) { Debug.WriteLine(" "); Debug.WriteLine($"*****button1_Click 同步方法 start {Thread.CurrentThread.ManagedThreadId}*****"); //当前线程的id,任意操作的相应都需要线程,方便开发的时候对线程的分析 for (int i = 0; i < 10; i++) { string name = $"button1_Click_{i}";//此时会阻塞,产生新的name Action action = () => DoSomethingLong(name); //这是一个委托,如有返回值使用:Func #region 常见错误 //注意!下面这句代码执行会出现i重复或乱掉的问题,这是一个执行时机和上下文的问题: //前者正确但是这句代码不正确,是因为循环时i没有阻塞(或者说循环内i是公共的),动作是交给Task线程去执行的(Task.Run并非立即执行,他只是交给线程的一个通知)。 //Task线程是什么时候启动的?这个是不确定的,因为电脑系统等着执行的时候,此时i已经不是之前的i了,因为i可能已经循环到8或者10了,所以会有i重复或乱掉的出现 //前者可以,是因为在循环时把i绑定到name变量上(每次循环都是新的name,此时阻塞了一下) //这个被称为:多线程的临时变量问题 //Action action1 = () => DoSomethingLong($"button1_Click_{i}"); #endregion Task.Run(action);//开启多线程!(循环5次就是5个线程并发执行) } Debug.WriteLine($"*****button1_Click 同步方法 end {Thread.CurrentThread.ManagedThreadId}*****"); Debug.WriteLine(" "); } 输出: 可以看到五个线程是同时执行的,到最后结束的时间都基本相同 线程结束顺序的掌控/// /// 线程结束顺序的掌控(线程的 阻塞&非阻塞) /// /// /// private void button3_Click(object sender, EventArgs e) { Debug.WriteLine($"*****button3_Click start {Thread.CurrentThread.ManagedThreadId}*****"); List taskList = new List(); taskList.Add(Task.Run(() => { DoSomethingLong("开发1"); })); taskList.Add(Task.Run(() => { DoSomethingLong("开发2"); })); taskList.Add(Task.Run(() => { DoSomethingLong("开发3"); })); taskList.Add(Task.Run(() => { DoSomethingLong("开发4"); })); taskList.Add(Task.Run(() => { DoSomethingLong("开发5"); })); #region 普通写法,当然这样是阻塞主线程的(winform窗口是不能拖动的) //Task.WaitAny(taskList.ToArray()); //阻塞到任意一个任务完成(应用场景如:多渠道获取数据,redis、es、本地数据库 等等) //Debug.WriteLine("任意一个人完成了任务!!!!"); //Task.WaitAll(taskList.ToArray());//阻塞到全部task完成。但是winform界面会卡主,因为主线程在等待。 //Debug.WriteLine("全部人完成任务了!!!!"); #endregion #region 解决阻塞问题 #region 方法1:在最外层包一层线程。可以实现,浪费服务器资源,千万别乱包(3层以后,神仙难救,经验之谈,不推荐) Task.Run(() => { Task.WaitAny(taskList.ToArray()); //阻塞到任意一个任务完成(应用场景如:多渠道获取数据,redis、es、本地数据库 等等) Debug.WriteLine("任意一个人完成了任务!!!!"); Task.WaitAll(taskList.ToArray());//阻塞到全部task完成。 Debug.WriteLine("全部人完成任务了!!!!"); }); #endregion #region 方法2:使用Task和Task.Factory提供的接口。(推荐) //1、指定某个任务完成后,做非阻塞回调 taskList[0].ContinueWith(x => { Debug.WriteLine("非阻塞,开发1完成了任务!!!!"); }); //2、指定任意一个任务完成后,做非阻塞回调 Task.Factory.ContinueWhenAny(taskList.ToArray(), x => { Debug.WriteLine("非阻塞,任意一个人最先完成了任务!!!!"); }); //3、全部任务完成后,做非阻塞回调 Task.Factory.ContinueWhenAll(taskList.ToArray(), x => { Debug.WriteLine("非阻塞,全部人完成任务了!!!!"); }); #region 扩展(阻塞与非阻塞灵活搭配) //taskList[0].ContinueWith(x => { Debug.WriteLine("非阻塞,开发1完成了任务!!!!"); }); //taskList.Add(Task.Factory.ContinueWhenAny(taskList.ToArray(), x => { Debug.WriteLine("非阻塞,任意一个人最先完成了任务!!!!"); })); //taskList.Add(Task.Factory.ContinueWhenAll(taskList.ToArray(), x => { Debug.WriteLine("非阻塞,全部人完成任务了!!!!"); })); 此时上面的非阻塞可能会比下面的阻塞(WaitAll)还晚执行完,那不就有问题了吗,如果想让上面的非阻塞线程也加到下面的阻塞中怎么做? 答:可以把上面的非阻塞线程 加到任务list中。如:taskList.Add(Task.Factory.ContinueWhenAll()),这样的话WaitAll就可以完全阻塞所有线程了。 //Task.WaitAny(taskList.ToArray()); //Debug.WriteLine("阻塞,任意一个人最先完成了任务(包含非阻塞)"); //Task.WaitAll(taskList.ToArray());//阻塞线程,因为上面的非阻塞线程taskList.Add到了任务列表中,所以这里阻塞本身的 "阻塞线程" 和 "非阻塞线程"。 //Debug.WriteLine("阻塞,全部人完成任务了(包含非阻塞)"); #endregion #endregion #endregion Debug.WriteLine($"*****button3_Click end {Thread.CurrentThread.ManagedThreadId}*****"); // } 输出: 很清晰明了,可以知道:某一个指定任务啥时候完成、第一个完成的任务、所有任务完成后,再做相对应的回调方法。然后一些注释的地方也做了一个阻塞与非阻塞的例子搭配使用。这些方法可以基本实现90%的业务场景,灵活应用即可。 可能有人问:这是获得了任务的回调方法,怎么掌控执行顺序?那你可以想一下,既然你要控制他的执行顺序的话,那为啥还要用多线程,挨个调用不得了。。。没太大意义 多线程相对平均分配/// /// 多线程分配 /// /// /// private void button4_Click(object sender, EventArgs e) { ShowTaskDispatcher(); } /// /// 有100个任务并行执行,每个任务的量是不一样的可能是1w可能是10w... /// 给27个线程,怎么分配 /// 如果不知道任务数量怎么办?更没有规律,给27个线程,如何近似均匀的分配任务? /// /// private void ShowTaskDispatcher() { #region 初始化数据(模拟每个任务的量不一样,数组累加1到100) int[][] typeaArray = new int[100][]; for (int i = 0; i < 100; i++) { int[] innerArray = new int[i]; for (int j = 0; j < i; j++) { innerArray[j] = j; } typeaArray[i] = innerArray; } #endregion List taskList = new List(); int threadNum = 27; #region 方法1,手写代码分配。 //#region 线程任务划分 //List dispacherTypeList = new List(); //for (int i = 0; i < threadNum; i++) //{ // dispacherTypeList.Add(new List() { });//初始化27个空集合的数组 //} //for (int i = 0; i < 100; i++) //{ // dispacherTypeList[i % threadNum].Add(typeaArray[i]); //100个任务,分组平均分配 //} //#endregion //for (int i = 0; i < threadNum; i++) //{ // List currentTypeArray = dispacherTypeList[i];//分配好的线程的任务,注:此处一定得弄临时变量。 // taskList.Add(Task.Run(() => // { // foreach (var array in currentTypeArray) // { // Debug.WriteLine($"CurrentThreadId={Thread.CurrentThread.ManagedThreadId},执行{string.Join(",", array)}"); // foreach (var num in array) // { // Thread.Sleep(num); // } // } // Debug.WriteLine($"CurrentThreadId={Thread.CurrentThread.ManagedThreadId},任务完成"); // })); //} #endregion #region 方法2,利用Task.WaitAny来判断实现,好像没方法3快 //for (int i = 0; i < typeaArray.Length; i++) //{ // var currentTypeArray = typeaArray[i]; // taskList.Add(Task.Run(() => // { // Debug.WriteLine($"CurrentThreadId={Thread.CurrentThread.ManagedThreadId},执行{string.Join(",", currentTypeArray)}"); // foreach (var num in currentTypeArray) // { // Thread.Sleep(num); // } // Debug.WriteLine($"CurrentThreadId={Thread.CurrentThread.ManagedThreadId},任务完成"); // })); // if (taskList.Count == threadNum)//到了最大线程数 // { // Task.WaitAny(taskList.ToArray());//等待,只要有一个线程提前完成,说明有线程空闲 // taskList = taskList.Where(x => x.Status == TaskStatus.Running).ToList();//获取所有运行中的线程更新给集合(当然,不包含waitany完成的那个) // } //} #endregion #region 方法3,这个更快速 ParallelOptions options = new ParallelOptions() { MaxDegreeOfParallelism = threadNum//最大线程数 }; Parallel.ForEach(typeaArray, options, currentTypeArray => { Debug.WriteLine($"CurrentThreadId={Thread.CurrentThread.ManagedThreadId},执行{string.Join(",", currentTypeArray)}"); foreach (var num in currentTypeArray) { Thread.Sleep(num); } Debug.WriteLine($"CurrentThreadId={Thread.CurrentThread.ManagedThreadId},任务完成"); }); #endregion Task.WaitAll(taskList.ToArray());// } 不展示输出了,因为方法3:Parallel.ForEach就是自动分配的,一开始配置了MaxDegreeOfParallelism=27 所以他会均匀的分配并执行任务。当然方法2也是可用的方法
|
CopyRight 2018-2019 实验室设备网 版权所有 |