异步编程最佳实践
异步编程在.NET平台上已经存在了好几年,但历史上一直很难做好。自从C# 5中引入async/await之后,异步编程已经成为主流。现代框架(如ASP.NET Core)是完全异步的,在编写Web服务时很难避免使用async关键字。因此,对于async的最佳实践以及如何正确使用它,人们一直有很多困惑。本文将利用代码来说明这种差异
异步有始有终
一旦你采用异步,所有的调用者都应该是异步的,因为除非整个调用栈都是异步的,否则异步的努力毫无意义。在很多情况下,部分异步可能比完全同步更糟糕。因此,最好的办法是想尽办法,一次把所有的操作都做成异步。
❌这个例子使用Task.Result,并作为结果阻塞当前线程等待结果。这是一个同步优势胜过异步的例子。
代码语言:javascript复制public int DoSomethingAsync()
{
var result = CallDependencyAsync().Result;
return result 1;
}
✅本例使用 await 关键字从 CallDependencyAsync 获取结果。
代码语言:javascript复制public async Task<int> DoSomethingAsync()
{
var result = await CallDependencyAsync();
return result 1;
}
谨慎使用Async void
在ASP.NET Core应用程序中使用async void通常都不是很好的选择。避免这样使用。如果抛出异常,Async void方法会使进程崩溃。我们将查看更多导致开发人员在ASP.NET Core应用程序中这样做的模式,但这里有一个简单的例子。
❌Async void方法不能被跟踪,因此未处理的异常会导致应用程序崩溃。
代码语言:javascript复制public class MyController : Controller
{
[HttpPost("/start")]
public IActionResult Post()
{
BackgroundOperationAsync();
return Accepted();
}
public async void BackgroundOperationAsync()
{
var result = await CallDependencyAsync();
DoSomething(result);
}
}
✅ 任务返回方法更好,因为未处理的异常会触发TaskScheduler.UnobservedTaskException。
代码语言:javascript复制public class MyController : Controller
{
[HttpPost("/start")]
public IActionResult Post()
{
Task.Run(BackgroundOperationAsync);
return Accepted();
}
public async Task BackgroundOperationAsync()
{
var result = await CallDependencyAsync();
DoSomething(result);
}
}
对于预先计算或琐碎计算的数据,优先选择Task.FromResult而不是Task.Run。
❌这个例子浪费了一个线程池线程来返回一个琐碎的计算值。
代码语言:javascript复制public class MyLibrary
{
public Task<int> AddAsync(int a, int b)
{
return Task.Run(() => a b);
}
}
✅这个例子使用Task.FromResult来返回微不足道的计算值,它没有使用任何额外的线程作为结果。它没有使用任何额外的线程作为结果。
注意:使用Task.FromResult将导致一个任务分配。使用ValueTask<T>可以完全删除该分配。
✅这个例子使用ValueTask<int>来返回琐碎计算的值。结果,它没有使用任何额外的线程。它也没有在托管堆上分配一个对象。
代码语言:javascript复制public class MyLibrary
{
public ValueTask<int> AddAsync(int a, int b)
{
return new ValueTask<int>(a b);
}
}
避免使用Task.Run进行长时间运行的工作,阻塞线程。
这里的长运行工作指的是一个线程,它在应用程序的生命周期中一直在运行,做后台工作。Task.Run会将一个工作项排队到线程池中。假设该工作会很快完成(或快到允许在某个合理的时间范围内重复使用该线程)。为长期运行的工作取一个线程池线程是不好的,因为它占用了该线程与其他可以完成的工作(定时器回调、任务延续等)。相反,手动生成一个新的线程来做长期运行的阻塞工作。
注:如果你阻塞线程,线程池会不断增大,但这样做是不好的做法。
注:Task.Factory.StartNew有个选项TaskCreationOptions.LongRunning,在后台创建一个新线程并返回一个表示执行的Task。正确地使用它需要传入几个不明显的参数,以在所有平台上获得正确的行为
注:不要在async代码中使用TaskCreationOptions.LongRunning,因为这会创建一个新的线程,而这个线程会在第一次 await之后被销毁。
❌这个例子永远获取一个线程池的线程,为了在BlockingCollection<T>上执行队列工作。
代码语言:javascript复制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) { }
}
✅这个例子使用一个专用线程来处理消息队列,而不是线程池线程。
代码语言:javascript复制public class QueueProcessor
{
private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
public void StartProcessing()
{
var thread = new Thread(ProcessQueue)
{
// This is important as it allows the process to exit while this thread is running
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) { }
}