异步编程最佳实践

2022-04-27 15:55:06 浏览数 (1)

异步编程最佳实践

异步编程在.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) { }
}

0 人点赞