異步編程指南

異步編程具有傳染性

一旦採用異步編程模型,所有調用者應該也是異步的。因爲只有整個調用鏈都採用異步編程模型,才能充分發揮異步編程的優勢。在很多情況下,部分異步的效果甚至不如完全同步。因此,最好一次性將所有內容都改成異步編程模型。

❌ BAD 這個例子使用了Task.Result,導致當前線程被阻塞以等待結果。.

public int DoSomethingAsync()
{
    var result = CallDependencyAsync().Result;
    return result + 1;
}

✅ GOOD 這個例子使用了await關鍵字來等待調用CallDependencyAsync方法的結果。

public async Task<int> DoSomethingAsync()
{
    var result = await CallDependencyAsync();
    return result + 1;
}

Async void

在ASP.NET Core應用程序中使用async void永遠都是不好的,應該儘量避免。通常情況下,開發者會在控制器操作觸發請求後立即返回,不需要等待響應 時使用Async Void方法。但是,如果出現異常,則會導致整個進程崩潰。

❌ BAD Async Void方法無法被追蹤,因此未處理的異常可能會導致應用程序崩潰。

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        BackgroundOperationAsync();
        return Accepted();
    }
    
    public async void BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
} 

✅ GOOD 使用返回任務(Task)的方法更好,因爲未處理的異常會觸發TaskScheduler.UnobservedTaskException事件。

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        Task.Run(BackgroundOperationAsync);
        return Accepted();
    }
    
    public async Task BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
}
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
    // 記錄未處理的任務異常
    foreach (var exception in args.Exception.InnerExceptions)
    {
        logger.LogError(exception, "Unobserved task exception occurred.");
    }
   
    // 標記異常已處理
    args.SetObserved();
};

 

對於預先計算或者計算非常簡單的數據,應優先考慮使用Task.FromResult而不是Task.Run。

Task.FromResult方法是一個靜態方法,用於創建一個已經完成的Task對象,並將結果作爲返回值封裝在Task中。由於Task已經完成,因此當等待Task對象時,將立即返回結果,而不需要開啓新的線程。

相比之下,Task.Run方法會在ThreadPool上啓動一個新的任務,並且該任務的執行需要時間和資源。對於預先計算或者計算非常簡單的數據,使用Task.Run來啓動這樣的任務可能是浪費資源的。

❌ BAD 這個示例浪費了一個線程池線程來返回一個計算非常簡單的值。

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.Run(() => a + b);
   }
} 

✅ GOOD 這個示例使用Task.FromResult來返回一個計算非常簡單的值。由於不需要額外的線程,因此它不會佔用任何多餘的系統資源。

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.FromResult(a + b);
   }
}

 ✅ GOOD在上個示例中,我們使用Task.FromResult創建一個Task<int>對象來返回計算結果,但是這會分配一個額外的對象(Task)。現在,我們可以使用ValueTask<int>來改善這個方法。在這個示例中,我們返回了一個ValueTask<int>對象,它不需要分配任何Task對象。 此外,當這個方法被同步調用時,它也可以提高性能,因爲它不需要等待Task對象被調度和執行,它可以直接返回封裝在ValueTask<int>中的結果。這個改進對於高性能應用程序非常有用。

public class MyLibrary
{
   public ValueTask<int> AddAsync(int a, int b)
   {
       return new ValueTask<int>(a + b);
   }
}

 

 在執行需要佔用很長時間的工作時,儘可能避免使用Task.Run方法。

 Task.Run方法是用於將一個操作分配到線程池上的異步方法,它通常用於在後臺線程上執行短時間運行的非阻塞操作。但是,如果您需要執行一個需要長時間運行的操作,而且該操作會阻塞線程,則使用Task.Run可能會導致一些問題。這是因爲它會佔用線程池中有限的線程資資源,從而影響應用程序的響應性能。

如果阻塞線程,線程池會增長,但是這是一種不好的編程實踐。

Task.Factory.StartNew 方法是一個強大的工具,可用於在多線程應用程序中以異步方式執行代碼。TaskCreationOptions.LongRunning 選項可以指示方法使用一個長時間運行的線程來執行任務,從而避免佔用線程池中的寶貴資源。但是,要正確使用此選項,需要考慮多種參數和配置。

不要在異步代碼中使用TaskCreationOptions.LongRunning選項,因爲這樣會創建一個新的線程,在第一次 await 後就會被銷燬。

 

應避免使用 Task.Result 和 Task.Wait。

在異步編程中,Task.Result 和 Task.Wait 方法可以用於等待任務完成,並返回其結果。但是,這種做法可能會導致應用程序死鎖,因爲它會阻塞當前線程並等待任務完成,而另一個任務或系統資源可能正在等待該線程釋放。

相反,應該優先使用 await 操作符來等待任務完成。await 操作符可以暫停當前方法的執行並允許其他代碼在該方法的上下文中運行,從而提高應用程序的響應性和併發性。此外,await 操作符也可以將異常傳播回調用者,以便更好地處理錯誤情況。

如果確實需要等待任務完成而無法使用 await 操作符,則應儘量避免在 UI 線程或 ASP.NET 應用程序中使用 Task.Wait 或 Task.Result 方法。這些方法可能會導致應用程序出現死鎖、線程池飽和或性能下降的問題。取而代之,可以考慮使用 Task.ConfigureAwait(false) 將等待操作切換到後臺線程,並指定 CancellationToken 以避免無限期地等待任務完成。

總之,在異步編程中,應該優先使用 await 操作符來等待任務完成,並避免使用 Task.Result 和 Task.Wait 方法,以提高應用程序的可靠性和性能。

❌ BAD

public string DoOperationBlocking()
{
    // Bad - Blocking the thread that enters.
    // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
    // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
    return Task.Run(() => DoAsyncOperation()).Result;
}

public string DoOperationBlocking2()
{
    // Bad - Blocking the thread that enters.
    // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
    // In the case of an exception, this method will throw the exception without wrapping it in an AggregateException.
    return Task.Run(() => DoAsyncOperation()).GetAwaiter().GetResult();
}

public string DoOperationBlocking3()
{
    // Bad - Blocking the thread that enters, and blocking the threadpool thread inside.
    // In the case of an exception, this method will throw an AggregateException containing another AggregateException, containing the original exception.
    return Task.Run(() => DoAsyncOperation().Result).Result;
}

public string DoOperationBlocking4()
{
    // Bad - Blocking the thread that enters, and blocking the threadpool thread inside.
    return Task.Run(() => DoAsyncOperation().GetAwaiter().GetResult()).GetAwaiter().GetResult();
}

public string DoOperationBlocking5()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
    return DoAsyncOperation().Result;
}

public string DoOperationBlocking6()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    return DoAsyncOperation().GetAwaiter().GetResult();
}

public string DoOperationBlocking7()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    var task = DoAsyncOperation();
    task.Wait();
    return task.GetAwaiter().GetResult();
}

 

在使用超時的 CancellationTokenSource 時,應該始終在使用後將其釋放(Dispose),以避免資源泄露。

using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
    // perform long-running operation here
    // and check for cancellation if possible:
    while (!cts.Token.IsCancellationRequested)
    {
        // do some work here...
    }
}

在使用 CancellationToken 來取消操作時,應該始終將令牌傳遞給 API,以確保可以正確地取消操作。

CancellationToken 是用於取消操作的一個標準機制。當操作正在執行時,如果取消令牌被請求,則可以使用 IsCancellationRequested 屬性檢查令牌是否已被取消,並相應地停止該操作。

許多 .NET 標準庫中的方法和 API 都支持 CancellationToken 參數,以便在取消操作時使用。如果不傳遞 CancellationToken 到這些 API,則可能會導致無法正確取消操作,從而影響應用程序的性能。

using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync("https://example.com", cts.Token);
        // process the response here...
    }
}

 

 

 

 

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章