C# Task详解
C# Task详解
推荐几篇写的很好的文章,本文部分转自
https://blog.csdn.net/btfireknight/article/details/97766193
https://blog.csdn.net/boonya/article/details/80541571
https://blog.csdn.net/nacl025/article/details/9163495/
1. Task 原理
这里简要的分析下CLR线程池,其实线程池中有一个叫做“全局队列”的概念,每一次我们使用QueueUserWorkItem的使用都会产生一个“工作项”,然后“工作项”进入“全局队列”进行排队,最后线程池中的的工作线程以FIFO(First Input First Output)的形式取出,这里值得一提的是在.net 4.0之后“全局队列”采用了无锁算法,相比以前版本锁定“全局队列”带来的性能瓶颈有了很大的改观。那么任务委托的线程池不光有“全局队列”,而且每一个工作线程都有”局部队列“。我们的第一反应肯定就是“局部队列“有什么好处呢?这里暂且不说,我们先来看一下线程池中的任务分配,如下图:
线程池的工作方式大致如下,线程池的最小线程数是6,线程1~3正在执行任务1~3,当有新的任务时,就会向线程池请求新的线程,线程池会将空闲线程分配出去,当线程不足时,线程池就会创建新的线程来执行任务,直到线程池达到最大线程数(线程池满)。总的来说,只有有任务就会分配一个线程去执行,当FIFO十分频繁时,会造成很大的线程管理开销。
下面我们来看一下task中是怎么做的,当我们new一个task的时候“工作项”就会进去”全局队列”,如果我们的task执行的非常快,那么“全局队列“就会FIFO的非常频繁,那么有什么办法缓解呢?当我们的task在嵌套的场景下,“局部队列”就要产生效果了,比如我们一个task里面有3个task,那么这3个task就会存在于“局部队列”中,如下图的任务一,里面有三个任务要执行,也就是产生了所谓的"局部队列",当任务三的线程执行完成时,就会从任务一种的队列中以FIFO的形式"窃取"任务执行,从而减少了线程管理的开销。这就相当于,有两个人,一个人干完了分配给自己的所有活,而另一个人却还有很多的活,闲的人应该接手点忙的人的活,一起快速完成。
从上面种种情况我们看到,这些分流和负载都是普通ThreadPool.QueueUserWorkItem所不能办到的,所以说在.net 4.0之后,我们尽可能的使用TPL,抛弃ThreadPool
2. Task细节
Task的属性IsCompleted, IsCanceled表示它是否完成和是否取消
具体的property参考官方API: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=netcore-3.1
Async: 当一个方法由async关键字标识,表明这个方法是异步方法,当它被调用时,会创建一个线程来执行
Async 只能修饰void,Task,Task<>
(1) Task创建
static void Main(string[] args) { //1.new方式实例化一个Task,需要通过Start方法启动 Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //2.Task.Factory.StartNew(Action action)创建和启动一个Task Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100); Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); //3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task Task task3 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("执行主线程!"); Console.ReadKey(); }
(2) Task的取消以及取消回调方法
Task中有一个专门的类 CancellationTokenSource 来取消任务执行,CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000)
实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action)
注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); //注册任务取消的事件 source.Token.Register(() => { Console.WriteLine("任务被取消后执行xx操作!"); }); int index = 0; //开启一个task执行任务 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次执行,线程运行中..."); } }); task1.Start(); //延时取消,效果等同于Thread.Sleep(5000);source.Cancel(); source.CancelAfter(5000); Console.ReadKey(); }
查看结果
(3) 实例分析
static void Main(string[] args) { Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); AsyncMethod(); SyncMethod(); Thread.Sleep(10000); Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); } private static async Task AsyncMethod() { Console.WriteLine("Helo I am AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); var ResultFromTimeConsumingMethod = TimeConsumingMethod(); string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId; Console.WriteLine(Result); //返回值是Task的函数可以不用return } private static Task SyncMethod() { var task = Task.Run(() => { Console.WriteLine("Helo I am SyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); Console.WriteLine("Helo I am SyncMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); }); return task; } //这个函数就是一个耗时函数,可能是IO操作,也可能是cpu密集型工作。 private static Task<string> TimeConsumingMethod() { var task = Task.Run(() => { Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); return "Hello I am TimeConsumingMethod"; }); return task; }
执行结果如下:
Main 函数里面增加Thread.Sleep(10000)是防止主线程结束,一旦主线程结束了,那么其他线程也释放了。
如图可以看出这几个关键字的真正含义
1. 当执行返回参数为Task或者Task<>类型的函数时,假如该函数没有用async标识,那么开启线程执行开方法
2. 当有async标识时,当前线程会把该方法当成同步函数执行,直到运行到await关键字的地方,开启新线程(此时假如中途执行另一个Task标识的方法,不管该方法是不是async,都会同步执行,不会开启新线程, 但是加入把一个task得方法放到变量中,会开启新的线程,这里非常重要。看如下代码)
public async Task Test() { await xxx; // 这里会在当前task得线程中执行RunOtherTask方法,并不会开启新的task RunOtherTask(); // 这里主线程会继续执行下面得代码,开启一个新的线程执行RunOtherTask _ = RunOtherTask(); await otherLogic } private Task RunOtherTask() { return Task.Run(() => { for (var i = 0; i < 100000; i++) { XXX } }); }
3. await关键字表示会开辟新线程来执行后面的方法,但是该线程会等待新线程执行完返回,然后继续执行
函数的执行途中是根据await关键字来判断是否需要开辟线程来执行代码(Async void方法调用时不能加await,所以它必定是在主线程中被调用),假如被调用的method前面有await,那么这个method必须包含async关键字,假如一个async标识的方法里面没有await,那么这个方法会被当成同步方法来调用
3. Task关键点
Async void 主要用于异步事件处理方法,其他时候请不要使用,在async void方法中,一定要加try catch来捕捉异常。
Async void 方法具有不同的错误处理语义。 当 async Task 或 async Task<T> 方法引发异常时,会捕获该异常并将其置于 Task 对象上。 对于 async void 方法,没有 Task 对象,因此 async void 方法引发的任何异常都会直接在 SynchronizationContext(在 async void 方法启动时处于活动状态)上引发。 无法捕获从 async void 方法引发的异常。所以对于Async void方法必须加入try/catch。
Async void 方法具有不同的组合语义。 返回 Task 或 Task<T> 的 async 方法可以使用 await、Task.WhenAny、Task.WhenAll 等方便地组合而成。 返回 void 的 async 方法未提供一种简单方式,用于向调用代码通知它们已完成。 启动几个 async void 方法不难,但是确定它们何时结束却不易。 Async void 方法会在启动和结束时通知 SynchronizationContext,但是对于常规应用程序代码而言,自定义 SynchronizationContext 是一种复杂的解决方案。
Async void 方法难以测试。 由于错误处理和组合方面的差异,因此调用 async void 方法的单元测试不易编写。 MSTest 异步测试支持仅适用于返回 Task 或 Task<T> 的 async 方法。 可以安装 SynchronizationContext 来检测所有 async void 方法都已完成的时间并收集所有异常,不过只需使 async void 方法改为返回 Task,这会简单得多。推荐使用下面方法实现
private async void button1_Click(object sender, EventArgs e) { await Button1ClickAsync(); } public async Task Button1ClickAsync() { // Do asynchronous work. await Task.Delay(1000); }
应避免混合使用异步代码和阻塞代码。 混合异步代码和阻塞代码可能会导致死锁、更复杂的错误处理及上下文线程的意外阻塞,推荐除了main方法外都使用async方法,不要再异步代码使用Task.Result和Task.Wait。并且推荐使用ConfigureAwait(false)。
还没有完全理解内部的原理,请看下面的链接
https://blog.csdn.net/WPwalter/article/details/79673214
http://blog.walterlv.com/post/deadlock-in-task-wait.html
4. async和Lambda
async Action == async void
async Func<string> == async Task<string>
当一个Action或者Func的类型是async void,并且作为参数传递到另一个方法中,当执行另一个方法时,并不能等待Action执行完再继续
看代码
public Task ExecuteAction(Action action) { Console.WriteLine("In ExecuteAction = " + Thread.CurrentThread.ManagedThreadId); action(); Console.WriteLine("In ExecuteAction = " + Thread.CurrentThread.ManagedThreadId); TestAsync(); Console.WriteLine("In ExecuteAction = " + Thread.CurrentThread.ManagedThreadId); return Task.CompletedTask; } public async Task ExecuteAwaitAction(Action action) { Console.WriteLine("In ExecuteAwaitAction = " + Thread.CurrentThread.ManagedThreadId); await Task.Run(action); await TestAsync(); } private static void Main(string[] args) { try { Console.WriteLine("In main = " + Thread.CurrentThread.ManagedThreadId); Test(); } catch (Exception e) { Console.WriteLine(e); throw; } Console.ReadKey(); } private async Task TestAsync() { Console.WriteLine("In Delay = " + Thread.CurrentThread.ManagedThreadId); await Task.Delay(3000); Console.WriteLine("In Delay = " + Thread.CurrentThread.ManagedThreadId); } public static async void Test() { Console.WriteLine("In Test = " + Thread.CurrentThread.ManagedThreadId); IActionTest actionTest = new ActionTest(); await actionTest.ExecuteAction( TestAwait); //await actionTest.ExecuteAwaitAction( () => //{ // TestAwait(); //}); var a = 1; } public static async void TestAwait() { Console.WriteLine("In TestAwait = " + Thread.CurrentThread.ManagedThreadId); await Testsss(); int a = 3; a++; } public static async Task Testsss() { Console.WriteLine("In Testsss = " + Thread.CurrentThread.ManagedThreadId); await Task.Run(() => { Console.WriteLine("In lambda = " + Thread.CurrentThread.ManagedThreadId); int ctr = 0; for (ctr = 0; ctr <= 1000000000; ctr++) { } Console.WriteLine("Finished {0} loop iterations", ctr); }); //下面的方法会将其当成void方法 //int ctr = 0; //for (ctr = 0; ctr <= 1000000000; ctr++) //{ } //Console.WriteLine("Finished {0} loop iterations", // ctr); }
最后的结果是不管是在ExecutAction还是ExecutAwaitAction里面,action方法都不会等待,会直接执行下面的test方法,因为action本身就是异步方法,而在实现ExecutAction不能实现await Action,所以会立即返回。写代码时要注意当需要使用Func的返回值时,这种形式是有问题的。
5. 判断Task超时的方法
用Task.Delay(ElapsedMilliseconds, _cancellationTokenSource.Token);而不用Task.Delay(ElapsedMilliseconds); 因为后者会卡住task固定的时常,但是用前者可以随时取消。
/// <summary> /// Gets another task which that the given task <paramref name="self"/> can be awaited with a <paramref name="timeout"/>. /// </summary> /// <param name="self">The task to be awaited.</param> /// <param name="timeout">The number of milliseconds to wait.</param> /// <returns> /// <c>true</c> if the <see cref="Task"/> completed execution within the allotted time; otherwise, <c>false</c>. /// </returns> public static async Task<bool> GetTaskWithTimeout(this Task self, int timeout) { var timeoutTask = Task.Delay(timeout); var finishedTask = await Task.WhenAny(self, timeoutTask); // If the returned task is the return ReferenceEquals(finishedTask, self); }
6. 使用CancellationTokenSource创建一个定时轮询的service, 本机测试的是每小时查询一次电压,假如过低就记录日志,并且只记录一次
private const int ElapsedMilliseconds = 3600000;private const int StopTaskTimeout = 2000; private bool _isBatteryLowShown; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();private Task _checkingStatusTask; /// <summary> /// Starts the service. /// </summary> public Task StartAsync() { _isBatteryLowShown = false; _checkingStatusTask = RunCheckBatteryStatusPeriodicTask(); return Task.CompletedTask; } /// <summary> /// Stops the service. /// </summary> public async Task StopAsync() { _cancellationTokenSource.Cancel(); await _checkingStatusTask.GetTaskWithTimeout(StopTaskTimeout); if (!_checkingStatusTask.IsCompleted) { _logger.Warning($"Failed to stop checking status task within {StopTaskTimeout} ms - stopping anyway."); } } private async Task RunCheckBatteryStatusPeriodicTask() { try { while (!_cancellationTokenSource.IsCancellationRequested) { var status = xxx(); if (status && !_isBatteryLowShown) { _isBatteryLowShown = true; _logger.Error("Battery is low."); } await Task.Delay(ElapsedMilliseconds, _cancellationTokenSource.Token); } } catch (Exception e) { _logger.ErrorEx(message: $"{nameof(_checkingStatusTask)} exception.", sourceType: nameof(BatteryMonitoringService), ex: e); } }