• 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);
                }
    
            }    
  • 相关阅读:
    Motion Estimation ,运动估计
    BT源代码学习心得(十三):客户端源代码分析(对等客户的连接建立及其握手协议) 转自CSDN:gushenghua的专栏
    BT源代码学习心得(十一):客户端源代码分析(存储管理) 转自CSDN:gushenghua的专栏
    BT源代码学习心得(六):跟踪服务器(Tracker)的代码分析(初始化) 转自CSDN:gushenghua的专栏
    BT源代码学习心得(十二):客户端源代码分析(从开始到连接建立阶段)
    BT源代码学习心得(七):跟踪服务器(Tracker)的代码分析(HTTP协议处理对象)
    BT源代码学习心得(十):客户端源代码分析(相关对象一览) 转自CSDN:gushenghua的专栏
    BT源代码学习心得(十四):客户端源代码分析(对等客户连接中的阻塞管理) 转自CSDN:gushenghua的专栏
    BT源代码学习心得(八):跟踪服务器(Tracker)的代码分析(用户请求的实际处理) 转自CSDN:gushenghua的专栏
    BT源代码学习心得(四):种子文件的生成 转自CSDN:gushenghua的专栏
  • 原文地址:https://www.cnblogs.com/xiaojidanbai/p/13224172.html
Copyright © 2020-2023  润新知