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


異步操作時應注意的要點

使用場景

異步操作時需要注意的要點

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

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

  • 無法得知異步函數的狀態機在什么時候執行完畢
  • 如果異步函數中出現異常,則會導致進程崩潰

❌異步函數不應該返回void

static  void Main(string[] args)
{
     try
     {
          //      如果Run方法無異常正常執行,那么程序無法得知其狀態機什么時候執行完畢
          Run();
     }
     catch (Exception ex)
     {   
          Console.WriteLine(ex.Message);
     }
     Console.Read();
}
static async void Run()
{
     //      由於方法返回的為void,所以在調用此方法時無法捕捉異常,使得進程崩潰
     throw new Exception("異常了");
     await Task.Run(() => { });

}

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

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

}

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

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

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

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

public async Task<int> RunAsync()
{
     return  await Task.Run(()=>1+1);
}

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

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

還有另外一種代替方法,那就是使用ValueTask 類型,ValueTask 是一個可被等待異步結構,所以並不會在堆中分配內存和任務分配,從而性能更優化.

☑️使用ValueTask 代替

static  async Task Main(string[] args)
{
     await AddAsync(1, 1);
}
static ValueTask<int> AddAsync(int a, int b)
{
     //      返回一個可被等待的ValueTask類型
     return new 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 class QueueProcessor
{
     private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();

     public void StartProcessing()
     {
          Task.Run(ProcessQueue);
     }

     public void Enqueue(Message message)
     {
          _messageQueue.Add(message);
     }

     private void ProcessQueue()
     {
          foreach (var item in _messageQueue.GetConsumingEnumerable())
          {
               ProcessItem(item);
          }
     }

     private void ProcessItem(Message message) { }
}

☑️所以應該改成這樣

public class QueueProcessor
{
     private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();

     public void StartProcessing()
     {
          var thread = new Thread(ProcessQueue)
          {
               // 設置線程為背后線程,使得在主線程結束時此線程也會自動結束
               IsBackground = true
          };
          thread.Start();
     }

     public void Enqueue(Message message)
     {
          _messageQueue.Add(message);
     }

     private void ProcessQueue()
     {
          foreach (var item in _messageQueue.GetConsumingEnumerable())
          {
               ProcessItem(item);
          }
     }

     private void ProcessItem(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);
     return await Task.Run(() => "Run");
}
string DoOperationBlocking()
{
     //  這種方法雖然擺脫了死鎖的問題,但是也導致了上下文問題,RunAsync不在以UI線程調用
     //  Result和Wait()方法如果出現異常,異常將被包裝為AggregateException進行拋出,
     return Task.Run(() => RunAsync()).Result;
}
}
private async void button1_Click(object sender, EventArgs e)
{
     Debug.WriteLine("RunAsync:" + Thread.CurrentThread.ManagedThreadId);
     Debug.WriteLine(DoOperationBlocking());
}
public string DoOperationBlocking2()
{
     //     此方法也是會導致上下文問題,
     //     GetAwaiter()方法對異常不會包裝
     return Task.Run(() => RunAsync()).GetAwaiter().GetResult();
}

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

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

❌下面例子就是使用continueWith

private  void button1_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);
   });
}
public async Task<int> RunAsync()
{
     return await Task.FromResult(1 + 1);
}

☑️應該使用await來代替continueWith

private async  void button1_Click(object sender, EventArgs e)
{
     Debug.WriteLine("UI線程:" + Thread.CurrentThread.ManagedThreadId);
     Debug.WriteLine("RunAsync returned:"+ await RunAsync());
     Debug.WriteLine("UI線程:" + Thread.CurrentThread.ManagedThreadId);
}
public async Task<int> RunAsync()
{
     return await Task.FromResult(1 + 1);
}

6.創建TaskCompletionSource 時建議使用TaskCreationOptions.RunContinuationsAsynchronously屬性

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

所以在創建TaskCompletionSourece<T>時,應該使用TaskCreationOption.RunContinuationAsyncchronously參數將后續任務交給線程池進行處理

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

static void Main(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();
}
static void print(int id) => Console.WriteLine($"continuation:{id}\tCurrentManagedThread:{Environment.CurrentManagedThreadId}");
static async 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

static void Main(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();
}
static void print(int id) => Console.WriteLine($"continuation:{id}\tCurrentManagedThread:{Environment.CurrentManagedThreadId}");
static async 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秒鍾

public async 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);
        return await response.Content.ReadAsStreamAsync();
    }
}

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

public async 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);
               return await response.Content.ReadAsStreamAsync();
          }
     }
}

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

1.構造器參數

public CancellationTokenSource(TimeSpan delay);
    public CancellationTokenSource(int millisecondsDelay);

2.調用實例對象CancelAfter()
public void CancelAfter(TimeSpan delay);
public void CancelAfter(int millisecondsDelay);

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

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

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

public async Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
{
     byte[] buffer = new byte[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(),以達到有效的取消

public async Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
{
     byte[] buffer = new byte[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,這個用於取消像CancellationTokenRegistrytimer這樣的東西,通常是在被取消或超時時創建另外一個線程進行操作,然后使用Task.WhenAny進行判斷是完成還是被取消了

使用CancellationToken

:x: 下面例子使用了Task.delay(-1,token)創建在觸發CancellationToken時觸發的任務,但是如果CancellationToken不觸發,則沒有辦法釋放CancellationTokenRegistry,就有可能會導致內存泄露	
public static async 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)
     {
          //  取消異步操作
          throw new OperationCanceledException();
     }

     return await task;
}
:ballot_box_with_check:所以應該改成下面這樣,在任務一完成,就釋放CancellationTokenRegistry
public static async 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)
          {
               //  取消異步操作
               throw new OperationCanceledException(cancellationToken);
          }

          return await task;
     }
}

使用超時任務

:x:下面這個例子即使在操作完成之后,也不會取消定時器,這也就是說最終會在計時器隊列中產生大量的計時器,從而浪費性能
public static async 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)
     {
          //  取消異步操作
          throw new OperationCanceledException();
     }

     return await task;
}
:ballot_box_with_check:應改成下面這樣,這樣將在任務完成之后,取消計時器的操作
public static async 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)
          {
               //  取消異步操作
               throw new OperationCanceledException();
          }
          else
          {
               //  取消計時器任務
               cts.Cancel();
          }

          return await task;
     }
}

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

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

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

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

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

public async static 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

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

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

使用場景

1. 使用定時器回調函數

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

public class Pinger
{
     private readonly Timer _timer;
     private readonly HttpClient _client;

     public Pinger(HttpClient client)
     {
          _client = new HttpClient();
          _timer = new Timer(Heartbeat, null, 1000, 1000);
     }

     public async void Heartbeat(object state)
     {
          await httpClient.GetAsync("http://mybackend/api/ping");
     }
}

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

public class Pinger
{
     private readonly Timer _timer;
     private readonly HttpClient _client;

     public Pinger(HttpClient client)
     {
          _client = new HttpClient();
          _timer = new Timer(Heartbeat, null, 1000, 1000);
     }

     public void Heartbeat(object state)
     {
          httpClient.GetAsync("http://mybackend/api/ping").GetAwaiter().GetResult();
     }
}

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

public class Pinger
{
    private readonly Timer _timer;
    private readonly HttpClient _client;
    
    public Pinger(HttpClient client)
    {
        _client = new HttpClient();
        _timer = new Timer(Heartbeat, null, 1000, 1000);
    }

    public void Heartbeat(object state)
    {
        _ = DoAsyncPing();
    }
    private async Task DoAsyncPing()
    {
        //		異步等待
        await _client.GetAsync("http://mybackend/api/ping");
    }

2.創建回調函數參數時注意避免 async void

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

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

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

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

public class BackgroundQueue
{
    public static void FireAndForget(Action action) { }
    public static void FireAndForget(Func<Task> action) { }
}

3.使用ConcurrentDictionary.GetOrAdd注意場景

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

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

public class PersonController : Controller
{
	private AppDbContext _db;
	private static ConcurrentDictionary<int, Person> _cache = new ConcurrentDictionary<int, Person>();

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

☑️可以改成緩存線程本身,而不是結果,這樣將不會導致線程池飢餓

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

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

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

public class PersonController : 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 class AsyncLazy<T> : Lazy<Task<T>>
   {
      public AsyncLazy(Func<Task<T>> valueFactory) : base(valueFactory)
      {
      }
   }

4.構造函數對於異步的問題

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

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

public interface IRemoteConnectionFactory
{
   Task<IRemoteConnection> ConnectAsync();
}

public interface IRemoteConnection
{
    Task PublishAsync(string channel, string message);
    Task DisposeAsync();
}

❌下面例子使用Task.Result在構造函數中進行連接,這有可能導致線程池飢餓和死鎖現象

public class Service : IService
{
    private readonly IRemoteConnection _connection;

    public Service(IRemoteConnectionFactory connectionFactory)
    {
        _connection = connectionFactory.ConnectAsync().Result;
    }
}

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

public class Service : IService
{
    private readonly IRemoteConnection _connection;

    private Service(IRemoteConnection connection)
    {
        _connection = connection;
    }

    public static async Task<Service> CreateAsync(IRemoteConnectionFactory connectionFactory)
    {
        return new Service(await connectionFactory.ConnectAsync());
    }
}

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


免責聲明!

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



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