参考资料:多线程MutiThread最佳实践专题-1-1_哔哩哔哩_bilibili
跟着视频学习的,下面是自己的注释笔记和实验结果
写了个窗体来学习线程的 多线程、线程掌控、线程分配
下面会用到这个方法
/// <summary>
/// 仅仅只是一个耗时方法
/// </summary>
/// <param name="name"></param>
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}*****");
}
单线程
只是个简单的例子,用于与下面多线程来做对比和参考
/// <summary>
/// 单线程
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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(" ");
}
输出:
可以看到是按照顺序输出了五遍,并且相隔一两秒。
多线程
/// <summary>
/// 多线程
/// 一、.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
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<string>#region 常见错误
//注意!下面这句代码执行会出现i重复或乱掉的问题,这是一个执行时机和上下文的问题:
//前者正确但是这句代码不正确,是因为循环时i没有阻塞(或者说循环内i是公共的),动作是交给Task线程去执行的(Task.Run并非立即执行,他只是交给线程的一个通知)。
//Task线程是什么时候启动的?这个是不确定的,因为电脑系统等着执行的时候,此时i已经不是之前的i了,因为i可能已经循环到8或者10了,所以会有i重复或乱掉的出现
//前者可以,是因为在循环时把i绑定到name变量上(每次循环都是新的name,此时阻塞了一下)
//这个被称为:多线程的临时变量问题
//Action action1 = () => DoSomethingLong($"button1_Click_{i}");
#endregionTask.Run(action);//开启多线程!(循环5次就是5个线程并发执行)
}Debug.WriteLine($"*****button1_Click 同步方法 end {Thread.CurrentThread.ManagedThreadId}*****");
Debug.WriteLine(" ");
}
输出:
可以看到五个线程是同时执行的,到最后结束的时间都基本相同
线程结束顺序的掌控
/// <summary>
/// 线程结束顺序的掌控(线程的 阻塞&非阻塞)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button3_Click(object sender, EventArgs e)
{
Debug.WriteLine($"*****button3_Click start {Thread.CurrentThread.ManagedThreadId}*****");
List<Task> taskList = new List<Task>();
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%的业务场景,灵活应用即可。
可能有人问:这是获得了任务的回调方法,怎么掌控执行顺序?那你可以想一下,既然你要控制他的执行顺序的话,那为啥还要用多线程,挨个调用不得了。。。没太大意义
多线程相对平均分配
/// <summary>
/// 多线程分配
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button4_Click(object sender, EventArgs e)
{
ShowTaskDispatcher();
}/// <summary>
/// 有100个任务并行执行,每个任务的量是不一样的可能是1w可能是10w...
/// 给27个线程,怎么分配
/// 如果不知道任务数量怎么办?更没有规律,给27个线程,如何近似均匀的分配任务?
///
/// </summary>
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;
}
#endregionList<Task> taskList = new List<Task>();
int threadNum = 27;
#region 方法1,手写代码分配。
//#region 线程任务划分
//List<List<int[]>> dispacherTypeList = new List<List<int[]>>();
//for (int i = 0; i < threadNum; i++)
//{
// dispacherTypeList.Add(new List<int[]>() { });//初始化27个空集合的数组
//}
//for (int i = 0; i < 100; i++)
//{
// dispacherTypeList[i % threadNum].Add(typeaArray[i]); //100个任务,分组平均分配
//}
//#endregion//for (int i = 0; i < threadNum; i++)
//{
// List<int[]> 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也是可用的方法