在今天来看,异步编程已经不是什么新鲜玩意了。但从过去编程的方式来看在 .Net 中想要使用异步并不是一件容易的事情。但在C# 5.0引入了 async 和 await 关键字之后,异步编程已经成为了主流。在现代的框架上,比如 .Net Core 上则是完全异步的。在写Web服务的时候往往很难避免使用 async 。因此很多开发人员对异步的最佳实现和如何正确使用异步存在很多困扰。文章《.Net异步编程进阶》将会分开多部分,逐步为大家讲解.Net异步编程的进阶技巧,本节我将要列出一些正确和错误的实例,以便让大家在日后的异步编程中更好的规避错误的用法。如果大家发现有描述的不准确的地方,欢迎评论指正。
异步具有传染性
异步是有传染性的,一旦你一个函数使用了异步,那么接下来所有调用这个函数的函数也都 应该 使用异步。因为除非整个调用栈都是异步的,否则这个异步则是没有任何意义。在很多情况下,如果中途堵塞进程强制把异步转为同步,甚至比完全同步更糟糕,所以最好是把整个调用栈都做成异步。
使用 Task.Result 阻塞当前线程等待异步结果,这是将异步强制转为同步。
复制代码
1 public int AppendValueAsync()
2 {
3 int result = GetValueAsync().Result;
4 return result + 1;
5 }
复制代码
使用 await 关键字获取值
复制代码
1 public async Task AppendValueAsync()
2 {
3 int result = await GetValueAsync();
4 return result + 1;
5 }
复制代码
避免让异步函数使用void作为返回类型
在 Asp.Net Core下使用 void 作为异步返回类型,是种坏习惯。尽量避免这样做 ,因为如果调用的函数一旦抛出异常,并且开发人员没有配置其他异常呈报策略,在默认的情况下,会终止进程。
无法追踪的 async void 函数,未经处理的异常会直接让程序崩溃。
复制代码
1 public class MyController : Controller
2 {
3 [HttpPost("/start")]
4 public IActionResult Post()
5 {
6 BackgroundOperationAsync();
7 return Accepted();
8 }
9
10 private async void BackgroundOperationAsync()
11 {
12 var result = await CallDependencyAsync();
13 DoSomething(result);
14 }
15 }
复制代码
使用 Task 类型返回值,未经处理的异常会触发TaskScheduler.UnobservedTaskException
复制代码
1 public class MyController : Controller
2 {
3 [HttpPost("/start")]
4 public IActionResult Post()
5 {
6 Task.Run(BackgroundOperationAsync);
7 return Accepted();
8 }
9
10 private async Task BackgroundOperationAsync()
11 {
12 var result = await CallDependencyAsync();
13 DoSomething(result);
14 }
15 }
复制代码
预计算简单运算的结果
对于一些简单的计算,不应该使用 Task.Run,因为这需要创建一个异步任务入列线程池的等待执行然后返回运算结果。相反,使用 Task.FromResult 则是创建一个已经完成计算已经有结果的任务。
这个是例子浪费了线程池里面的一个线程来计算一个简单计算的值。
复制代码
1 public class Calculator
2 {
3 public Task AddAsync(int x, int y)
4 {
5 return Task.Run(() => x + y);
6 }
7 }
复制代码
使用 Task.FromResult 去返回一些简单计算的值,这样不会使用额外的线程,而且这样也不需要在CLR托管的堆上额外申请内存空间存放异步任务的对象。
复制代码
1 public class Calculator
2 {
3 public Task AddAsync(int x, int y)
4 {
5 return Task.FromResult(x + y);
6 }
7 }
复制代码
注意:使用 Task.FromResult 虽然不会额外占用线程,但依然会创建一个异步任务,使用ValueTask则完全可以避免创建这个多余的任务。
使用 ValueTask 返回简单运算结果,不但不会额外占用线程,也不会在CLR的堆上额外分配空间创建多余的任务对象。
复制代码
1 public class Calculator
2 {
3 public ValueTask AddAsync(int a, int b)
4 {
5 return new ValueTask(a + b);
6 }
7 }
复制代码
避免使用Task.Run来执行需要长时间运行的阻塞工作
这里说的长时间工作是指贯穿应用程序整个生命周期后台执行的工作(如休眠等待一定时间再次唤醒处理数据,处理消息队列里的数据等等)。Task.Run 会把任务入列线程池,如果这个工作是能够快完成又或者说在合理的时间内完成的话,这个线程则会得到复用。但如果这是一个长时间运行的阻塞工作,则会一直占用该线程。因此需要手动分配一个新的线程去执行需要长时间运行的阻塞工作。
注意:如果阻塞了线程池里的线程,线程池里会增加大量的线程,导致大量的上下文切换,从而拖慢应用程序的整体性能。
注意:Task.Factor.StartNew 有一个选项 TaskCreatetionOptions.LongRunning 它会在后台创建一个新的线程去执行长时间运行的任务。
这个例子,永远占用着线程池里的一个线程去处理消息队列
复制代码
1 public class QueueProcessor
2 {
3 private readonly BlockingCollection _messageQueue = new BlockingCollection();
4
5 public void StartProcessing()
6 {
7 Task.Run(ProcessQueue);
8 }
9
10 public void Enqueue(Message message)
11 {
12 _messageQueue.Add(message);
13 }
14
15 private void ProcessQueue()
16 {
17 foreach (var item in _messageQueue.GetConsumingEnumerable())
18 {
19 ProcessItem(item);
20 }
21 }
22
23 private void ProcessItem(Message message) { }
24 }
复制代码
使用专用的线程来处理队列,而不是线程池里的线程。
复制代码
1 public class QueueProcessor
2 {
3 private readonly BlockingCollection _messageQueue = new BlockingCollection();
4
5 public void StartProcessing()
6 {
7 var thread = new Thread(ProcessQueue)
8 {
9 // 这选项很重要,CLR 会在进程终止的时候对每一个活在后台的线程调用Abort,来彻底终止应用程序
10 IsBackground = true
11 };
12 thread.Start();
13 }
14
15 public void Enqueue(Message message)
16 {
17 _messageQueue.Add(message);
18 }
19
20 private void ProcessQueue()
21 {
22 foreach (var item in _messageQueue.GetConsumingEnumerable())
23 {
24 ProcessItem(item);
25 }
26 }
27
28 private void ProcessItem(Message message) { }
29 }
复制代码https://www.cnblogs.com/zigit/p/9930290.html