C# 任务(Task) ConfigureAwait的使用


默认情况下,当您使用async/await时,它将在开始请求的原始线程上继续运行(状态机)。

但是,如果当前另一个长时间运行的进程已经接管了该线程,那么你就不得不等待它完成。要避免这个问题,可以使用ConfigureAwait的方法和false参数。当你用这个方法的时候,这将告诉Task它可以在任何可用的线程上恢复自己继续运行,而不是等待最初创建它的线程。这将加快响应速度并避免许多死锁。

但是,这里有一点点损失。当您在另一个线程上继续时,线程同步上下文将丢失,因为状态机改变。这里最大的损失是你会失去归属于线程的Culture和Language,其中包含了国家语言时区信息,以及来自原始线程的HttpContext.Current之类的信息,因此,如果您不需要以此来做多语系或操作任何HttpContext类型设置,则可以安全地进行此方法的调用。注意:如果需要language/culture,可以始终在await之前存储当前相关状态值,然后在await新线程之后重新应用它。

ConfigureAwait 的作用

ConfigureAwait(false) 方法用于指定是否将当前上下文捕获到任务中。默认情况下,ConfigureAwait 设置为 true,这意味着当前上下文(例如同步上下文或任务调度程序)将捕获到任务中。

设置 ConfigureAwait(false) 的好处:

  • **提高性能:**避免将当前上下文捕获到任务中可以提高性能,因为不需要在不同的上下文之间进行切换。
  • **避免死锁:**在某些情况下,将当前上下文捕获到任务中可能会导致死锁。例如,如果你在 UI 线程上调用 await,并且 UI 线程被阻塞,那么任务将永远无法完成。

不设置 ConfigureAwait(false) 的好处:

  • **简化调试:**如果当前上下文捕获到任务中,则在调试时更容易跟踪任务的执行。
  • **避免意外行为:**在某些情况下,不捕获当前上下文可能会导致意外行为。例如,如果你在后台线程上调用 await,并且后台线程退出,那么任务将被取消。

何时使用 ConfigureAwait(false)

通常,建议在以下情况下使用 ConfigureAwait(false)

  • 在性能至关重要的场景中。
  • 在可能导致死锁的场景中。
  • 在不需要在不同的上下文之间进行切换的场景中。

何时不使用 ConfigureAwait(false)

通常,建议在以下情况下不使用 ConfigureAwait(false)

  • 在调试场景中。
  • 在需要在不同的上下文之间进行切换的场景中。
  • 在可能导致意外行为的场景中。

示例

以下是如何使用和不使用 ConfigureAwait(false) 的示例:

// 使用 ConfigureAwait(false)
Task task = Task.Run(async () =>
{
    await Task.Delay(1000).ConfigureAwait(false);
    Console.WriteLine("Task completed.");
});

// 不使用 ConfigureAwait(false)
Task task = Task.Run(async () =>
{
    await Task.Delay(1000);
    Console.WriteLine("Task completed.");
});

 

在第一个示例中,ConfigureAwait(false) 用于避免将 UI 线程的上下文捕获到任务中。这可以提高性能并避免死锁。

在第二个示例中,没有使用 ConfigureAwait(false)。这使得在调试时更容易跟踪任务的执行,但可能会导致性能下降或死锁。

注意事项

如果有同步方法调用异步方法,则必须使用ConfigureAwait(false)。如果不这样做,就会立即掉进死锁陷阱。

发生的情况是主线程将调用async方法,最终会阻塞这个线程,直到那个async方法完成。然而,一旦异步方法完成,它必须等待原始调用者完成后才能继续。他们都在等待对方完成,而且永远不会。通过在调用中使用configurewait (false), async方法将能够在另一个线程上完成自己操作,而不关心自己的状态机的位置,并通知原始线程它已经完成。

死锁举例

 public class HomeController : Controller
    {
        public  ActionResult Index()
        {
             DoAsync().Wait();//同步调用1,发生死锁
             //var r = DoAsync().Result;//同步调用2,发生死锁
             return View();
        }
 
        public async Task<int> DoAsync()
        {
            await Task.Delay(2000).ConfigureAwait(true);//默认就是ture
            return 1;
        }
    }

复制代码
 public class HomeController : Controller
    {
        public  ActionResult Index()
        {
             DoAsync().Wait();//同步调用1,发生死锁
             //var r = DoAsync().Result;//同步调用2,发生死锁
             return View();
        }
 
        public async Task<int> DoAsync()
        {
            await Task.Delay(2000).ConfigureAwait(true);//默认就是ture
            return 1;
        }
    }
复制代码

探讨.NetCore中异步注意事项

在.NetCore中已经剔除了SynchronizationContext,剔除他的主要原因主要是性能和进一步简化操作

在.NetCore中我们不用继续关心异步同步混用情况下,是否哪里没有设置ConfigureAwait(false) 会导致的死锁问题,因为在.netcore中的async/await 可能在任何线程上执行,并且可能并行运行!

如下代码,在旧版ASP.NET(.NetFramework)中工作正常,而ASP.NET Core上不是线程安全的

public class HomeController : Controller
    {
        public async Task<ActionResult> Index()
        {            
            var result = await GetBothAsync();
            return View();
        }
        async Task<List<string>> GetBothAsync()
        {
            var result = new List<string>();
            var task1 = GetOneAsync(result);
            var task2 = GetOneAsync(result);
            await Task.WhenAll(task1, task2);
            return result;
        }
        async Task GetOneAsync(List<string> result)
        {
            await Task.Delay(2000);
            for (int i = 0; i < 10 * 10 * 10 * 10 * 10; i++)
            {
                result.Add(i.ToString());
            }
        }
}

复制代码
public class HomeController : Controller
    {
        public async Task<ActionResult> Index()
        {            
            var result = await GetBothAsync();
            return View();
        }
        async Task<List<string>> GetBothAsync()
        {
            var result = new List<string>();
            var task1 = GetOneAsync(result);
            var task2 = GetOneAsync(result);
            await Task.WhenAll(task1, task2);
            return result;
        }
        async Task GetOneAsync(List<string> result)
        {
            await Task.Delay(2000);
            for (int i = 0; i < 10 * 10 * 10 * 10 * 10; i++)
            {
                result.Add(i.ToString());
            }
        }
}
复制代码

此代码在旧版ASP.NET(.NetFramework)中工作正常,由于请求处设置了await,请求上下文一次只允许一个连接.

其中result.Add(data)一次只能由一个线程执行,因为它在请求上下文中执行。(可以理解为在源线程执行,是吧?QAQ)

但是,这个相同的代码在ASP.NET Core上是不安全的; 具体地说,该result.Add(data)行可以由两个线程同时执行,而不保护共享List<string>

所以在.Netcore中要特别注意异步代码在并行执行情况下引发的问题

posted @ 2022-11-07 19:07  卖雨伞的小男孩  阅读(253)  评论(0编辑  收藏  举报