.Net非同步程式設計進階之(一)
在今天來看,非同步程式設計已經不是什麼新鮮玩意了。但從過去程式設計的方式來看在 .Net 中想要使用非同步並不是一件容易的事情。但在C# 5.0引入了 async 和 await 關鍵字之後,非同步程式設計已經成為了主流。在現代的框架上,比如 .Net Core 上則是完全非同步的。在寫Web服務的時候往往很難避免使用 async 。因此很多開發人員對非同步的最佳實現和如何正確使用非同步存在很多困擾。本節我將要列出一些正確和錯誤的例項,以便讓大家在日後的非同步程式設計中更好的規避錯誤的用法。
非同步具有傳染性
非同步是有傳染性的,一旦你一個函式使用了非同步,那麼接下來所有呼叫這個函式的函式也都 應該 使用非同步。因為除非整個呼叫棧都是非同步的,否則這個非同步則是沒有任何意義。在很多情況下,如果中途堵塞程序強制把非同步轉為同步,甚至比完全同步更糟糕,所以最好是把整個呼叫棧都做成非同步。
使用 Task.Result 阻塞當前執行緒等待非同步結果,這是將非同步強制轉為同步。
1 public int AppendValueAsync() 2 { 3int result = GetValueAsync().Result; 4return result + 1; 5 }
Line"/> 使用 await 關鍵字獲取值
1 public async Task<int> AppendValueAsync() 2 { 3int result = await GetValueAsync(); 4return result + 1; 5 }
避免讓非同步函式使用void作為返回型別
在 Asp.Net Core下使用 void 作為非同步返回型別,是種壞習慣。 儘量避免這樣做 ,因為如果呼叫的函式一旦丟擲異常,並且開發人員沒有配置其他異常呈報策略,在預設的情況下,會終止程序。
無法追蹤的 async void 函式,未經處理的異常會直接讓程式崩潰。
1 public class MyController : Controller 2 { 3[HttpPost("/start")] 4public IActionResult Post() 5{ 6BackgroundOperationAsync(); 7return Accepted(); 8} 9 10private async void BackgroundOperationAsync() 11{ 12var result = await CallDependencyAsync(); 13DoSomething(result); 14} 15 }
使用 Task 型別返回值,未經處理的異常會觸發 ofollow,noindex" target="_blank">TaskScheduler.UnobservedTaskException
1 public class MyController : Controller 2 { 3[HttpPost("/start")] 4public IActionResult Post() 5{ 6Task.Run(BackgroundOperationAsync); 7return Accepted(); 8} 9 10private async Task BackgroundOperationAsync() 11{ 12var result = await CallDependencyAsync(); 13DoSomething(result); 14} 15 }
預計算簡單運算的結果
對於一些簡單的計算,不應該使用 Task.Run ,因為這需要建立一個非同步任務放到執行緒池的佇列上等待執行然後返回運算結果。相反,使用 Task.FromResult 則是建立一個已經完成計算已經有結果的任務。
這個是例子浪費了執行緒池裡面的一個執行緒來計算一個簡單計算的值。
1 public class Calculator 2 { 3public Task<int> AddAsync(int x, int y) 4{ 5return Task.Run(() => x + y); 6} 7 }
使用 Task.FromResult 去返回一些簡單計算的值,這樣不會使用額外的執行緒,而且這樣也不需要在CLR託管的堆上額外申請記憶體空間存放非同步任務的物件。
1 public class Calculator 2 { 3public Task<int> AddAsync(int x, int y) 4{ 5return Task.FromResult(x + y); 6} 7 }
注意:使用 Task.FromResult 雖然不會額外佔用執行緒,但依然會建立一個非同步任務,使用ValueTask<T>則完全可以避免建立這個多餘的任務。
使用 ValueTask<T> 返回簡單運算結果,不但不會額外佔用執行緒,也不會在CLR的堆上額外分配空間建立多餘的任務物件。
1 public class Calculator 2 { 3public ValueTask<int> AddAsync(int a, int b) 4{ 5return new ValueTask<int>(a + b); 6} 7 }
避免使用Task.Run來執行需要長時間執行的阻塞工作
這裡說的長時間工作是指貫穿應用程式整個生命週期後臺執行的工作(如休眠等待一定時間再次喚醒處理資料,處理訊息佇列裡的資料等等)。 Task.Run 會把任務入列執行緒池,如果這個工作是能夠快完成又或者說在合理的時間內完成的話,這個執行緒則會得到複用。但如果這是一個長時間執行的阻塞工作,則會一直佔用該執行緒。因此需要手動分配一個新的執行緒去執行需要長時間執行的阻塞工作。
注意:如果阻塞了執行緒池裡的執行緒,執行緒池裡會增加大量的執行緒,導致大量的上下文切換,從而拖慢應用程式的整體效能。
注意: Task.Factor.StartNew 有一個選項 TaskCreatetionOptions.LongRunning 它會在後臺建立一個新的執行緒去執行長時間執行的任務。
這個例子,永遠佔用著執行緒池裡的一個執行緒去處理訊息佇列
1 public class QueueProcessor 2 { 3private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>(); 4 5public void StartProcessing() 6{ 7Task.Run(ProcessQueue); 8} 9 10public void Enqueue(Message message) 11{ 12_messageQueue.Add(message); 13} 14 15private void ProcessQueue() 16{ 17foreach (var item in _messageQueue.GetConsumingEnumerable()) 18{ 19ProcessItem(item); 20} 21} 22 23private void ProcessItem(Message message) { } 24 }
使用專用的執行緒來處理佇列,而不是執行緒池裡的執行緒。
1 public class QueueProcessor 2 { 3private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>(); 4 5public void StartProcessing() 6{ 7var thread = new Thread(ProcessQueue) 8{ 9// 這選項很重要,CLR 會在程序終止的時候對每一個活在後臺的執行緒呼叫Abort,來徹底終止應用程式 10IsBackground = true 11}; 12thread.Start(); 13} 14 15public void Enqueue(Message message) 16{ 17_messageQueue.Add(message); 18} 19 20private void ProcessQueue() 21{ 22foreach (var item in _messageQueue.GetConsumingEnumerable()) 23{ 24ProcessItem(item); 25} 26} 27 28private void ProcessItem(Message message) { } 29 }