異步方法和TPL: async / await / Parallel

封裝

咱們要把上面這個Task封裝成方法,怎麼辦?api

最重要的一點,這個方法要能返回生成的random,後面的代碼要用!數組

public static Task<int> getRandom() { return Task<int>.Run(() => { Thread.Sleep(500); //模擬耗時 return new Random().Next(); }); }安全

@想想@:應該如何調用這個方法?(_提示:不要直接__getRandom().Result_)架構

假如咱們還須要進一步的封裝,添加一個方法Process,裏面調用getRandom()並把其結果輸出:併發

public static void Process() { Task<int> task = getRandom(); Console.WriteLine(task.Result); }dom

故技重施,好像不行了,此次……異步

@想想@:再讓Process()返回Task行不行?一個Task套另外一Task會出現什麼狀況?async

在getRandom()和Process()中展現線程Id看一看:ide

Console.WriteLine("in getRandom() with Thread-" + Thread.CurrentThread.ManagedThreadId);性能

在.NET core的I/O類庫中,咱們會發現這樣的方法:

public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, CancellationToken cancellationToken = default); public static Task<byte[]> ReadAllBytesAsync(string path, CancellationToken cancellationToken = default);

注意:

  • 方法名被添加了Async後綴(推薦命名規範)
  • 方法的返回類型爲Task或Task<T>

異步方法

.NET爲咱們提供了簡潔優雅的異步方法,只須要兩個關鍵字:

async 和 await

被async標記的方法被稱爲異步方法,

  • 可是,async不必定(沒有強制力保證)異步。同步的方法同樣能夠標記async。async的做用只是:
  • 告訴編譯器方法中能夠出現await。若是隻有async沒有await,報編譯時警告

只有await沒有async,報編譯錯誤。

static async void Process() { int random = await getRandom(); Console.WriteLine(random); }

await,能夠理解爲:異步(async)等待,後接 awaitable 實例。

咱們能夠簡單的把awaitable理解成Task。

非阻塞等待

異步方法一直同步運行,直到 await。

從 await 開始異步(分叉):

  • 執行 awatable 中的內容,同時
  • 返回方法的調用處,執行其後內容

直到 awaitable 中內容執行完畢,才暫停方法調用處內容,繼續執行await以後的代碼。

異步方法執行完畢,繼續方法調用處內容。

以上述代碼爲例:

//33 --> 44 --> 45 --> 46 --> 47 --> 調用異步方法處 // +--> 52 --> 57-- + 被調用異步方法 // +--> 35 --> 38 +--> 39 awaitable

注意:若是52行以前還有普通(非異步)代碼,這些代碼不會被異步執行。

await不像Wait()或Result同樣,

開始(但不是當即或同步的)
async和await會不會開啓一個新的任務(或者線程)?不會。

異步方法分爲兩種:

返回 void 或 Task

public static async void Getup() { Console.WriteLine($"before await-1 with thread {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"before await-2 with thread {Thread.CurrentThread.ManagedThreadId}"); //await 以前的代碼,在主線程上運行 // await Task.Run(()=> { Console.WriteLine($"in await with thread {Thread.CurrentThread.ManagedThreadId}"); }); // Console.WriteLine($"after await-3 with thread {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"after await-4 with thread {Thread.CurrentThread.ManagedThreadId}"); }

從await開始,代碼開始分叉(只是異步,不必定新開線程):

  • 一邊執行await後的表達式(Task)
  • 一邊返回到方法調用者處繼續執行

直到await後的Task執行完畢,纔會返回async方法,繼續執行其await(非阻塞)以後的剩餘代碼。

Console.WriteLine($"before async-1 with thread {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"before async-2 with thread {Thread.CurrentThread.ManagedThreadId}"); Getup(); for (int i = 0; i < 10; i++) { //Getup()裏await部分的運行,會打亂這裏代碼的同步運行 Console.WriteLine($"after async-{3 + i} with thread {Thread.CurrentThread.ManagedThreadId}"); }

實質上,await採用的是Task的ContinueWith()機制:await 以後的方法內代碼,被 await Task 執行完畢後調用。

對比演示:

  • 非異步方法:只有Task異步執行
  • 調用Wait()的非異步方法:Wait()會阻塞當前線程進行等待

異步方法中的 void 能夠被直接替換成 Task(推薦),以便於該方法進一步的被 await 傳遞。

void一般作爲頂級(top-level)方法使用。

思考:當async方法中拋出異常,void方法和Task方法的區別?

返回Task<T>

返回值被Task包裹,寫成Task<T>,T指方法體內聲明返回的類型

//方法的聲明:返回的是Task<int> public static async Task<int> Getup() { int result = await Task<int>.Run(() => { Thread.Sleep(500); Console.WriteLine($"at await in Getup() with thread {Thread.CurrentThread.ManagedThreadId}"); return new Random().Next(); }); //方法體內,返回的是int return result; }

特別注意:不能直接Getup().Result 或 await Getup()取值,不然……

思考:和直接返回Task<T>的區別?

任務並行庫Task Parallel Library

.NET中System.Threading 和System.Threading.Tasks名稱空間下的類庫

簡化異步/並行開發,在底層實現:

  • 動態調整並行規模
  • 處理分區
  • 線程(池)調度(器)等……

於Task的並行

最簡單的例子,Parallel.Invoke():

for (int i = 0; i < 5; i++) { Console.WriteLine(); Parallel.Invoke( () => { Console.WriteLine(i + $":task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} begin in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} end in thread-{Thread.CurrentThread.ManagedThreadId}"); }, () => { Console.WriteLine(i + $":task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in begin in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"task-{Task.CurrentId} in end in thread-{Thread.CurrentThread.ManagedThreadId}"); } ); }

其餘方法:

  • For循環

    Parallel.For(0, 10, x => { Console.WriteLine(x); });

  • ForEach

    Parallel.ForEach(Enumerable.Range(1,10), x => Console.WriteLine(x));

引入線程數組:Task[]

  • WaitAll / WaitAny:
  • WhenAll / WhenAny:

對比如下代碼,體會 await 的 continuation:

public static async Task Getup() { //await Task.Run(() => { Console.WriteLine("洗臉"); }); //await Task.Run(() => { Console.WriteLine("刷牙"); }); //await Task.Run(() => { Console.WriteLine("吃早餐"); }); //await Task.Run(() => { Console.WriteLine("背單詞"); }); Task[] tasks = { Task.Run(() => { Console.WriteLine("洗臉"); }), Task.Run(() => { Console.WriteLine("刷牙"); }), Task.Run(() => { Console.WriteLine("吃早餐"); }), Task.Run(() => { Console.WriteLine("背單詞"); }) }; await Task.WhenAll(tasks); }

補充:

Delay()

FromResult()

AsyncState

並行Linq Parallel LINQ (PLINQ)

僅適用於Linq to Object,主要的措施是:對數據源進行分區,而後多核併發運行(保守模式:若是能不併發就不併發)

核心方法:AsParallel(),在數據源後添加。

try { IEnumerable<int> numbers = Enumerable.Range(0, 1000); var filtered = numbers.AsParallel() //.Where(n => n % 11 == 0) .Where(n => 8 % (n > 100 ? n : 0) == 0) ; filtered.ForAll(f => Console.WriteLine(f)); } catch (AggregateException ae) { ae.Handle(e => { Console.WriteLine(e); return true; }); }

ForAll():一樣能夠併發執行

仍然是AggregateException異常

最佳實踐

使用異步/並行的反作用(side effect):

  1. 增長代碼的複雜性(尤爲是bug調試)
  2. 異步/並行的切換須要消耗額外的資源

簡單理解:

  • 鎖、死鎖(Deadlock)、資源爭奪(race condition)
  • 線程安全 (Thread Safty)
  • 天下沒有免費的午飯
  • 越是複雜精巧的東西越不「耐操」(健壯性robust)

老是最後考慮異步/並行:(我的建議)

  • 老是在最後考慮異步/併發(尤爲是B/S架構)
  • 肯定性能瓶頸
  • 肯定該瓶頸能夠經過異步/並行的方法解決
相關文章
相關標籤/搜索