C#使用异步操作时的注意要点(翻译)

异步操作时应注意的要点

  • 使用异步方法返回值应避免使用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

 

static void Main(string[] args)

{
  1. try
  2. {
  3. // 如果Run方法无异常正常执行,那么程序无法得知其状态机什么时候执行完毕
  4. Run();
  5. }
  6. catch (Exception ex)
  7. {
  8. Console.WriteLine(ex.Message);
  9. }
  10. Console.Read();
  11. }
  12. static async void Run()
  13. {
  14. // 由于方法返回的为void,所以在调用此方法时无法捕捉异常,使得进程崩溃
  15. throw new Exception("异常了");
  16. await Task.Run(() => { });
  17. }

☑️应该将异步函数返回Task

 

static async Task Main(string[] args)

{
  1. try
  2. {
  3. // 因为在此进行await,所以主程序知道什么时候状态机执行完成
  4. await RunAsync();
  5. Console.Read();
  6. }
  7. catch (Exception ex)
  8. {
  9. Console.WriteLine(ex.Message);
  10. }
  11. }
  12. static async Task RunAsync()
  13. {
  14. // 因为此异步方法返回的为Task,所以此异常可以被捕捉
  15. throw new Exception("异常了");
  16. await Task.Run(() => { });
  17. }

:事件是一个例外,异步事件也是返回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()

{
  1. return await Task.Run(()=>1+1);
  2. }

☑️而应该使用Task.FromResult代替

 

public async Task<int> RunAsync()

{
  1. return await Task.FromResult(1 + 1);
  2. }

还有另外一种代替方法,那就是使用ValueTask类型,ValueTask是一个可被等待异步结构,所以并不会在堆中分配内存和任务分配,从而性能更优化.

☑️使用ValueTask代替

 

static async Task Main(string[] args)

{
  1. await AddAsync(1, 1);
  2. }
  3. static ValueTask<int> AddAsync(int a, int b)
  4. {
  5. // 返回一个可被等待的ValueTask类型
  6. return new ValueTask<int>(a + b);
  7. }

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

{
  1. private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
  2. public void StartProcessing()
  3. {
  4. Task.Run(ProcessQueue);
  5. }
  6. public void Enqueue(Message message)
  7. {
  8. _messageQueue.Add(message);
  9. }
  10. private void ProcessQueue()
  11. {
  12. foreach (var item in _messageQueue.GetConsumingEnumerable())
  13. {
  14. ProcessItem(item);
  15. }
  16. }
  17. private void ProcessItem(Message message) { }
  18. }

☑️所以应该改成这样

 

public class QueueProcessor

{
  1. private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
  2. public void StartProcessing()
  3. {
  4. var thread = new Thread(ProcessQueue)
  5. {
  6. // 设置线程为背后线程,使得在主线程结束时此线程也会自动结束
  7. IsBackground = true
  8. };
  9. thread.Start();
  10. }
  11. public void Enqueue(Message message)
  12. {
  13. _messageQueue.Add(message);
  14. }
  15. private void ProcessQueue()
  16. {
  17. foreach (var item in _messageQueue.GetConsumingEnumerable())
  18. {
  19. ProcessItem(item);
  20. }
  21. }
  22. private void ProcessItem(Message message) { }
  23. }

🔔线程池内线程增加会导致在执行时大量的进行上下文切换,从而浪费程序的整体性能, 线程池详细信息请参考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()

{
  1. // 此线程ID输出与UI线程ID不一致
  2. Debug.WriteLine("UI线程:"+Thread.CurrentThread.ManagedThreadId);
  3. return await Task.Run(() => "Run");
  4. }
  5. string DoOperationBlocking()
  6. {
  7. // 这种方法虽然摆脱了死锁的问题,但是也导致了上下文问题,RunAsync不在以UI线程调用
  8. // Result和Wait()方法如果出现异常,异常将被包装为AggregateException进行抛出,
  9. return Task.Run(() => RunAsync()).Result;
  10. }
  11. }
  12. private async void button1_Click(object sender, EventArgs e)
  13. {
  14. Debug.WriteLine("RunAsync:" + Thread.CurrentThread.ManagedThreadId);
  15. Debug.WriteLine(DoOperationBlocking());
  16. }
 

public string DoOperationBlocking2()

{
  1. // 此方法也是会导致上下文问题,
  2. // GetAwaiter()方法对异常不会包装
  3. return Task.Run(() => RunAsync()).GetAwaiter().GetResult();
  4. }

5.建议使用await来代替continueWith任务

在async和await,当时可以使用continueWith来延迟执行一些方法,但是continueWith并不会捕捉`SynchronizationContext `,所以建议使用await代替continueWith

❌下面例子就是使用continueWith

 

private void button1_Click(object sender, EventArgs e)

{
  1. Debug.WriteLine("UI线程:" + Thread.CurrentThread.ManagedThreadId);
  2. RunAsync().ContinueWith(task =>
  3. {
  4. Console.WriteLine("RunAsync returned:"+task.Result);
  5. // 因为是使用的continueWith,所以线程ID与UI线程并不一致
  6. Debug.WriteLine("ContinueWith:" + Thread.CurrentThread.ManagedThreadId);
  7. });
  8. }
  9. public async Task<int> RunAsync()
  10. {
  11. return await Task.FromResult(1 + 1);
  12. }

☑️应该使用await来代替continueWith

 

private async void button1_Click(object sender, EventArgs e)

{
  1. Debug.WriteLine("UI线程:" + Thread.CurrentThread.ManagedThreadId);
  2. Debug.WriteLine("RunAsync returned:"+ await RunAsync());
  3. Debug.WriteLine("UI线程:" + Thread.CurrentThread.ManagedThreadId);
  4. }
  5. public async Task<int> RunAsync()
  6. {
  7. return await Task.FromResult(1 + 1);
  8. }

6.创建TaskCompletionSource时建议使用TaskCreationOptions.RunContinuationsAsynchronously属性

对于编写类库的人来说TaskCompletionSource<T>是一个具有非常重要的作用,默认情况下任务延续可能会在调用try/set(Result/Exception/Cancel)的线程上进行运行,这也就是说作为编写类库的人来说必须需要考虑上下文,这通常是非常危险,可能就会导致死锁线程池饥饿 *数据结构损坏(如果代码异常运行)

所以在创建TaskCompletionSourece<T>时,应该使用TaskCreationOption.RunContinuationAsyncchronously参数将后续任务交给线程池进行处理

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

 

static void Main(string[] args)

{
  1. ThreadPool.SetMinThreads(100, 100);
  2. Console.WriteLine("Main CurrentManagedThreadId:" + Environment.CurrentManagedThreadId);
  3. var tcs = new TaskCompletionSource<bool>();
  4. // 使用TaskContinuationOptions.ExecuteSynchronously来测试延续任务
  5. ContinueWith(1, tcs.Task);
  6. // 测试await延续任务
  7. ContinueAsync(2, tcs.Task);
  8. Task.Run(() =>
  9. {
  10. Console.WriteLine("Task Run CurrentManagedThreadId:" + Environment.CurrentManagedThreadId );
  11. tcs.TrySetResult(true);
  12. });
  13. Console.ReadLine();
  14. }
  15. static void print(int id) => Console.WriteLine($"continuation:{id}\tCurrentManagedThread:{Environment.CurrentManagedThreadId}");
  16. static async Task ContinueAsync(int id, Task task)
  17. {
  18. await task.ConfigureAwait(false);
  19. print(id);
  20. }
  21. static Task ContinueWith(int id, Task task)
  22. {
  23. return task.ContinueWith(
  24. t => print(id),
  25. CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
  26. }

☑️所以应该改为使用TaskCreationOptions.RunComtinuationsAsynchronously参数进行设置TaskCompletionSoure

 

static void Main(string[] args)

{
  1. ThreadPool.SetMinThreads(100, 100);
  2. Console.WriteLine("Main CurrentManagedThreadId:" + Environment.CurrentManagedThreadId);
  3. var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
  4. // 使用TaskContinuationOptions.ExecuteSynchronously来测试延续任务
  5. ContinueWith(1, tcs.Task);
  6. // 测试await延续任务
  7. ContinueAsync(2, tcs.Task);
  8. Task.Run(() =>
  9. {
  10. Console.WriteLine("Task Run CurrentManagedThreadId:" + Environment.CurrentManagedThreadId);
  11. tcs.TrySetResult(true);
  12. });
  13. Console.ReadLine();
  14. }
  15. static void print(int id) => Console.WriteLine($"continuation:{id}\tCurrentManagedThread:{Environment.CurrentManagedThreadId}");
  16. static async Task ContinueAsync(int id, Task task)
  17. {
  18. await task.ConfigureAwait(false);
  19. print(id);
  20. }
  21. static Task ContinueWith(int id, Task task)
  22. {
  23. return task.ContinueWith(
  24. t => print(id),
  25. CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
  26. }

🔔TaskCreationOptions.RunContinuationsAsynchronously属性和TaskContinuationOptions.RunContinuationsAsynchronously很相似,但请注意它们的使用方式

7.建议使用CancellationTokenSource(s)进行超时管理时总是释放(dispose)

用于进行超时的CancellationTokenSources,如果不释放,则会增加timer queue(计时器队列)的压力

❌下面例子因为没有释放,所以在每次请求发出之后,计时器在队列中停留10秒钟

 

public async Task<Stream> HttpClientAsyncWithCancellationBad()

{
  1. var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
  2. using (var client = _httpClientFactory.CreateClient())
  3. {
  4. var response = await client.GetAsync("http://backend/api/1", cts.Token);
  5. return await response.Content.ReadAsStreamAsync();
  6. }
  7. }

☑️所以应该及时的释放CancellationSoure,使得正确的从队列中删除计时器

 

public async Task<Stream> HttpClientAsyncWithCancellationGood()

{
  1. using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
  2. {
  3. using (var client = _httpClientFactory.CreateClient())
  4. {
  5. var response = await client.GetAsync("http://backend/api/1", cts.Token);
  6. return await response.Content.ReadAsStreamAsync();
  7. }
  8. }
  9. }

🔔设置延迟时间具有两种方式

1.构造器参数

  1.  
    public CancellationTokenSource(TimeSpan delay);
  2.  
    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)

{
  1. byte[] buffer = new byte[1024];
  2. // 使用FileOptions.Asynchronous参数指定异步通信
  3. using(Stream stream = new FileStream(
  4. @"d:\资料\Blogs\Task\TaskTest",
  5. FileMode.OpenOrCreate,
  6. FileAccess.ReadWrite,
  7. FileShare.None,
  8. 1024,
  9. options:FileOptions.Asynchronous))
  10. {
  11. // 由于并没有将cancellationToken传递给ReadAsync,所以无法进行有效的取消
  12. int read = await stream.ReadAsync(buffer, 0, buffer.Length);
  13. return Encoding.UTF8.GetString(buffer, 0, read);
  14. }
  15. }

☑️所以应该将CancellationToken传递给ReadAsync(),以达到有效的取消

 

public async Task<string> DoAsyncThing(CancellationToken cancellationToken = default)

{
  1. byte[] buffer = new byte[1024];
  2. // 使用FileOptions.Asynchronous参数指定异步通信
  3. using(Stream stream = new FileStream(
  4. @"d:\资料\Blogs\Task\TaskTest",
  5. FileMode.OpenOrCreate,
  6. FileAccess.ReadWrite,
  7. FileShare.None,
  8. 1024,
  9. options:FileOptions.Asynchronous))
  10. {
  11. // 由于并没有将cancellationToken传递给ReadAsync,所以无法进行有效的取消
  12. int read = await stream.ReadAsync(buffer, 0, buffer.Length,cancellationToken);
  13. return Encoding.UTF8.GetString(buffer, 0, read);
  14. }
  15. }

🔔在使用异步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)
{
  1. // 没有方法释放cancellationToken注册
  2. var delayTask = Task.Delay(-1, cancellationToken);
  3. var resultTask = await Task.WhenAny(task, delayTask);
  4. if (resultTask == delayTask)
  5. {
  6. // 取消异步操作
  7. throw new OperationCanceledException();
  8. }
  9. return await task;
  10. }
:ballot_box_with_check:所以应该改成下面这样,在任务一完成,就释放CancellationTokenRegistry
 
 

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)

{
  1. var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  2. using (cancellationToken.Register(state =>
  3. {
  4. // 这样将在其中一个任务触发时立即释放CancellationTokenRegistry
  5. ((TaskCompletionSource<object>)state).TrySetResult(null);
  6. },
  7. tcs))
  8. {
  9. var resultTask = await Task.WhenAny(task, tcs.Task);
  10. if (resultTask == tcs.Task)
  11. {
  12. // 取消异步操作
  13. throw new OperationCanceledException(cancellationToken);
  14. }
  15. return await task;
  16. }
  17. }

使用超时任务

:x:下面这个例子即使在操作完成之后,也不会取消定时器,这也就是说最终会在计时器队列中产生大量的计时器,从而浪费性能
 
 

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)

{
  1. var delayTask = Task.Delay(timeout);
  2. var resultTask = await Task.WhenAny(task, delayTask);
  3. if (resultTask == delayTask)
  4. {
  5. // 取消异步操作
  6. throw new OperationCanceledException();
  7. }
  8. return await task;
  9. }
:ballot_box_with_check:应改成下面这样,这样将在任务完成之后,取消计时器的操作
 
 

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)

{
  1. using (var cts = new CancellationTokenSource())
  2. {
  3. var delayTask = Task.Delay(timeout, cts.Token);
  4. var resultTask = await Task.WhenAny(task, delayTask);
  5. if (resultTask == delayTask)
  6. {
  7. // 取消异步操作
  8. throw new OperationCanceledException();
  9. }
  10. else
  11. {
  12. // 取消计时器任务
  13. cts.Cancel();
  14. }
  15. return await task;
  16. }
  17. }

10.使用StreamWriter(s)或Stream(s)时在Dispose之前建议先调用FlushAsync

当使用Stream和StreamWriter进行异步写入时,底层数据也有可能被缓冲,当数据被缓冲时,Stream和StreamWriter将使用同步的方式进行write/flush,这将会导致线程阻塞,并且有可能导致线程池内线程不足(线程池饥饿)

❌下面例子由于没有调用FlushAsync(),所以最后是以同步方式进行write/flush的

 

public async static Task RunAsync()

{
  1. using (var streamWriter = new StreamWriter(@"C:\资料\Blogs\Task"))
  2. {
  3. // 由于没有调用FlushAsync,所以最后是以同步方式进行write/flush的
  4. await streamWriter.WriteAsync("Hello World");
  5. }
  6. }

☑️所以应该改为下面这样,在Dispose之前调用FlushAsync()

 

public async static Task RunAsync()

{
  1. using (var streamWriter = new StreamWriter(@"C:\资料\Blogs\Task"))
  2. {
  3. await streamWriter.WriteAsync("Hello World");
  4. // 调用FlushAsync() 使其使用异步write/flush
  5. await streamWriter.FlushAsync();
  6. }
  7. }

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

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

  • 异步和同步的异常都被始终被规范为了异步

  • 代码更容易修改(例如:增加一个using)

  • 异步的方法诊断起来更加容易(例如:调试,挂起)

  • 抛出的异常将自动包装在返回的任务之中,而不是抛出实际异常

❌下面这个错误的例子是将Task直接返回给了调用者

 

public Task<int> RunAsync()

{
  1. return Task.FromResult(1 + 1);
  2. }

☑️所以应该使用async/await来代替返回Task

 

public async Task<int> RunAsync()

{
  1. return await Task.FromResult(1 + 1);
  2. }

🔔使用async/await来代替返回Task时,还有性能上的考虑,虽然直接Task会更快,但是最终却改变了异步的行为,失去了异步状态机的一些好处

使用场景

1. 使用定时器回调函数

❌下面例子使用一个返回值为void的异步,将其传递给Timer进行,因此,如果其中任务抛出异常,则整个进程将退出

 

public class Pinger

{
  1. private readonly Timer _timer;
  2. private readonly HttpClient _client;
  3. public Pinger(HttpClient client)
  4. {
  5. _client = new HttpClient();
  6. _timer = new Timer(Heartbeat, null, 1000, 1000);
  7. }
  8. public async void Heartbeat(object state)
  9. {
  10. await httpClient.GetAsync("http://mybackend/api/ping");
  11. }
  12. }

❌下面例子将阻止计时器回调,这有可能导致线程池中线程耗尽,这也是一个异步差于同步的例子

 

public class Pinger

{
  1. private readonly Timer _timer;
  2. private readonly HttpClient _client;
  3. public Pinger(HttpClient client)
  4. {
  5. _client = new HttpClient();
  6. _timer = new Timer(Heartbeat, null, 1000, 1000);
  7. }
  8. public void Heartbeat(object state)
  9. {
  10. httpClient.GetAsync("http://mybackend/api/ping").GetAwaiter().GetResult();
  11. }
  12. }

☑️下面例子是使用基于的异步的方法,并在定时器回调函数中丢弃该任务,并且如果此方法抛出异常,则也不会关闭进程,而是会触发TaskScheduler.UnobservedTaskException事件

 

public class Pinger

{
  1. private readonly Timer _timer;
  2. private readonly HttpClient _client;
  3. public Pinger(HttpClient client)
  4. {
  5. _client = new HttpClient();
  6. _timer = new Timer(Heartbeat, null, 1000, 1000);
  7. }
  8. public void Heartbeat(object state)
  9. {
  10. _ = DoAsyncPing();
  11. }
  12. private async Task DoAsyncPing()
  13. {
  14. // 异步等待
  15. await _client.GetAsync("http://mybackend/api/ping");
  16. }

2.创建回调函数参数时注意避免 async void

假如有BackgroudQueue类中有一个接收回调函数的FireAndForget方法,该方法在某个时候执行调用

❌下面这个错误例子将强制调用者要么阻塞要么使用async void异步方法

 

public class BackgroundQueue

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

☑️所以应该构建一个回调异步方法的重载

 

public class BackgroundQueue

{
  1. public static void FireAndForget(Action action) { }
  2. public static void FireAndForget(Func<Task> action) { }
  3. }

3.使用ConcurrentDictionary.GetOrAdd注意场景

缓存异步结果是一种很常见的做法,ConcurrentDictionary是一个很好的集合,而GetOrAdd也是一个很方便的方法,它用于尝试获取已经存在的项,如果没有则添加项.因为回调是同步的,所以很容易编写Task.Result的代码,从而生成异步的结果值,但是这样很容易导致线程池饥饿

❌下面这个例子就有可能导致线程池饥饿,因为当如果没有缓存人员数据时,将阻塞请求线程

 

public class PersonController : Controller

{
  1. private AppDbContext _db;
  2. private static ConcurrentDictionary<int, Person> _cache = new ConcurrentDictionary<int, Person>();
  3. public PersonController(AppDbContext db)
  4. {
  5. _db = db;
  6. }
  7. public IActionResult Get(int id)
  8. {
  9. // 如果不存在缓存数据,则会进入堵塞状态
  10. var person = _cache.GetOrAdd(id, (key) => db.People.FindAsync(key).Result);
  11. return Ok(person);
  12. }
  13. }

☑️可以改成缓存线程本身,而不是结果,这样将不会导致线程池饥饿

 

public class PersonController : Controller

{
  1. private AppDbContext _db;
  2. private static ConcurrentDictionary<int, Task<Person>> _cache = new ConcurrentDictionary<int, Task<Person>>();
  3. public PersonController(AppDbContext db)
  4. {
  5. _db = db;
  6. }
  7. public async Task<IActionResult> Get(int id)
  8. {
  9. // 因为缓存的是线程本身,所以没有进行堵塞,也就不会产生线程池饥饿
  10. var person = await _cache.GetOrAdd(id, (key) => db.People.FindAsync(key));
  11. return Ok(person);
  12. }
  13. }

🔔这种方法,在最后,GetOrAdd()可能并行多次来执行缓存回调,这可能导致启动多次昂贵的计算

☑️可以使用async lazy模式来取代多次执行回调问题

 

public class PersonController : Controller

{
  1. private AppDbContext _db;
  2. private static ConcurrentDictionary<int, AsyncLazy<Person>> _cache = new ConcurrentDictionary<int, AsyncLazy<Person>>();
  3. public PersonController(AppDbContext db)
  4. {
  5. _db = db;
  6. }
  7. public async Task<IActionResult> Get(int id)
  8. {
  9. // 使用Lazy进行了延迟加载(使用时调用),解决了多次执行回调问题
  10. var person = await _cache.GetOrAdd(id, (key) => new AsyncLazy<Person>(() => db.People.FindAsync(key)));
  11. return Ok(person);
  12. }
  13. private class AsyncLazy<T> : Lazy<Task<T>>
  14. {
  15. public AsyncLazy(Func<Task<T>> valueFactory) : base(valueFactory)
  16. {
  17. }
  18. }

4.构造函数对于异步的问题

构造函数是同步,下面看看在构造函数中处理异步情况

下面是使用客户端API的例子,当然,在使用API之前需要异步进行连接

 

public interface IRemoteConnectionFactory

{
  1. Task<IRemoteConnection> ConnectAsync();
  2. }
  3. public interface IRemoteConnection
  4. {
  5. Task PublishAsync(string channel, string message);
  6. Task DisposeAsync();
  7. }

❌下面例子使用Task.Result在构造函数中进行连接,这有可能导致线程池饥饿和死锁现象

 

public class Service : IService

{
  1. private readonly IRemoteConnection _connection;
  2. public Service(IRemoteConnectionFactory connectionFactory)
  3. {
  4. _connection = connectionFactory.ConnectAsync().Result;
  5. }
  6. }

☑️正确的方式应该使用静态工厂模式进行异步连接

 

public class Service : IService

{
  1. private readonly IRemoteConnection _connection;
  2. private Service(IRemoteConnection connection)
  3. {
  4. _connection = connection;
  5. }
  6. public static async Task<Service> CreateAsync(IRemoteConnectionFactory connectionFactory)
  7. {
  8. return new Service(await connectionFactory.ConnectAsync());
  9. }
  10. }

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

 
 
posted @ 2022-02-23 15:50  江境纣州  阅读(307)  评论(0编辑  收藏  举报