异步编程指南

异步编程具有传染性

原文:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#warning-sync-over-async

一旦采用异步编程模型,所有调用者应该也是异步的。因为只有整个调用链都采用异步编程模型,才能充分发挥异步编程的优势。在很多情况下,部分异步的效果甚至不如完全同步。因此,最好一次性将所有内容都改成异步编程模型。

❌ BAD 这个例子使用了Task.Result,导致当前线程被阻塞以等待结果。.

public int DoSomethingAsync()
{
    var result = CallDependencyAsync().Result;
    return result + 1;
}

✅ GOOD 这个例子使用了await关键字来等待调用CallDependencyAsync方法的结果。

public async Task<int> DoSomethingAsync()
{
    var result = await CallDependencyAsync();
    return result + 1;
}

Async void

在ASP.NET Core应用程序中使用async void永远都是不好的,应该尽量避免。通常情况下,开发者会在控制器操作触发请求后立即返回,不需要等待响应 时使用Async Void方法。但是,如果出现异常,则会导致整个进程崩溃。

❌ BAD Async Void方法无法被追踪,因此未处理的异常可能会导致应用程序崩溃。

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        BackgroundOperationAsync();
        return Accepted();
    }
    
    public async void BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
} 

✅ GOOD 使用返回任务(Task)的方法更好,因为未处理的异常会触发TaskScheduler.UnobservedTaskException事件。

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        Task.Run(BackgroundOperationAsync);
        return Accepted();
    }
    
    public async Task BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
}
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
    // 记录未处理的任务异常
    foreach (var exception in args.Exception.InnerExceptions)
    {
        logger.LogError(exception, "Unobserved task exception occurred.");
    }
   
    // 标记异常已处理
    args.SetObserved();
};

 

对于预先计算或者计算非常简单的数据,应优先考虑使用Task.FromResult而不是Task.Run。

Task.FromResult方法是一个静态方法,用于创建一个已经完成的Task对象,并将结果作为返回值封装在Task中。由于Task已经完成,因此当等待Task对象时,将立即返回结果,而不需要开启新的线程。

相比之下,Task.Run方法会在ThreadPool上启动一个新的任务,并且该任务的执行需要时间和资源。对于预先计算或者计算非常简单的数据,使用Task.Run来启动这样的任务可能是浪费资源的。

❌ BAD 这个示例浪费了一个线程池线程来返回一个计算非常简单的值。

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.Run(() => a + b);
   }
} 

✅ GOOD 这个示例使用Task.FromResult来返回一个计算非常简单的值。由于不需要额外的线程,因此它不会占用任何多余的系统资源。

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.FromResult(a + b);
   }
}

 ✅ GOOD在上个示例中,我们使用Task.FromResult创建一个Task<int>对象来返回计算结果,但是这会分配一个额外的对象(Task)。现在,我们可以使用ValueTask<int>来改善这个方法。在这个示例中,我们返回了一个ValueTask<int>对象,它不需要分配任何Task对象。 此外,当这个方法被同步调用时,它也可以提高性能,因为它不需要等待Task对象被调度和执行,它可以直接返回封装在ValueTask<int>中的结果。这个改进对于高性能应用程序非常有用。

public class MyLibrary
{
   public ValueTask<int> AddAsync(int a, int b)
   {
       return new ValueTask<int>(a + b);
   }
}

 

 在执行需要占用很长时间的工作时,尽可能避免使用Task.Run方法。

 Task.Run方法是用于将一个操作分配到线程池上的异步方法,它通常用于在后台线程上执行短时间运行的非阻塞操作。但是,如果您需要执行一个需要长时间运行的操作,而且该操作会阻塞线程,则使用Task.Run可能会导致一些问题。这是因为它会占用线程池中有限的线程资资源,从而影响应用程序的响应性能。

如果阻塞线程,线程池会增长,但是这是一种不好的编程实践。

Task.Factory.StartNew 方法是一个强大的工具,可用于在多线程应用程序中以异步方式执行代码。TaskCreationOptions.LongRunning 选项可以指示方法使用一个长时间运行的线程来执行任务,从而避免占用线程池中的宝贵资源。但是,要正确使用此选项,需要考虑多种参数和配置。

不要在异步代码中使用TaskCreationOptions.LongRunning选项,因为这样会创建一个新的线程,在第一次 await 后就会被销毁。

 

应避免使用 Task.Result 和 Task.Wait。

在异步编程中,Task.Result 和 Task.Wait 方法可以用于等待任务完成,并返回其结果。但是,这种做法可能会导致应用程序死锁,因为它会阻塞当前线程并等待任务完成,而另一个任务或系统资源可能正在等待该线程释放。

相反,应该优先使用 await 操作符来等待任务完成。await 操作符可以暂停当前方法的执行并允许其他代码在该方法的上下文中运行,从而提高应用程序的响应性和并发性。此外,await 操作符也可以将异常传播回调用者,以便更好地处理错误情况。

如果确实需要等待任务完成而无法使用 await 操作符,则应尽量避免在 UI 线程或 ASP.NET 应用程序中使用 Task.Wait 或 Task.Result 方法。这些方法可能会导致应用程序出现死锁、线程池饱和或性能下降的问题。取而代之,可以考虑使用 Task.ConfigureAwait(false) 将等待操作切换到后台线程,并指定 CancellationToken 以避免无限期地等待任务完成。

总之,在异步编程中,应该优先使用 await 操作符来等待任务完成,并避免使用 Task.Result 和 Task.Wait 方法,以提高应用程序的可靠性和性能。

❌ BAD

public string DoOperationBlocking()
{
    // Bad - Blocking the thread that enters.
    // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
    // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
    return Task.Run(() => DoAsyncOperation()).Result;
}

public string DoOperationBlocking2()
{
    // Bad - Blocking the thread that enters.
    // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
    // In the case of an exception, this method will throw the exception without wrapping it in an AggregateException.
    return Task.Run(() => DoAsyncOperation()).GetAwaiter().GetResult();
}

public string DoOperationBlocking3()
{
    // Bad - Blocking the thread that enters, and blocking the threadpool thread inside.
    // In the case of an exception, this method will throw an AggregateException containing another AggregateException, containing the original exception.
    return Task.Run(() => DoAsyncOperation().Result).Result;
}

public string DoOperationBlocking4()
{
    // Bad - Blocking the thread that enters, and blocking the threadpool thread inside.
    return Task.Run(() => DoAsyncOperation().GetAwaiter().GetResult()).GetAwaiter().GetResult();
}

public string DoOperationBlocking5()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
    return DoAsyncOperation().Result;
}

public string DoOperationBlocking6()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    return DoAsyncOperation().GetAwaiter().GetResult();
}

public string DoOperationBlocking7()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    var task = DoAsyncOperation();
    task.Wait();
    return task.GetAwaiter().GetResult();
}

 

在使用超时的 CancellationTokenSource 时,应该始终在使用后将其释放(Dispose),以避免资源泄露。

using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
    // perform long-running operation here
    // and check for cancellation if possible:
    while (!cts.Token.IsCancellationRequested)
    {
        // do some work here...
    }
}

在使用 CancellationToken 来取消操作时,应该始终将令牌传递给 API,以确保可以正确地取消操作。

CancellationToken 是用于取消操作的一个标准机制。当操作正在执行时,如果取消令牌被请求,则可以使用 IsCancellationRequested 属性检查令牌是否已被取消,并相应地停止该操作。

许多 .NET 标准库中的方法和 API 都支持 CancellationToken 参数,以便在取消操作时使用。如果不传递 CancellationToken 到这些 API,则可能会导致无法正确取消操作,从而影响应用程序的性能。

using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync("https://example.com", cts.Token);
        // process the response here...
    }
}

 

 

 

 


posted @ 2023-06-01 01:17  广州大雄  阅读(47)  评论(0编辑  收藏  举报