當前位置: 妍妍網 > 碼農

C#使用異步操作時的註意要點(轉譯)

2024-01-31碼農

異步操作時應註意的要點

  • 使用異步方法返回值應避免使用void

  • 對於預計算或者簡單計算的函式建議使用Task.FromResult代替Task.Run

  • 避免使用Task.Run()方法執行長時間堵塞執行緒的工作

  • 避免使用Task.Result和Task.Wait()來堵塞執行緒

  • 建議使用await來代替continueWith任務

  • 建立TaskCompletionSource 時建議使用TaskCreationOptions.RunContinuationsAsynchronously內容

  • 建議使用CancellationTokenSource(s)進行超時管理時總是釋放(dispose)

  • 建議將協作式取消物件(CancellationToken)傳遞給所有使用到的API

  • 建議取消那些不會自動取消的操作(CancellationTokenRegistry,timer)

  • 使用StreamWriter(s)或Stream(s)時在Dispose之前建議先呼叫FlushAsync

  • 建議使用 async/await而不是直接返回Task

  • 使用場景

  • 使用定時器回呼函式

  • 建立回呼函式參數時註意避免 async void

  • 使用ConcurrentDictionary.GetOrAdd註意場景

  • 建構函式對於異步的問題

  • 異步操作時需要註意的要點

    1.使用異步方法返回值應當避免使用void

    在使用異步方法中最好不要使用 void 當做返回值,無返回值也應使用 Task 作為返回值,因為使用 void 作為返回值具有以下缺點

  • 無法得知異步函式的狀態機在什麽時候執行完畢

  • 如果異步函式中出現異常,則會導致行程崩潰

  • ❌異步函式不應該返回void

    staticvoidMain(string[] args)
    {
    try
    {
    // 如果Run方法無異常正常執行,那麽程式無法得知其狀態機什麽時候執行完畢
    Run();
    }
    catch (Exception ex)
    {
    Console.WriteLine(ex.Message);
    }
    Console.Read();
    }
    staticasyncvoidRun()
    {
    // 由於方法返回的為void,所以在呼叫此方法時無法捕捉異常,使得行程崩潰
    thrownew Exception("異常了");
    await Task.Run(() => { });
    }

    ☑️應該將異步函式返回 Task

    staticasync Task Main(string[] args)
    {
    try
    {
    // 因為在此進行await,所以主程式知道什麽時候狀態機執行完成
    await RunAsync();
    Console.Read();
    }
    catch (Exception ex)
    {
    Console.WriteLine(ex.Message);
    }
    }
    staticasync Task RunAsync()
    {
    // 因為此異步方法返回的為Task,所以此異常可以被捕捉
    thrownew Exception("異常了");
    await Task.Run(() => { });
    }

    :事件是一個例外,異步事件也是返回void

    2.對於預計算或者簡單計算的函式建議使用Task.FromResult代替Task.Run

    對於一些預先知道的結果或者只是一個簡單的計算函式,使用Task,FromResult要比Task.Run效能要好,因為Task.FromResult只是建立了一個包裝已計算任務的任務,而Task.Run會將一個工作項線上程池進行排隊,計算,返回.並且使用Task.FromResult在具有SynchronizationContext 程式中(例如WinForm)呼叫Result或wait()並不會死結(雖然並不建議這麽幹)

    ❌對於預計算或普通計算的函式不應該這麽寫

    publicasync Task<int> RunAsync()
    {
    returnawait Task.Run(()=>1+1);
    }

    ☑️而應該使用Task.FromResult代替

    publicasync Task<int> RunAsync()
    {
    returnawait Task.FromResult(1 + 1);
    }

    還有另外一種代替方法,那就是使用 ValueTask 型別, ValueTask 是一個可被等待異步結構,所以並不會在堆中分配記憶體和任務分配,從而效能更最佳化.

    ☑️使用ValueTask 代替

    staticasync Task Main(string[] args)
    {
    await AddAsync(1, 1);
    }
    static ValueTask<int> AddAsync(int a, int b)
    {
    // 返回一個可被等待的ValueTask型別
    returnnew ValueTask<int>(a + b);
    }

    : ValueTask 結構是C#7.0加入的,存在於 Sysntem,Threading.Task.Extensions 包中

    ValueTask 相關文章

    ValueTask 相關文章

    3.避免使用Task.Run()方法執行長時間堵塞執行緒的工作

    長時間執行的工作是指在應用程式生命周期執行後台工作的執行緒,如:執行 processing queue items ,執行 sleeping ,執行 waiting 或者處理某些數據,此類執行緒不建議使用Task.Run方法執行,因為Task.Run方法是將任務線上程池內進行排隊執行,如果執行緒池執行緒進行長時間堵塞,會導致執行緒池增長,進而浪費效能,所以如果想要執行長時間的工作建議直接建立一個新執行緒進行工作

    ❌下面這個例子就利用了執行緒池執行長時間的阻塞工作

    public classQueueProcessor
    {
    privatereadonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
    publicvoidStartProcessing()
    {
    Task.Run(ProcessQueue);
    }
    publicvoidEnqueue(Message message)
    {
    _messageQueue.Add(message);
    }
    privatevoidProcessQueue()
    {
    foreach (var item in _messageQueue.GetConsumingEnumerable())
    {
    ProcessItem(item);
    }
    }
    privatevoidProcessItem(Message message) { }
    }



    ☑️所以應該改成這樣

    public classQueueProcessor
    {
    privatereadonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
    publicvoidStartProcessing()
    {
    var thread = new Thread(ProcessQueue)
    {
    // 設定執行緒為背後執行緒,使得在主執行緒結束時此執行緒也會自動結束
    IsBackground = true
    };
    thread.Start();
    }
    publicvoidEnqueue(Message message)
    {
    _messageQueue.Add(message);
    }
    privatevoidProcessQueue()
    {
    foreach (var item in _messageQueue.GetConsumingEnumerable())
    {
    ProcessItem(item);
    }
    }
    privatevoidProcessItem(Message message) { }
    }



    🔔執行緒池內執行緒增加會導致在執行時大量的進行上下文切換,從而浪費程式的整體效能, 執行緒池詳細資訊請參考CLR第27章

    🔔Task.Factory.StartNew方法中有一個 TaskCreationOptions 參數多載,如果設定為 LongRunning ,則會建立一個新執行緒執行

    // 此方法會建立一個新執行緒進行執行
    Task.Factory.StartNew(() => { }, TaskCreationOptions.LongRunning);

    4.避免使用Task.Result和Task.Wait()來堵塞執行緒

    使用Task.Result和Task.Wait()兩個方法進行阻塞異步同步化比直接同步方法阻塞還要 MUCH worse(更糟) ,這種方式被稱為 Sync over async 此方式操作步驟如下

    1.異步執行緒啟動

    2.呼叫執行緒呼叫Result或者Wait()進行阻塞

    3.異步完成時,將一個延續程式碼排程到執行緒池,恢復等待該操作的程式碼

    雖然看起來並沒有什麽關系,但是其實這裏卻是使用了兩個執行緒來完成同步操作,這樣通常會導致 執行緒饑餓 死結

    🔔執行緒饑餓(starvation):指等待時間已經影響到行程執行,如果等待時間過長,導致行程使命沒有意義時,稱之為 餓死

    🔔死結(deadlock):指兩個或兩個以上的執行緒相互爭奪資源,導致行程永久堵塞,

    🔔使用Task.Result和Task.Wait()會在winform和ASP.NET中會死結,因為它們 SynchronizationContext 具有物件,兩個執行緒在 SynchronizationContext 爭奪導致死結,而 ASP.NET Core 則不會產生死結,因為ASP.NET Core本質是一個控制台應用程式,並沒有上下文

    ❌下面的例子,雖然都不會產生死結,但是依然具有很多問題

    async Task<string> RunAsync()
    {
    // 此執行緒ID輸出與UI執行緒ID不一致
    Debug.WriteLine("UI執行緒:"+Thread.CurrentThread.ManagedThreadId);
    returnawait Task.Run(() => "Run");
    }
    stringDoOperationBlocking()
    {
    // 這種方法雖然擺脫了死結的問題,但是也導致了上下文問題,RunAsync不在以UI執行緒呼叫
    // Result和Wait()方法如果出現異常,異常將被包裝為AggregateException進行丟擲,
    return Task.Run(() => RunAsync()).Result;
    }
    }
    privateasyncvoidbutton1_Click(object sender, EventArgs e)
    {
    Debug.WriteLine("RunAsync:" + Thread.CurrentThread.ManagedThreadId);
    Debug.WriteLine(DoOperationBlocking());
    }

    publicstringDoOperationBlocking2()
    {
    // 此方法也是會導致上下文問題,
    // GetAwaiter()方法對異常不會包裝
    returnTask.Run(() => RunAsync()).GetAwaiter().GetResult();
    }

    5.建議使用await來代替continueWith任務

    在async和await,當時可以使用continueWith來延遲執行一些方法,但是continueWith並不會捕捉` SynchronizationContext `,所以建議使用await代替continueWith

    ❌下面例子就是使用continueWith

    privatevoidbutton1_Click(object sender, EventArgs e)
    {
    Debug.WriteLine("UI執行緒:" + Thread.CurrentThread.ManagedThreadId);
    RunAsync().ContinueWith(task =>
    {
    Console.WriteLine("RunAsync returned:"+task.Result);
    // 因為是使用的continueWith,所以執行緒ID與UI執行緒並不一致
    Debug.WriteLine("ContinueWith:" + Thread.CurrentThread.ManagedThreadId);
    });
    }
    publicasync Task<int> RunAsync()
    {
    returnawait Task.FromResult(1 + 1);
    }

    ☑️應該使用await來代替continueWith

    privateasyncvoidbutton1_Click(object sender, EventArgs e)
    {
    Debug.WriteLine("UI執行緒:" + Thread.CurrentThread.ManagedThreadId);
    Debug.WriteLine("RunAsync returned:"+ await RunAsync());
    Debug.WriteLine("UI執行緒:" + Thread.CurrentThread.ManagedThreadId);
    }
    publicasync Task<int> RunAsync()
    {
    returnawait Task.FromResult(1 + 1);
    }

    6.建立TaskCompletionSource 時建議使用TaskCreationOptions.RunContinuationsAsynchronously內容

    對於編寫類別庫的人來說 TaskCompletionSource<T> 是一個具有非常重要的作用,預設情況下任務延續可能會在呼叫 try/set(Result/Exception/Cancel) 的執行緒上進行執行,這也就是說作為編寫類別庫的人來說必須需要考慮上下文,這通常是非常危險,可能就會導致 死結 ' 執行緒池饑餓 *數據結構損壞(如果程式碼異常執行)

    所以在建立 TaskCompletionSourece<T> 時,應該使用 TaskCreationOption.RunContinuationAsyncchronously 參數將後續任務交給執行緒池進行處理

    ❌下面例子就沒有使用 TaskCreationOptions.RunComtinuationsAsynchronously ,

    staticvoidMain(string[] args)
    {
    ThreadPool.SetMinThreads(100, 100);
    Console.WriteLine("Main CurrentManagedThreadId:" + Environment.CurrentManagedThreadId);
    var tcs = new TaskCompletionSource<bool>();
    // 使用TaskContinuationOptions.ExecuteSynchronously來測試延續任務
    ContinueWith(1, tcs.Task);
    // 測試await延續任務
    ContinueAsync(2, tcs.Task);
    Task.Run(() =>
    {
    Console.WriteLine("Task Run CurrentManagedThreadId:" + Environment.CurrentManagedThreadId );
    tcs.TrySetResult(true);
    });
    Console.ReadLine();
    }
    staticvoidprint(int id) => Console.WriteLine($"continuation:{id}\tCurrentManagedThread:{Environment.CurrentManagedThreadId}");
    staticasync Task ContinueAsync(int id, Task task)
    {
    await task.ConfigureAwait(false);
    print(id);
    }
    static Task ContinueWith(int id, Task task)
    {
    return task.ContinueWith(
    t => print(id),
    CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
    }

    ☑️所以應該改為使用 TaskCreationOptions.RunComtinuationsAsynchronously 參數進行設定TaskCompletionSoure

    staticvoidMain(string[] args)
    {
    ThreadPool.SetMinThreads(100, 100);
    Console.WriteLine("Main CurrentManagedThreadId:" + Environment.CurrentManagedThreadId);
    var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
    // 使用TaskContinuationOptions.ExecuteSynchronously來測試延續任務
    ContinueWith(1, tcs.Task);
    // 測試await延續任務
    ContinueAsync(2, tcs.Task);
    Task.Run(() =>
    {
    Console.WriteLine("Task Run CurrentManagedThreadId:" + Environment.CurrentManagedThreadId);
    tcs.TrySetResult(true);
    });
    Console.ReadLine();
    }
    staticvoidprint(int id) => Console.WriteLine($"continuation:{id}\tCurrentManagedThread:{Environment.CurrentManagedThreadId}");
    staticasync Task ContinueAsync(int id, Task task)
    {
    await task.ConfigureAwait(false);
    print(id);
    }
    static Task ContinueWith(int id, Task task)
    {
    return task.ContinueWith(
    t => print(id),
    CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
    }

    🔔 TaskCreationOptions.RunContinuationsAsynchronously 內容和 TaskContinuationOptions.RunContinuationsAsynchronously 很相似,但請註意它們的使用方式

    7.建議使用CancellationTokenSource(s)進行超時管理時總是釋放(dispose)

    用於進行超時的CancellationTokenSources,如果不釋放,則會增加 timer queue(計時器佇列) 的壓力

    ❌下面例子因為沒有釋放,所以在每次請求發出之後,計時器在佇列中停留10秒鐘

    publicasync Task<Stream> HttpClientAsyncWithCancellationBad()
    {
    var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    using (var client = _httpClientFactory.CreateClient())
    {
    var response = await client.GetAsync("http://backend/api/1", cts.Token);
    returnawait response.Content.ReadAsStreamAsync();
    }
    }

    ☑️所以應該及時的釋放CancellationSoure,使得正確的從佇列中刪除計時器

    publicasync Task<Stream> HttpClientAsyncWithCancellationGood()
    {
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
    {
    using (var client = _httpClientFactory.CreateClient())
    {
    var response = await client.GetAsync("http://backend/api/1", cts.Token);
    returnawait response.Content.ReadAsStreamAsync();
    }
    }
    }

    🔔設定延遲時間具有兩種方式

    1.構造器參數

    publicCancellationTokenSource(TimeSpan delay);
    publicCancellationTokenSource(int millisecondsDelay);

    2.呼叫例項物件CancelAfter()
    public void CancelAfter(TimeSpan delay);
    public void CancelAfter(int millisecondsDelay);

    8.建議將協作式取消物件(CancellationToken)傳遞給所有使用到的API

    由於在.NET中取消操作必須顯示的傳遞 CancellationToken ,所以如果想取消所有呼叫的異步函式,那麽應該將 CancllationToken 傳遞給此呼叫鏈中的所有函式

    ❌下面例子在呼叫ReadAsync時並沒有傳遞 CancellationToken ,所以不能有效的取消

    publicasync Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
    {
    byte[] buffer = newbyte[1024];
    // 使用FileOptions.Asynchronous參數指定異步通訊
    using(Stream stream = new FileStream(
    @"d:\資料\Blogs\Task\TaskTest",
    FileMode.OpenOrCreate,
    FileAccess.ReadWrite,
    FileShare.None,
    1024,
    options:FileOptions.Asynchronous))
    {
    // 由於並沒有將cancellationToken傳遞給ReadAsync,所以無法進行有效的取消
    int read = await stream.ReadAsync(buffer, 0, buffer.Length);
    return Encoding.UTF8.GetString(buffer, 0, read);
    }
    }

    ☑️所以應該將 CancellationToken 傳遞給ReadAsync(),以達到有效的取消

    publicasync Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
    {
    byte[] buffer = newbyte[1024];
    // 使用FileOptions.Asynchronous參數指定異步通訊
    using(Stream stream = new FileStream(
    @"d:\資料\Blogs\Task\TaskTest",
    FileMode.OpenOrCreate,
    FileAccess.ReadWrite,
    FileShare.None,
    1024,
    options:FileOptions.Asynchronous))
    {
    // 由於並沒有將cancellationToken傳遞給ReadAsync,所以無法進行有效的取消
    int read = await stream.ReadAsync(buffer, 0, buffer.Length,cancellationToken);
    return Encoding.UTF8.GetString(buffer, 0, read);
    }
    }

    🔔在使用異步IO時,應該將 options 參數設定為 FileOptions.Asynchronous ,否則會產生額外的執行緒浪費,詳細資訊請參考CLR中28.12節

    9.建議取消那些不會自動取消的操作(CancellationTokenRegistry,timer)

    在異步編程時出現了一種模式 cancelling an uncancellable operation ,這個用於取消像 CancellationTokenRegistry timer 這樣的東西,通常是在被取消或超時時建立另外一個執行緒進行操作,然後使用Task.WhenAny進行判斷是完成還是被取消了

    使用CancellationToken

    :x: 下面例子使用了Task.delay(-1,token)建立在觸發CancellationToken時觸發的任務,但是如果CancellationToken不觸發,則沒有辦法釋放CancellationTokenRegistry,就有可能會導致記憶體泄露

    publicstaticasync Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
    {
    // 沒有方法釋放cancellationToken註冊
    var delayTask = Task.Delay(-1, cancellationToken);
    var resultTask = await Task.WhenAny(task, delayTask);
    if (resultTask == delayTask)
    {
    // 取消異步操作
    thrownew OperationCanceledException();
    }
    returnawait task;
    }

    :ballot_box_with_check:所以應該改成下面這樣,在任務一完成,就釋放CancellationTokenRegistry

    publicstaticasync Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
    {
    var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
    using (cancellationToken.Register(state =>
    {
    // 這樣將在其中一個任務觸發時立即釋放CancellationTokenRegistry
    ((TaskCompletionSource<object>)state).TrySetResult(null);
    },
    tcs))
    {
    var resultTask = await Task.WhenAny(task, tcs.Task);
    if (resultTask == tcs.Task)
    {
    // 取消異步操作
    thrownew OperationCanceledException(cancellationToken);
    }
    returnawait task;
    }
    }

    使用超時任務

    :x:下面這個例子即使在操作完成之後,也不會取消定時器,這也就是說最終會在計時器佇列中產生大量的計時器,從而浪費效能

    publicstaticasync Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
    {
    var delayTask = Task.Delay(timeout);
    var resultTask = await Task.WhenAny(task, delayTask);
    if (resultTask == delayTask)
    {
    // 取消異步操作
    thrownew OperationCanceledException();
    }
    returnawait task;
    }

    :ballot_box_with_check:應改成下面這樣,這樣將在任務完成之後,取消計時器的操作

    publicstaticasync Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
    {
    using (var cts = new CancellationTokenSource())
    {
    var delayTask = Task.Delay(timeout, cts.Token);
    var resultTask = await Task.WhenAny(task, delayTask);
    if (resultTask == delayTask)
    {
    // 取消異步操作
    thrownew OperationCanceledException();
    }
    else
    {
    // 取消計時器任務
    cts.Cancel();
    }
    returnawait task;
    }
    }

    10.使用StreamWriter(s)或Stream(s)時在Dispose之前建議先呼叫FlushAsync

    當使用Stream和StreamWriter進行異步寫入時,底層數據也有可能被緩沖,當數據被緩沖時,Stream和StreamWriter將使用同步的方式進行 write/flush ,這將會導致執行緒阻塞,並且有可能導致執行緒池內執行緒不足(執行緒池饑餓)

    ❌下面例子由於沒有呼叫 FlushAsync() ,所以最後是以同步方式進行write/flush的

    publicasyncstatic Task RunAsync()
    {
    using (var streamWriter = new StreamWriter(@"C:\資料\Blogs\Task"))
    {
    // 由於沒有呼叫FlushAsync,所以最後是以同步方式進行write/flush的
    await streamWriter.WriteAsync("Hello World");
    }
    }

    ☑️所以應該改為下面這樣,在 Dispose 之前呼叫 FlushAsync()

    publicasyncstatic Task RunAsync()
    {
    using (var streamWriter = new StreamWriter(@"C:\資料\Blogs\Task"))
    {
    await streamWriter.WriteAsync("Hello World");
    // 呼叫FlushAsync() 使其使用異步write/flush
    await streamWriter.FlushAsync();
    }
    }

    11.建議使用 async/await而不是直接返回Task

    使用async/await 代替直接返回Task具有以上好處

  • 異步和同步的異常都被始終被規範為了異步

  • 程式碼更容易修改(例如:增加一個 using )

  • 異步的方法診斷起來更加容易(例如:偵錯,掛起)

  • 丟擲的異常將自動包裝在返回的任務之中,而不是丟擲實際異常

  • ❌下面這個錯誤的例子是將Task直接返回給了呼叫者

    public Task<int> RunAsync()
    {
    return Task.FromResult(1 + 1);
    }

    ☑️所以應該使用async/await來代替返回Task

    publicasync Task<int> RunAsync()
    {
    returnawait Task.FromResult(1 + 1);
    }

    🔔使用async/await來代替返回Task時,還有效能上的考慮,雖然直接Task會更快,但是最終卻改變了異步的行為,失去了異步狀態機的一些好處

    使用場景

    1. 使用定時器回呼函式

    ❌下面例子使用一個返回值為void的異步,將其傳遞給Timer進行,因此,如果其中任務丟擲異常,則整個行程將結束

    public classPinger
    {
    privatereadonly Timer _timer;
    privatereadonly HttpClient _client;
    publicPinger(HttpClient client)
    {
    _client = new HttpClient();
    _timer = new Timer(Heartbeat, null, 1000, 1000);
    }
    publicasyncvoidHeartbeat(object state)
    {
    await httpClient.GetAsync("http://mybackend/api/ping");
    }
    }

    ❌下面例子將阻止計時器回呼,這有可能導致執行緒池中執行緒耗盡,這也是一個異步差於同步的例子

    public classPinger
    {
    privatereadonly Timer _timer;
    privatereadonly HttpClient _client;
    publicPinger(HttpClient client)
    {
    _client = new HttpClient();
    _timer = new Timer(Heartbeat, null, 1000, 1000);
    }
    publicvoidHeartbeat(object state)
    {
    httpClient.GetAsync("http://mybackend/api/ping").GetAwaiter().GetResult();
    }
    }

    ☑️下面例子是使用基於的異步的方法,並在定時器回呼函式中丟棄該任務,並且如果此方法丟擲異常,則也不會關閉行程,而是會觸發 TaskScheduler.UnobservedTaskException 事件

    public classPinger
    {
    privatereadonly Timer _timer;
    privatereadonly HttpClient _client;
    publicPinger(HttpClient client)
    {
    _client = new HttpClient();
    _timer = new Timer(Heartbeat, null, 1000, 1000);
    }
    publicvoidHeartbeat(object state)
    {
    _ = DoAsyncPing();
    }
    privateasync Task DoAsyncPing()
    {
    // 異步等待
    await _client.GetAsync("http://mybackend/api/ping");
    }

    2.建立回呼函式參數時註意避免 async void

    假如有 BackgroudQueue 類中有一個接收回呼函式的 FireAndForget 方法,該方法在某個時候執行呼叫

    ❌下面這個錯誤例子將強制呼叫者要麽阻塞要麽使用async void異步方法

    public classBackgroundQueue
    {
    publicstaticvoidFireAndForget(Action action) { }
    }

    staticasync Task Main(string[] args)
    {
    var httpClient = new HttpClient();
    // 因為方法型別是Action,所以只能使用async void
    BackgroundQueue.FireAndForget(async () =>
    {
    await httpClient.GetAsync("http://pinger/api/1");
    });
    }

    ☑️所以應該構建一個回呼異步方法的多載

    public classBackgroundQueue
    {
    publicstaticvoidFireAndForget(Action action) { }
    publicstaticvoidFireAndForget(Func<Task> action) { }
    }

    3.使用ConcurrentDictionary.GetOrAdd註意場景

    緩存異步結果是一種很常見的做法,ConcurrentDictionary是一個很好的集合,而GetOrAdd也是一個很方便的方法,它用於嘗試獲取已經存在的項,如果沒有則添加項.因為回呼是同步的,所以很容易編寫 Task.Result 的程式碼,從而生成異步的結果值,但是這樣很容易導致執行緒池饑餓

    ❌下面這個例子就有可能導致執行緒池饑餓,因為當如果沒有緩存人員數據時,將阻塞請求執行緒

    public classPersonController : Controller
    {
    private AppDbContext _db;
    privatestatic ConcurrentDictionary<int, Person> _cache = new ConcurrentDictionary<int, Person>();
    publicPersonController(AppDbContext db)
    {
    _db = db;
    }
    public IActionResult Get(int id)
    {
    // 如果不存在緩存數據,則會進入堵塞狀態
    var person = _cache.GetOrAdd(id, (key) => db.People.FindAsync(key).Result);
    return Ok(person);
    }
    }

    ☑️可以改成緩存執行緒本身,而不是結果,這樣將不會導致執行緒池饑餓

    public classPersonController : Controller
    {
    private AppDbContext _db;
    privatestatic ConcurrentDictionary<int, Task<Person>> _cache = new ConcurrentDictionary<int, Task<Person>>();
    publicPersonController(AppDbContext db)
    {
    _db = db;
    }
    publicasync Task<IActionResult> Get(int id)
    {
    // 因為緩存的是執行緒本身,所以沒有進行堵塞,也就不會產生執行緒池饑餓
    var person = await _cache.GetOrAdd(id, (key) => db.People.FindAsync(key));
    return Ok(person);
    }
    }

    🔔這種方法,在最後,GetOrAdd()可能並列多次來執行緩存回呼,這可能導致啟動多次昂貴的計算

    ☑️可以使用 async lazy 模式來取代多次執行回呼問題

    public classPersonController : Controller
    {
    private AppDbContext _db;
    private static ConcurrentDictionary<int, AsyncLazy<Person>> _cache = new ConcurrentDictionary<int, AsyncLazy<Person>>();
    public PersonController(AppDbContext db)
    {
    _db = db;
    }
    public async Task<IActionResult> Get(int id)
    {
    // 使用Lazy進行了延遲載入(使用時呼叫),解決了多次執行回呼問題
    var person = await _cache.GetOrAdd(id, (key) => new AsyncLazy<Person>(() => db.People.FindAsync(key)));
    return Ok(person);
    }
    private classAsyncLazy<T> : Lazy<Task<T>>
    {
    public AsyncLazy(Func<Task<T>> valueFactory) : base(valueFactory)
    {
    }
    }

    4.建構函式對於異步的問題

    建構函式是同步,下面看看在建構函式中處理異步情況

    下面是使用客戶端API的例子,當然,在使用API之前需要異步進行連線

    publicinterfaceIRemoteConnectionFactory
    {
    Task<IRemoteConnection> ConnectAsync();
    }
    publicinterfaceIRemoteConnection
    {
    Task PublishAsync(string channel, string message);
    Task DisposeAsync();
    }

    ❌下面例子使用 Task.Result 在建構函式中進行連線,這有可能導致執行緒池饑餓和死結現象

    public classService : IService
    {
    privatereadonly IRemoteConnection _connection;
    publicService(IRemoteConnectionFactory connectionFactory)
    {
    _connection = connectionFactory.ConnectAsync().Result;
    }
    }

    ☑️正確的方式應該使用靜態工廠模式進行異步連線

    public classService : IService
    {
    privatereadonly IRemoteConnection _connection;
    privateService(IRemoteConnection connection)
    {
    _connection = connection;
    }
    publicstaticasync Task<Service> CreateAsync(IRemoteConnectionFactory connectionFactory)
    {
    returnnew Service(await connectionFactory.ConnectAsync());
    }
    }

    原文地址:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/93e39b8f48169cce4803615519ef87bb2a969c8e/AsyncGuidance.md#prefer-taskfromresult-over-taskrun-for-pre-computed-or-trivially-computed-data