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);
            }

        }    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM