.NET 异步多线程,Thread,ThreadPool,Task,Parallel

今天记录一下异步多线程的进阶历史,以及简单的使用方法

主要还是以Task,Parallel为主,毕竟用的比较多的现在就是这些了,再往前去的,除非是老项目,不然真的应该是挺少了,大概有个概念,就当了解一下进化史了

1:委托异步多线程,所有的异步都是基于委托来实现的

复制代码
#region 委托异步多线程
{
  //委托异步多线程
  Stopwatch watch = new Stopwatch();   watch.Start();   Console.WriteLine($"开始执行了,{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff")},,,,{Thread.CurrentThread.ManagedThreadId}");   Action<string> act = DoSomethingLong;   for (int i = 0; i < 5; i++)   {     //int ThreadId = Thread.CurrentThread.ManagedThreadId;//获取当前线程ID     string name = $"Async {i}";     act.BeginInvoke(name, null, null);   }   watch.Stop();   Console.WriteLine($"结束执行了,{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff")},,,,{watch.ElapsedMilliseconds}"); } #endregion
复制代码

 

2:多线程的最老版本:Thread(好像是2.0的时候出的?记不得了)

复制代码
//Thread
//Thread默认属于前台线程,启动后必须完成
//Thread有很多Api,但大多数都已经不用了 Console.WriteLine("Thread多线程开始了"); Stopwatch watch = new Stopwatch(); watch.Start(); //线程容器 List<Thread> list = new List<Thread>(); for (int i = 0; i < 5; i++) { //int ThreadId = Thread.CurrentThread.ManagedThreadId;//获取当前线程ID string name = $"Async {i}"; ThreadStart start1 = () => { DoSomethingLong(name); }; Thread thread = new Thread(start1); thread.IsBackground = true;//设置为后台线程,关闭后立即退出  thread.Start(); list.Add(thread); //thread.Suspend();//暂停,已经不用了 //thread.Resume();//恢复,已经不用了 //thread.Abort();//销毁线程 //停止线程靠的不是外部力量,而是线程自身,外部修改信号量,线程检测信号量 } //判断当前线程状态,来做线程等待 while (list.Count(t => t.ThreadState != System.Threading.ThreadState.Stopped) > 0) {   Console.WriteLine("等待中.....");   Thread.Sleep(500); } //统计正确全部耗时,通过join来做线程等待 foreach (var item in list) {   item.Join();//线程等待,表示把thread线程任务join到当前线程,也就是当前线程等着thread任务完成   //等待完成后统计时间 } watch.Stop();
复制代码

Thread的无返回值回调:

封装一个方法

复制代码
/// <summary>
/// 回调封装,无返回值
/// </summary> /// <param name="start"></param> /// <param name="callback"></param> private static void ThreadWithCallback(ThreadStart start, Action callback) {   ThreadStart nweStart = () =>   {     start.Invoke();     callback.Invoke();   };   Thread thread = new Thread(nweStart);   thread.Start(); }
复制代码
复制代码
//回调的委托
Action act = () =>
{
    Console.WriteLine("回调函数"); }; //要执行的异步操作 ThreadStart start = () => { Console.WriteLine("正常执行。。"); }; ThreadWithCallback(start, act);
复制代码

有返回值的回调:

复制代码
/// <summary>
/// 回调封装,有返回值
/// 想要获取返回值,必须要有一个等待的过程 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="func"></param> /// <returns></returns> private static Func<T> ThreadWithReturn<T>(Func<T> func) {   T t = default(T);   //ThreadStart本身也是一个委托   ThreadStart start = () =>     {       t = func.Invoke();     };   //开启一个子线程   Thread thread = new Thread(start);   thread.Start();   //返回一个委托,因为委托本身是不执行的,所以这里返回出去的是还没有执行的委托   //等在获取结果的地方,调用该委托   return () =>   {     //只是判断状态的方法     while (thread.ThreadState != System.Threading.ThreadState.Stopped)     {       Thread.Sleep(500);     }     //使用线程等待     //thread.Join();     //以上两种都可以     return t;   }; }
复制代码
复制代码
Func<int> func = () =>
{
  Console.WriteLine("正在执行。。。");   Thread.Sleep(10000);   Console.WriteLine("执行结束。。。");   return DateTime.Now.Year; }; Func<int> newfunc = ThreadWithReturn(func); //这里为了方便测试,只管感受回调的执行原理 Console.WriteLine("Else....."); Thread.Sleep(100); Console.WriteLine("Else....."); Thread.Sleep(100); Console.WriteLine("Else....."); //执行回调函数里return的委托获取结果 //newfunc.Invoke(); Console.WriteLine($"有参数回调函数的回调结果:{newfunc.Invoke()}");
复制代码

关于有返回值的回调,因为委托是在执行Invoke的时候才会去调用委托的方法,所以在调用newfunc.Invoke()的时候,才会去委托里面抓取值,这里会有一个等待的过程,等待线程执行完成

 

3:ThreadPool:线程池

线程池是在Thread后出的,算是一种很给力的进化

在Thread的年代,线程是直接从计算机里抓取的,而线程池的出现,就是给开发人员提供一个容器,可以从容器中抓取线程,也就是线程池

好处就在于可以避免频繁的创建和销毁线程,直接从线程池拿线程,用完在还回去,当不够的时候,线程池在从计算机中给你分配,岂不是很爽?

线程池的数量是可以控制的,最小线程数量:8

复制代码
ThreadPool.QueueUserWorkItem(p =>
{
  //这里的p是没有值的
  Console.WriteLine(p);
  Thread.Sleep(2000);   Console.WriteLine($"线程池线程,{Thread.CurrentThread.ManagedThreadId}"); }); ThreadPool.QueueUserWorkItem(p => {   //这里的p就是传进来的值   Console.WriteLine(p);   Thread.Sleep(2000);   Console.WriteLine($"线程池线程,带参数,{Thread.CurrentThread.ManagedThreadId}"); }, "这里是参数");
复制代码

线程池用起来还是很方便的,也可以控制线程数量

线程池里有两种线程:普通线程,IO线程,我比较喜欢在操作IO的时候使用ThreadPool

复制代码
int workerThreads = 0;
int completionPortThreads = 0; //设置线程池的最大线程数量(普通线程,IO线程) ThreadPool.SetMaxThreads(80, 80); //设置线程池的最小线程数量(普通线程,IO线程) ThreadPool.SetMinThreads(8, 8); ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads); Console.WriteLine($"当前可用最大普通线程:{workerThreads},IO:{completionPortThreads}"); ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads); Console.WriteLine($"当前可用最小普通线程:{workerThreads},IO:{completionPortThreads}");
复制代码

ThreadPool并没有Thread的Join等待接口,那么想让ThreadPool等待要这么做呢?

ManualResetEvent:通知一个或多个正在等待的线程已发生的事件,相当于发送了一个信号

mre.WaitOne:卡住当前主线程,一直等到信号修改为true的时候,才会接着往下跑

复制代码
//用来控制线程等待,false默认为关闭状态
ManualResetEvent mre = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(p => {   DoSomethingLong("控制线程等待");   Console.WriteLine($"线程池线程,带参数,{Thread.CurrentThread.ManagedThreadId}");   //通知线程,修改信号为true   mre.Set(); }); //阻止当前线程,直到等到信号为true在继续执行 mre.WaitOne(); //关闭线程,相当于设置成false mre.Reset(); Console.WriteLine("信号被关闭了"); ThreadPool.QueueUserWorkItem(p => {   Console.WriteLine("再次等待");   mre.Set(); }); mre.WaitOne();
复制代码

 

4:Task,这也是现在用的最多的了,毕竟是比较新的玩意

关于Task就介绍几个最常用的接口,基本上就够用了(一招鲜吃遍天)

Task.Factory.StartNew:创建一个新的线程,Task的线程也是从线程池中拿的(ThreadPool)

Task.WaitAny:等待一群线程中的其中一个完成,这里是卡主线程,一直等到一群线程中的最快的一个完成,才能继续往下执行(20年前我也差点被后面的给追上),打个简单的比方:从三个地方获取配置信息(数据库,config,IO),同时开启三个线程来访问,谁快我用谁。

Task.WaitAll:等待所有线程完成,这里也是卡主线程,一直等待所有子线程完成任务,才能继续往下执行。

Task.ContinueWhenAny:回调形式的,任意一个线程完成后执行的后续动作,这个就跟WaitAny差不多,只不是是回调形式的。

Task.ContinueWhenAll:回调形式的,所有线程完成后执行的后续动作,理解同上

复制代码
//线程容器 List<Task> taskList = new List<Task>();
Stopwatch watch = new Stopwatch();
watch.Start();
Console.WriteLine("************Task Begin**************"); //启动5个线程 for (int i = 0; i < 5; i++) {   string name = $"Task:{i}";   Task task = Task.Factory.StartNew(() =>   {     DoSomethingLong(name);   });   taskList.Add(task); } //回调形式的,任意一个完成后执行的后续动作 Task any = Task.Factory.ContinueWhenAny(taskList.ToArray(), task => {   Console.WriteLine("ContinueWhenAny"); }); //回调形式的,全部任务完成后执行的后续动作 Task all = Task.Factory.ContinueWhenAll(taskList.ToArray(), tasks => {   Console.WriteLine($"ContinueWhenAll{tasks.Length}"); }); //把两个回调也放到容器里面,包含回调一起等待 taskList.Add(any); taskList.Add(all); //WaitAny:线程等待,当前线程等待其中一个线程完成 Task.WaitAny(taskList.ToArray()); Console.WriteLine("WaitAny"); //WaitAll:线程等待,当前线程等待所有线程的完成 Task.WaitAll(taskList.ToArray()); Console.WriteLine("WaitAll"); Console.WriteLine($"************Task End**************{watch.ElapsedMilliseconds},{Thread.CurrentThread.ManagedThreadId}");
复制代码

关于Task其实只要熟练掌握这几个接口,基本上就够了,完全够用

 

5:Parallel(并行编程):其实就是在Task基础上又进行了一次封装,当然,Parallel也有很酷炫功能

问:首先是为什么叫做并行编程,跟Task有什么区别?

答:使用Task开启子线程的时候,主线程是属于空闲状态,并不参与执行(我是领导,有5件事情需要处理,我安排了5个手下去做,而我本身就是观望状态 PS:到底是领导。),Parallel开启子线程的时候,主线程也会参与计算(我是领导,我有5件事情需要处理,我身为领导,但是我很勤劳,所以我做了一件事情,另外四件事情安排4个手下去完成),很明显,减少了开销。

你以为Parallel就只有这个炫酷的功能?大错特错,更炫酷的还有;

Parallel可以控制线程的最大并发数量,啥意思?就是不管你是脑溢血,还是心脏病,还是动脉大出血,我的手术室只有2个,同时也只能给两个人做手术,做完一个在进来一个,同时做完两个,就同时在进来两个,多了不行。

当前,你想使用Task,或者ThreadPool来实现这样的效果也是可以的,不过这就需要你动动脑筋了,当然,有微软给封装好的接口直接使用,岂不美哉?

复制代码
 //并行编程 
Console.WriteLine($"*************Parallel start********{Thread.CurrentThread.ManagedThreadId}****"); //一次性执行1个或多个线程,效果类似:Task WaitAll,只不过Parallel的主线程也参与了计算 Parallel.Invoke(   () => { DoSomethingLong("Parallel`1"); },   () => { DoSomethingLong("Parallel`2"); },   () => { DoSomethingLong("Parallel`3"); },   () => { DoSomethingLong("Parallel`4"); },   () => { DoSomethingLong("Parallel`5"); }); //定义要执行的线程数量 Parallel.For(0, 5, t => {   int a = t;   DoSomethingLong($"Parallel`{a}"); }); {   ParallelOptions options = new ParallelOptions()   {     MaxDegreeOfParallelism = 3//执行线程的最大并发数量,执行完成一个,就接着开启一个   };   //遍历集合,根据集合数量执行线程数量   Parallel.ForEach(new List<string> { "a", "b", "c", "d", "e", "f", "g" }, options, t =>   {     DoSomethingLong($"Parallel`{t}");   }); } {   ParallelOptions options = new ParallelOptions()   {     MaxDegreeOfParallelism = 3//执行线程的最大并发数量,执行完成一个,就接着开启一个   };   //遍历集合,根据集合数量执行线程数量   Parallel.ForEach(new List<string> { "a", "b", "c", "d", "e", "f", "g" }, options, (t, status) =>   {     //status.Break();//这一次结束。     //status.Stop();//整个Parallel结束掉,Break和Stop不可以共存     DoSomethingLong($"Parallel`{t}");   }); } Console.WriteLine("*************Parallel end************");
复制代码

可以多了解一下Parallel的接口,里面的用法有很多,我这里也只是列出了比较常用的几个,像ParallelOptions类可以控制并发数量,当然,不可以也可以,Parallel的重载方法很多,可以自己看看

 

6:线程取消,异常处理

关于线程取消这块呢,要记住一点,线程只能自身终结自身,也就是除非我自杀,否则你干不掉我,不要妄想通过主线程来控制计算中的子线程。

关于线程异常处理这块呢,想要捕获子线程的异常,最好在子线程本身抓捕,也可以使用Task的WaitAll,不过这种方法不推荐,如果用了,别忘了一点,catch里面放的是AggregateException,不是Exception,不然捕捉不到别怪我

复制代码
TaskFactory taskFactory = new TaskFactory();
//通知线程是否取消
CancellationTokenSource cts = new CancellationTokenSource(); List<Task> taskList = new List<Task>(); //想要主线程抓捕到子线程异常,必须使用Task.WaitAll,这种方法不推荐 //想要捕获子线程的异常,最好在子线程本身抓捕 //完全搞不懂啊,看懵逼了都 try {   for (int i = 0; i < 40; i++)   {     int name = i;     //在子线程中抓捕异常     Action<object> a = t =>     {       try       {         Thread.Sleep(2000);         Console.WriteLine(cts.IsCancellationRequested);         if (cts.IsCancellationRequested)         {           Console.WriteLine($"放弃执行{name}");         }         else         {           if (name == 1)           {             throw new Exception($"取消了线程{name}{t}");           }           if (name == 5)           {             throw new Exception($"取消了线程{name}{t}");           }           Console.WriteLine($"执行成功:{name}");         }       }       catch (Exception ex)       {         //通知线程取消,后面的都不执行         cts.Cancel();         Console.WriteLine(ex.Message);       }     };   taskList.Add(taskFactory.StartNew(a, name, cts.Token));   }     Task.WaitAll(taskList.ToArray()); } catch (AggregateException ex) {   foreach (var item in ex.InnerExceptions)   {     Console.WriteLine(item.Message);   } } 
复制代码

 

7:给线程上个锁,防止并发的时候数据丢失,覆盖等

//先准备三个公共变量
private static int iIndex; private static object obj = new object(); private static List<int> indexList = new List<int>();
复制代码
List<Task> taskList = new List<Task>();
//开启1000个线程
for (int i = 0; i < 10000; i++) {   int newI = i;   Task task = Task.Factory.StartNew(() =>   {   iIndex += 1;   indexList.Add(newI);   });   taskList.Add(task); } //等待所有线程完成 Task.WhenAll(taskList.ToArray()); //打印结果 Console.WriteLine(iIndex); Console.WriteLine(indexList.Count);
复制代码

给你们看一下我这里运行三次打印出的结果

第一次:

第二次:

第三次:

看的出来,还是比较稳定的只丢失那么几个数据的对吧?

为啥会这样呢?因为当两个线程同时操作一个数据的时候,你觉得会以谁的操作为标准来保存?就好像我们操作IO的时候,不允许多多个线程同时操作一个IO,因为计算机不知道以谁的标准来保存修改

下面给它加个锁,稍微修改一下代码:

复制代码
List<Task> taskList = new List<Task>();
//开启1000个线程
for (int i = 0; i < 10000; i++) { int newI = i; Task task = Task.Factory.StartNew(() => { //加个锁 lock (objLock) { iIndex += 1; indexList.Add(newI); } }); taskList.Add(task); } //等待所有线程完成 Task.WhenAll(taskList.ToArray()); //打印结果 Console.WriteLine(iIndex); Console.WriteLine(indexList.Count);
复制代码

在看一下运行结果:

线程锁会破坏线程,增加耗时,降低效率,不要看效果很爽就到处加锁,万一你钥匙丢了呢(死锁);

共有变量:都能访问的局部变量/全局变量/数据库的值/硬盘文件,这些都有可能是数据不安全的,如果有需求,还是得加个锁

如果确实是要用到锁,推荐大家就使用一个:私有的,静态的,object类型的变量就可以了;

 

漏掉了一个方法,我给补上:

复制代码
/// <summary>
/// 一个比较耗时的方法,循环1000W次
/// </summary> /// <param name="name"></param> public static void DoSomethingLong(string name) { int iResult = 0; for (int i = 0; i < 1000000000; i++) { iResult += i; } Console.WriteLine($"********************{name}*******{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff")}****{Thread.CurrentThread.ManagedThreadId}****"); }
复制代码

 

转载于:https://www.cnblogs.com/MuNet/p/8545139.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.几种同步方法的区别 lock和Monitor是.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方 面可能更为有效,同步速度较快,但不能跨进程同步。lock(Monitor.Enter和Monitor.Exit方法的封装),主要作用是锁定临界区,使临 界区代码只能被获得锁的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死 锁。 互斥体Mutex和事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模 式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。 互斥体Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热 闹。 EventWaitHandle 类允许线程通过发信号互相通信。 通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。 2.什么时候需要锁定 首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程 同时加一,后果就是损失了一个计数,但相当频繁的锁定又可能带来性能上的消耗,还有最可怕的情况死锁。那么什么情况下我们需要使用锁,什么情况下不需要 呢? 1)只有共享资源才需要锁定 只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存中的值,而属于线程内部的变量不需要锁定。 2)多使用lock,少用Mutex 如果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清 楚的了解到他们的不同和适用范围。 3)了解你的程序是怎么运行的 实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑锁 定,当然对于ASP.NET中的Application对象中的数据,我们就要考虑加锁了。 4)把锁定交给数据库 数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条记录时,我们才考虑加锁。 5)业务逻辑对事务和线程安全的要求 这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,和很多的开发时间来做这方面的工作。而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用锁定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管它。 3.InterLocked类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存中,则不同进程的线程就可以使用该机制。互锁操作是原子的,即整个操作是不能由相 同变量上的另一个互锁操作所中断的单元。这在抢先多线程操作系统中是很重要的,在这样的操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改 和存储该值之前被挂起。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值