多线程Task详解

锦通  金牌会员 | 2024-12-18 22:23:42 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 880|帖子 880|积分 2640

1. 常用多线程比力


1.1 Thread



  • 界说:Thread(线程)是操作系统中能够独立运行的代码段,是程序实行流的最小单元。它是CPU调度的基本单元,是进程中的一个实体,是CPU分配资源的基本单元,它是比进程更独立运行的单元,线程一般不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
  • 优点:Thread方式提供直接控制线程生命周期的机动性,包括创建、启动和终止。每个Thread独立并行实行,需开辟者通过同步机制协调交互。适用于精确控制线程行为或复杂交互的场景,且具有良好的跨平台性。
  • 缺点:频仍创建和烧毁线程会斲丧较多系统资源。此外,由于线程管理是由操作系统进行的,对线程的操作(如启动、停止)不会立刻得到响应,存在一定的延迟。

1.2 ThreadPool



  • 界说:ThreadPool(线程池)是并发编程中一种常用的技术,用于管理和重用线程。它由线程池管理器、工作队列和线程池线程构成,旨在提高系统性能和资源利用率
  • 优点:通过线程池管理线程,有效避免了频仍创建和烧毁线程的开销,实现了线程的复用,提高了性能。
  • 缺点:虽然提供了基本的线程管理功能,但API相对简朴,对于复杂的线程等待顺序控制场景支持较弱,可能不满足所有业务需求。

1.3 Task



  • 界说:Task是.NET Framework 4.0中引入的一个类,用于表示一个异步操作。它允许开辟者以非壅闭的方式实行耗时的操作,从而提高应用程序的响应性和性能。
  • 线程池基础:Task是在ThreadPool(线程池)的基础上推出的。ThreadPool中有一定数目的线程,当有新任务必要处置惩罚时,会从线程池中获取一个空闲的线程来实行任务。任务实行完毕后,线程不会被烧毁,而是被线程池回收以供后续任务使用。
  • 优点:Task继承了ThreadPool的优点,同时提供了更为丰富和机动的线程控制API,提供了一种简洁的异步编程模型,使得开辟者能够更容易地编写、明确和维护异步代码。。

1.4 使用多线程场景

适用场景:当任务可以并发实行时,使用多线程可以显著提高程序的实行效率。比方,在大型项目中,可能必要同时从多个数据源(如数据库、缓存、第三方接口)获取数据,这时开启多个线程并行查询可以大幅淘汰等待时间。


  • 用户界面(UI)响应性提升
    在图形用户界面(GUI)应用程序中,如果某个操作(如文件加载、数据计算或网络哀求)必要较长时间才能完成,使用多线程可以避免壅闭UI线程,从而保持用户界面的响应性。用户可以继承与应用程序交互,而无需等待长时间运行的任务完成。
  • 并行处置惩罚大量数据
    当必要处置惩罚大量数据时,可以将数据分割成小块,并使用多线程或线程池并行处置惩罚这些小块。这样可以显著淘汰处置惩罚时间,提高程序的效率。比方,在大数据处置惩罚、图像处置惩罚或科学计算等领域中,并行处置惩罚尤为告急。
  • 网络通信
    在网络应用程序中,如服务器应用程序,可能必要同时处置惩罚多个客户端的哀求。使用多线程可以使得服务器能够同时为多个客户端提供服务,提高了服务器的并发处置惩罚本领。
  • 多任务并发实行
    在一些应用程序中,可能必要同时实行多个独立的任务。比方,一个游戏可能必要同时处置惩罚用户输入、图形渲染、音频播放和物理模拟等多个任务。通过将这些任务分配给不同的线程,可以实现更好的并发性和性能。
  • 后台任务处置惩罚
    在必要实行一些不紧急但耗时的后台任务时(如日志记录、数据备份、定时任务等),可以使用多线程来实行这些任务,从而避免对主程序流程的影响。
  • 资源密集型任务
    当程序必要实行一些CPU密集型或I/O密集型任务时,使用多线程可以更有效地利用多核CPU的资源,或同时处置惩罚多个I/O操作,提高团体性能。
  • 模拟并发用户
    在测试阶段,为了模拟真真相况中的高并发场景,可以使用多线程来模拟多个用户同时访问系统,从而评估系统的并发处置惩罚本领和性能体现。
  • 及时系统
    在一些对及时性要求较高的系统中(如及时监控系统、及时买卖业务系统等),使用多线程可以确保系统能够及时处置惩罚各种事件和哀求,保证系统的及时性。

2.Task创建任务方式


2.1先创建后实行

起首通过new Task关键字创建一个Task实例,并传入一个表示任务体的lambda表达式或委托。然后通过调用Start方法来启动该任务。
  1. Task task = new Task(() => {  
  2.     // 方法体  
  3. });  
  4. task.Start();
复制代码

2.2创建并实行

使用Task.Run方法可以直接创建并启动一个任务。Task.Run方法内部会调用Task.Factory.StartNew,但它提供了更简洁的语法,并且默认使用线程池中的线程来实行任务。
  1. Task task =Task.Run(() => {  
  2.     // 方法体  
  3. });
复制代码

2.3使用TaskFactory

TaskFactory 类是一个高级工厂,用于创建和启动 Task 和 Task 实例。使用 TaskFactory 可以更机动地控制任务的创建和启动方式,包括设置任务的取消令牌、任务创建选项以及任务的父子关系等。
通过TaskFactory的StartNew方法可以创建并启动一个任务。TaskFactory提供了比Task.Run更多的配置选项,比方可以指定任务的创建选项、调度程序等。
  1. TaskFactory taskFactory = Task.Factory;  
  2. Task task = taskFactory.StartNew(() => {  
  3.     // 方法体  
  4. });
复制代码

2.4延时实行

可以使用Task.Delay方法创建一个在指定时间后实行的Task,并通过ContinueWith来指定延时后的操作。
  1. Task task = Task.Delay(2000).ContinueWith(t => {  
  2.     // 方法体  
  3. });
复制代码

3 Task常用的API



  • Task.Run(Action action): 在ThreadPool上列队一个任务以实行指定的操作。
  • Task.WhenAll(Task[] tasks): 创建一个任务,该任务将在提供的所有任务完成后完成。
  • Task.WhenAny(Task[] tasks): 创建一个任务,该任务将在提供的任务之一完成时完成。
  • Task.Delay(int millisecondsDelay): 返回一个表示已完成的延迟的Task。
  • Task.ContinueWith(Func<Task, TResult> continuationFunction): 继承实行指定的操作,该操作在当前任务完成后实行。

3.1Task.Wait

Task.Wait 是 Task 的一个实例方法,用于等待 Task 完成,如果 Task 未完成,会壅闭当火线程。非必要情况下,不建议使用 Task.Wait,而应该使用 await。
  1. static void Main(string[] args)
  2.         {
  3.             Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}开启");
  4.             var task1 = Task.Run(() =>
  5.             {
  6.                 Console.WriteLine($"Task1 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  7.                 Thread.Sleep(10000);
  8.             });
  9.             task1.Wait();
  10.             var task2 = Task.Run(() =>
  11.             {
  12.                 Console.WriteLine($"Task2 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  13.                 Thread.Sleep(5000);
  14.             });
  15.             task2.Wait();
  16.             var task3 = Task.Run(() =>
  17.             {
  18.                 Console.WriteLine($"Task3 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  19.                 Thread.Sleep(500);
  20.             });
  21.             task3.Wait();
  22.             var task4 = Task.Run(() =>
  23.             {
  24.                 Console.WriteLine($"Task4 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  25.                 Thread.Sleep(2000);
  26.             });
  27.             task4.Wait();
  28.             Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}完成");
  29.             Console.ReadLine();
  30.         }
复制代码
 输出结果:
  1. 主线程1开启
  2. Task1 开启线程3处理业务 //(等待10秒)
  3. Task2 开启线程3处理业务 //(等待5秒)
  4. Task3 开启线程4处理业务 //(几乎立即,因为只等待了500毫秒)
  5. Task4 开启线程3处理业务 //(等待2秒)
  6. 主线程1完成
复制代码

3.1.1Task.Wait导致的问题


  • 死锁
    在 UI 或 ASP.NET 上下文中,Task.Wait(或 Task<T>.Result,它内部也调用 Wait)可能会导致死锁。这是由于在这些情况中,有一个同步上下文(比方,UI 线程或 ASP.NET 哀求上下文),它用于确保某些操作(如更新 UI 或响应 HTTP 哀求)只在精确的线程上实行。当 Task.Wait 被调用时,它会实验捕捉当前同步上下文(如果有的话),并在等待任务完成时重新捕捉它。然而,如果任务自己试图返回该同步上下文(比方,通过更新 UI 或实验访问 HttpContext),则可能会导致死锁,由于两个线程(等待的线程和任务线程)都在等待对方开释锁。
  • 壅闭线程
    Task.Wait 会壅闭调用它的线程,直到任务完成。如果任务必要很长时间才能完成,或者如果有很多这样的等待操作,那么这可能会导致性能问题,由于名贵的线程资源被浪费了。
  • 异常处置惩罚复杂
    当使用 Task.Wait 时,如果任务抛出异常,那么这些异常会在 Wait 方法返回后被封装在 AggregateException 中抛出。这增加了异常处置惩罚的复杂性,由于你必要查抄 AggregateException 的内部异常。相比之下,使用 await 可以更直接地处置惩罚异常,由于 await 会将任务中的异常直接抛出,就像它们是同步代码中的异常一样。
  • 不支持取消
    虽然 Task 支持取消(通过 CancellationToken),但 Task.Wait 方法自己并不直接支持取消等待操作。你可以通过传递一个 CancellationToken 给 Task.Run 或任务自己,并在任务内部查抄取消哀求,但 Wait 方法自己不会由于取消哀求而提前返回。
  • 难以调试
    由于 Task.Wait 可以导致死锁和壅闭线程,这可能会使调试异步代码变得更加困难。线程可能看似“挂起”而没有明显的缘故原由,尤其是在复杂的并发场景中。
为了避免这些问题,建议使用 async 和 await 关键字来编写异步代码。这些关键字提供了一种更自然、更简洁的方式来处置惩罚异步操作,同时避免了 Task.Wait 带来的许多问题。async 方法会隐式地返回一个 Task 或 Task<T>,而 await 关键字则用于等待任务完成,但它不会壅闭调用线程,并且可以以更优雅的方式处置惩罚异常和取消。

3.2Task.ContinueWith

ContinueWith() 等调用者结束之后才进行调用内里的相干业务,由线程池分配线程进行处置惩罚接下来的业务,不壅闭主线程,但却能控制业务之间的先后顺序;
  1. static void Main(string[] args)
  2.         {
  3.             Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}开启");
  4.             var task1 = Task.Run(() =>
  5.             {
  6.                 Console.WriteLine($"Task1 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  7.                 Thread.Sleep(10000);
  8.             });
  9.             var task2 = Task.Run(() =>
  10.             {
  11.                 Console.WriteLine($"Task2 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  12.                 Thread.Sleep(5000);
  13.             });
  14.            
  15.             var task3 = Task.Run(() =>
  16.             {
  17.                 Console.WriteLine($"Task3 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  18.                 Thread.Sleep(500);
  19.             });
  20.             task3.ContinueWith(t =>
  21.             {
  22.                 Console.WriteLine($"Task3 后续执行{Thread.CurrentThread.ManagedThreadId}处理业务");
  23.                 Thread.Sleep(500);
  24.             });
  25.             var task4 = Task.Run(() =>
  26.             {
  27.                 Console.WriteLine($"Task4 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  28.                 Thread.Sleep(2);
  29.             });
  30.          
  31.             Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}完成");
  32.             Console.ReadLine();
  33.         }
复制代码
输出结果:
  1. 主线程1开启
  2. 主线程1完成
  3. Task1 开启线程3处理业务 //task3是最快完成的,因为它只睡眠了500毫秒
  4. Task4 开启线程6处理业务 //
  5. Task2 开启线程4处理业务
  6. Task3 开启线程5处理业务
  7. Task3 后续执行9处理业务
复制代码

3.3Task.WaitAll

同Task.WaitAll,等待任何一个任务完成就继承向下实行,将上面的代码WaitAll更换为WaitAny
即当task,task2,task3…N恣意一个任务都实行完成之后就会往下实行代码,
task.WaitAny等到其中一个任务完成之后,才进行主线程的下一步操作,其中任务没有完成之前也壅闭主线程;
3.4Task.Factory.ContinueWhenAll
当ContinueWhenAll中所有任务都完成时实行回调方法,不壅闭主线程
  1. static void Main(string[] args)
  2.         {
  3.             Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}开启");
  4.             var task1 = Task.Run(() =>
  5.             {
  6.                 Console.WriteLine($"Task1 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  7.                 Thread.Sleep(10000);
  8.             });
  9.             var task2 = Task.Run(() =>
  10.             {
  11.                 Console.WriteLine($"Task2 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  12.                 Thread.Sleep(5000);
  13.             });
  14.            
  15.             var task3 = Task.Run(() =>
  16.             {
  17.                 Console.WriteLine($"Task3 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  18.                 Thread.Sleep(500);
  19.             });
  20.             var task4 = Task.Run(() =>
  21.             {
  22.                 Console.WriteLine($"Task4 开启线程{Thread.CurrentThread.ManagedThreadId}处理业务");
  23.                 Thread.Sleep(2);
  24.             });
  25.             List<Task> list1 = new List<Task> {task1,task2,task3,task4};
  26.             Task.Factory.ContinueWhenAll(list1.ToArray (),tasks =>
  27.             {
  28.                 Console.WriteLine($"任务执行结束");
  29.             });
  30.          
  31.             Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}完成");
  32.             Console.ReadLine();
  33.         }
复制代码
 输出结果:
  1. 主线程1开启
  2. 主线程1完成
  3. Task1 开启线程3处理业务
  4. Task3 开启线程8处理业务
  5. Task2 开启线程4处理业务
  6. Task4 开启线程9处理业务
复制代码

3.5Task.Factory.ContinueWhenAny

当参数中的任务有一个完成之后就进行回调,实行下一个任务。
Task.Factory.ContinueWhenAny方法等其中的任务有一项完成之后就立刻返回,调用后续业务,不壅闭主线程
实际开辟中建议使用ContinueWhenAny、ContinueWhenAll不壅闭线程,尤其是在UI界面开辟中
3.6Task.Delay与Thread.Sleep的区别
这两个函数,实际工程中也经常用到,都表示延期实行某个功能,但是 Thread.Sleep在延期时间内会壅闭主线程,比方:Thread.Sleep(5000),在UI界面中会卡顿界面5秒,界面无法实行其他操作,缘故原由是Thread属性IsBackground默认为前台线程,Task.Delay(5000)延时期间不会壅闭主线程

4.Task任务取消

在C#中,你可以使用CancellationToken来取消Task。你必要将CancellationToken作为参数传递给任务,并在任务内部定期查抄是否已哀求取消。
  1. static async Task Main(string[] args)
  2.     {
  3.         var cts = new CancellationTokenSource();
  4.         // 启动一个长时间运行的任务  
  5.         var task = LongRunningOperation(cts.Token);
  6.         // 假设在一段时间后取消任务  
  7.         await Task.Delay(2000);
  8.         cts.Cancel();
  9.         try
  10.         {
  11.             await task;
  12.         }
  13.         catch (OperationCanceledException)
  14.         {
  15.             Console.WriteLine("任务已取消");
  16.         }
  17.     }
  18.     static async Task LongRunningOperation(CancellationToken cancellationToken)
  19.     {
  20.         for (int i = 0; i < 10; i++)
  21.         {
  22.             if (cancellationToken.IsCancellationRequested)
  23.             {
  24.                 cancellationToken.ThrowIfCancellationRequested();
  25.             }
  26.             Console.WriteLine($"正在执行... {i}");
  27.             await Task.Delay(1000, cancellationToken); // 注意这里也传递了cancellationToken  
  28.         }
  29.     }
复制代码
输出结果:
  1. 正在执行...0
  2. 正在执行...1
  3. 正在执行...2 //之后退出程序
复制代码

5.线程同步、异步

5.1线程同步

常见例子:我们用饭用手机点菜的时间,多个人同时点菜,在最后结账的时间,如果大家都争着买单,那如果没有同步信息,就会造成多个人都买单成功。这就是线程同步的问题之一。
所谓的同步,即按照代码的顺序实行,也就是用同一个线程来实行所有的操作,或者多个线程按顺序实行,比方***4.1 实例方法.wait()***中的部分,多个线程按顺序实行。
有序性:告急针对程序的实行顺序来说.比如单线程编程中,A();B(); ,必须等待A方法实行完了,B方法才可以实行.再比如,lock(sync){A();},无论多少个线程调用这段代码,A方法在同一个时间只允许一个线程调用,其它线程必须等待
同等性:告急针对数据来说.我们必须确保对临界区数据的变更不会影响其它线程.比如说,A线程和B线程在某一段时间都对data进行修改,线程之间共享变量可能会造成死锁的现象,为避免出现两个线程同时修改,后者的修改将前者的修改覆盖掉,我们对data的修改进行加锁,这样data一次只会允许一个线程进行修改.也就保证了数据的同等性.
线程安全:多线程实行结果与单线程同等,线程安全
线程不安全:多线程同时修改一个变量;或者一个线程修改,一个线程读取,则可能出现BUG

在多线程问题的处置惩罚上,我们是在异步中谋求同步的目的,以确保程序的安全
线程同步的方法常见的告急有锁 SpinLock 、Mutex、Monitor、lock。以下仅介绍最常用的Monitor、lock方法

5.1.1lock方法

1、lock锁定的是一个引用范例,值范例不能被lock
2、避免lock一个string,由于程序中任何跟锁定的string的字符串是一样的都会被锁定。
模拟售票系统代码如下:
  1. internal class Program
  2.     {
  3.         int num = 10;
  4.         void ticket()
  5.         {
  6.             while (true)        //无线循环
  7.             {
  8.                 lock (this)     //锁定代码快,以便线程同步
  9.                 {
  10.                     if (num > 0)
  11.                     {
  12.                         Thread.Sleep(1000);
  13.                         Console.WriteLine(Thread.CurrentThread.Name + "------票数" + num--);
  14.                     }
  15.                 }
  16.             }
  17.         }
  18.         static void Main(string[] args)
  19.         {
  20.             Program p=new Program();
  21.             Thread ta = new Thread(p.ticket);
  22.             ta.Name = "线程一";
  23.             Thread tb = new Thread(p.ticket);
  24.             tb.Name = "线程二";
  25.             Thread tc = new Thread(p.ticket);
  26.             tc.Name = "线程三";
  27.             Thread td = new Thread(p.ticket);
  28.             td.Name = "线程四";
  29.             ta.Start();
  30.             tb.Start();
  31.             tc.Start();
  32.             td.Start();
  33.             Console.ReadLine();
  34.         }
  35.     }
复制代码
输出结果:
  1. 线程四------票数10
  2. 线程二------票数9
  3. 线程一------票数8
  4. 线程三------票数7
  5. 线程四------票数6
  6. 线程二------票数5
  7. 线程一------票数4
  8. 线程三------票数3
  9. 线程四------票数2
  10. 线程二------票数1
复制代码
说明:4个线程调用的是同一个函数,同一时间,一次只允许一个线程进入,每次实行将票数减一,线程实行的顺序尽管是无序的,但是实行的“票数”都是在上一次的结果上减1。
倘若注释同步代码,即//lock (this) //锁定代码快,以便线程同步,实行结果如下图所示,同一时间,多个线程进入同一函数,线程顺序无序,实行结果也无序,“票数3”同时被“线程四”和“线程一”实行。
实际情况中,当不考虑退票时,票数应该是越卖越少,且不会出现多人购买同一张票都成功的现象。如果不使用线程同步的写法,得不到想要的功能。
  1. 线程一------票数10
  2. 线程二------票数9
  3. 线程四------票数8
  4. 线程三------票数7
  5. 线程一------票数6
  6. 线程二------票数5
  7. 线程四------票数4
  8. 线程三------票数3
  9. 线程一------票数2
  10. 线程二------票数1
复制代码

5.1.2monitor类

Monitor类提供了与lock类似的功能,不外与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会开释对o的独占权
可以使用 TryEnter() 方法可以给它传送一个超时值,决定等待获得对象锁的最长时间,该方法能在指定的毫秒数内结束线程,这样能避免线程之间的死锁现象。

5.2线程异步

异步与同步概念:当一个方法被调用时,调用者必要等待该方法实行完毕并返回才能继承实行,我们称这个方法是同步方法;当一个方法被调用时立刻返回,并获取一个线程实行该方法内部的业务,调用者不消等待该方法实行完毕,我们称这个方法为异步方法
异步方法的优点:异步线程最大的好处在于非壅闭,即各线程之间实行任务时,互不干扰,当任务完成之后就可立刻响应,不需等待其他任务是否实行完成。异步编程中最好用的便是 async和await 。
在C#5.0中出现的 async和await ,让异步编程变得更简朴,用同步的写法写异步同步代码的逻辑结构
线程异步与多任务实行关系:异步是同时实行多个任务,而Task则是允许多个任务可以在线程内同时进行,多任务的同时进行,则是由异步来进行,而异步方法,必须为Task

5.2.1控制台应用async和await

  1. class Program
  2. {
  3.     static void Main(string[] args)
  4.     {
  5.         Console.WriteLine("开始");
  6.         WriteAsync();
  7.         Console.WriteLine("结束");
  8.         Console.ReadKey();
  9.     }
  10.     static void WriteAsync()
  11.     {
  12.         Task.Run(() =>
  13.         {
  14.             for (int i = 0; i < 10; i++)
  15.             {
  16.                 Console.WriteLine("T " + i);
  17.             }
  18.         });
  19.         Task.Run(() =>
  20.         {
  21.             for (int i = 0; i < 10; i++)
  22.             {
  23.                 Console.WriteLine("S " + i);
  24.             }
  25.         });
  26.     }
  27. }
复制代码
输出结果有两种实行结果,未壅闭主线程,两次实行结果,S在上或者T在上,这是任务内部无序实行的结果:
  1. 开始
  2. 结束
  3. T 0
  4. T 1
  5. T 2
  6. T 3
  7. T 4
  8. T 5
  9. T 6
  10. T 7
  11. T 8
  12. T 9
  13. S 0
  14. S 1
  15. S 2
  16. S 3
  17. S 4
  18. S 5
  19. S 6
  20. S 7
  21. S 8
  22. S 9
复制代码
 或者:
  1. 开始
  2. 结束
  3. S 0
  4. S 1
  5. S 2
  6. S 3
  7. S 4
  8. S 5
  9. S 6
  10. S 7
  11. S 8
  12. S 9
  13. T 0
  14. T 1
  15. T 2
  16. T 3
  17. T 4
  18. T 5
  19. T 6
  20. T 7
  21. T 8
  22. T 9
复制代码
 加上await修饰:
  1. class Program
  2. {
  3.     static void Main(string[] args)
  4.     {
  5.         Console.WriteLine("开始");
  6.         WriteAsync();
  7.         Console.WriteLine("结束");
  8.         Console.ReadKey();
  9.     }
  10.     static async void WriteAsync()
  11.     {
  12.         await Task.Run(() =>
  13.         {
  14.             for (int i = 0; i < 10; i++)
  15.             {
  16.                 Console.WriteLine("T " + i);
  17.             }
  18.         });
  19.         await Task.Run(() =>
  20.         {
  21.             for (int i = 0; i < 10; i++)
  22.             {
  23.                 Console.WriteLine("S " + i);
  24.             }
  25.         });
  26.     }
  27. }
复制代码
输出结果:
  1. 开始
  2. 结束
  3. T 0
  4. T 1
  5. T 2
  6. T 3
  7. T 4
  8. T 5
  9. T 6
  10. T 7
  11. T 8
  12. T 9
  13. S 0
  14. S 1
  15. S 2
  16. S 3
  17. S 4
  18. S 5
  19. S 6
  20. S 7
  21. S 8
  22. S 9
复制代码

6.多线程异常处置惩罚

6.1单线程异常与多线程异常比对

单线程处置惩罚:try-catch,catch中进6行异常处置惩罚,捕捉异常----处置惩罚异常
  1.                         try
  2.             {
  3.                 {
  4.                     //捕捉异常
  5.                 }
  6.             }
  7.             catch
  8.             {
  9.                 {
  10.                     throw;
  11.                     //处理异常
  12.                 }
  13.             }
复制代码
多线程处置惩罚:无法直接用try-catch包裹
在C#项目中处置惩罚多线程时的异常必要特殊注意,由于每个线程都可能在实行过程中遇到错误,而这些错误如果不被适当处置惩罚,可能会导致程序的不稳定或数据损坏。以下是一些在C#项目中处置惩罚多线程异常的策略和技巧:
1. 使用try-catch块

最直接的方法是在每个线程的实行体(比方,在Thread的Start方法中指定的ThreadStart委托,或在Task的Run方法中)使用try-catch块来捕捉并处置惩罚异常。
  1. Thread thread = new Thread(() =>  
  2. {  
  3.     try  
  4.     {  
  5.         // 线程执行的代码  
  6.         throw new InvalidOperationException("这是一个异常");  
  7.     }  
  8.     catch (Exception ex)  
  9.     {  
  10.         // 处理异常  
  11.         Console.WriteLine($"线程异常: {ex.Message}");  
  12.     }  
  13. });  
  14.   
  15. thread.Start();
复制代码
对于使用Task的情况,异常处置惩罚稍微复杂一些,由于Task的异常不会直接流传到创建它的线程中,而是必要通过Task.Wait、Task.Result(这会壅闭调用线程并抛出异常)或Task.ContinueWith(无论任务成功完成还是抛出异常都会实行)来捕捉。
2. 使用Task的异常处置惩罚

当使用Task时,通常推荐的方式是调用Task.ContinueWith来处置惩罚异常,或者使用async和await(这将主动流传异常)。
  1. Task.Run(() =>  
  2. {  
  3.     // 可能会抛出异常的代码  
  4.     throw new InvalidOperationException("Task中的异常");  
  5. })  
  6. .ContinueWith(task =>  
  7. {  
  8.     if (task.IsFaulted)  
  9.     {  
  10.         AggregateException ae = task.Exception;  
  11.         foreach (var ex in ae.InnerExceptions)  
  12.         {  
  13.             Console.WriteLine($"Task异常: {ex.Message}");  
  14.         }  
  15.     }  
  16. }, TaskContinuationOptions.OnlyOnFaulted);
复制代码
使用async和await时,异常会主动流传到await表达式之后的代码中。
  1. async Task DoWorkAsync()  
  2. {  
  3.     try  
  4.     {  
  5.         await Task.Run(() =>  
  6.         {  
  7.             // 可能会抛出异常的代码  
  8.             throw new InvalidOperationException("Task中的异步异常");  
  9.         });  
  10.     }  
  11.     catch (Exception ex)  
  12.     {  
  13.         Console.WriteLine($"捕获到异步异常: {ex.Message}");  
  14.     }  
  15. }
复制代码

6.2单线程与多线程的异常处置惩罚


单线程异常处置惩罚代码:
  1. //单线程异常处理
  2. private void button1_Click(object sender, EventArgs e)
  3.         {
  4.             try
  5.             {
  6.                 for (int i = 0; i < 20; i++)
  7.                 {
  8.                     string k = $"button2_Click{i}";
  9.                     if (k.Equals("button2_Click8"))
  10.                     {
  11.                         throw new Exception("k==button2_Click8异常");
  12.                     }
  13.                     else if (k.Equals("button2_Click10"))
  14.                     {
  15.                         throw new Exception("k==button2_Click10异常");
  16.                     }
  17.                     else if (k.Equals("button2_Click15"))
  18.                     {
  19.                         throw new Exception("k==button2_Click15异常");
  20.                     }
  21.                 }
  22.             }
  23.             catch
  24.             {
  25.                 throw;
  26.             }
  27.         }
复制代码
点击单线程异常处置惩罚按钮后:

多线程异常处置惩罚代码:
  1. //多线程异常处理
  2. private void button2_Click(object sender, EventArgs e)
  3.         {
  4.             try
  5.             {
  6.                 for (int i = 0; i < 20; i++)
  7.                 {
  8.                     string k = $"button2_Click{i}";
  9.                     Task.Run(() =>
  10.                     {
  11.                         if (k.Equals("button2_Click2"))
  12.                         {
  13.                             throw new Exception("k==button2_Click8异常");
  14.                         }
  15.                         else if (k.Equals("button2_Click10"))
  16.                         {
  17.                             throw new Exception("k==button2_Click10异常");
  18.                         }
  19.                         else if (k.Equals("button2_Click15"))
  20.                         {
  21.                             throw new Exception("k==button2_Click15异常");
  22.                         }
  23.                     });
  24.                 }
  25.             }
  26.             catch
  27.             {
  28.                 throw;
  29.             }
  30.         }
复制代码
点击多线程异常处置惩罚按钮:


6.3多线程异常捕捉与处置惩罚

那么多线程内部发生的异常怎样捕捉?需实现以下2大条件
1.必要做线程的等待,比方task.waitall,即壅闭主线程
2.try-catch包裹
将上述多线程的代码改写成如下形式:
  1. private void button2_Click(object sender, EventArgs e)
  2.         {
  3.             try
  4.             {
  5.                 List<Task> list1 = new List<Task>();
  6.                 for (int i = 0; i < 20; i++)
  7.                 {
  8.                     string k = $"button2_Click{i}";
  9.                     //线程列表
  10.                     list1.Add(Task.Run(() =>
  11.                     {
  12.                         if (k.Equals("button2_Click2"))
  13.                         {
  14.                             throw new Exception("k==button2_Click8异常");
  15.                         }
  16.                         else if (k.Equals("button2_Click10"))
  17.                         {
  18.                             throw new Exception("k==button2_Click10异常");
  19.                         }
  20.                         else if (k.Equals("button2_Click15"))
  21.                         {
  22.                             throw new Exception("k==button2_Click15异常");
  23.                         }
  24.                     }));
  25.                 }
  26.                 Task.WaitAll(list1.ToArray());                //等待线程完成
  27.             }
  28.             catch(Exception ex)
  29.             {
  30.                 throw;
  31.             }
  32.         }
复制代码
点击多线程异常处置惩罚按钮:

上述例子中多线程出现3个异常,只用了一个“try-catch”结构,会合显示异常情况,对于异常检察并不方便,解决上述问题,接纳如下方式。
1.一个try可以对应多个catch
2.AggregateException捕捉异常,多线程特有异常捕捉,AggregateException继承至Exception,Exception子类,AggregateException内部包罗多线程集合


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

锦通

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表