【问题标题】:Trying to Understand Task.Run + async + Wait()/Result试图理解 Task.Run + async + Wait()/Result
【发布时间】:2020-05-04 11:39:03
【问题描述】:

我正在尝试了解 Task.Run + Wait() + async + await 的工作原理。
我已阅读此页面:Understanding the use of Task.Run + Wait() + async + await used in one line 但不太了解。

在我的代码中,我从 Microsoft EventHub 接收事件并使用实现 IEventProcessor 的类来处理它们。 我在ConvertToEntity()中调用DoAnotherWork()方法,这是一个async方法,这是一个同步方法。 由于方法是async,所以我使用Task.Run()async进行委托。 (即Task.Run(async () => entities = await DoAnotherWork(entities)).Wait()
该代码已经运行了一段时间,但现在我的团队成员删除了Task.Run() 并将其更改为DoAnotherWork(entities).Result;。我不确定这会不会导致死锁。
我没有问他为什么要更改它,但这个更改让我思考“我的代码好吗?为什么?”。

我的问题是:
* 两者有什么区别?
* 哪个代码合适和/或安全(= 不会导致死锁)?
* 如果是,在什么情况下会导致死锁?
* 为什么Task.Run() 解决了我的死锁? (详情见下文)

注意:我使用的是 .NET Core 3.1。

我为什么使用 Task.Run()
我的团队在使用 AbcAsync().Result.Wait() 时遇到过几次死锁问题(该方法在 NET Core Web API 方法中调用,死锁主要发生在我们运行执行该方法的单元测试时),所以我们使用Task.Run(async () => await AbcAsync()).Wait()/Result 从那时起我们没有看到任何死锁问题。
但是,此页面:https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d 表示,在某些情况下,延迟会导致死锁。

public class EventProcessor : IEventProcessor
{
    public async Task ProcessEventsAsync(PartitionContext context, IEnumerable<EventData> messages) 
    {
        ...
        var result = await eventHandler.ProcessAsync(messages);
        ...
    }
}

public Task async ProcessAsync(IEnumerable<EventData> messages)
{
    ...
    var entities = ConvertToEntity(messages);
    ...
}

public List<Entity> ConvertToEntity(IEnumerable<EventData> messages)
{
    var serializedMessages = Serialize(messages);
    var entities = autoMapper.Map<Entity[]>(serializedMessages);

    // Task.Run(async () => entities = await DoAnotherWork(entities)).Wait(); // before change
    entities = DoAnotherWork(entities).Result; // after change

    return entities;
}    

public Task async Entity[] DoAnotherWork(Entity[] entities)
{
    // Do stuff async
    await DoMoreStuff(entities)...
}

【问题讨论】:

  • 另外,顺便说一下,死锁通常发生在两个线程最终等待同一资源或彼此等待,而两个线程之一永远不会完成或向前移动时。
  • Joe Albahari 出色且免费的eBook on Threading in C# 非常详细地解释了所有这些。

标签: c# asynchronous task


【解决方案1】:

两者有什么区别?

Task.Run 开始在线程池线程上运行委托;直接调用方法starts running the delegate on the current thread

在学习async 时,将所有内容分开会很有帮助,这样您就可以准确地看到发生了什么:

entities = DoAnotherWork(entities).Result;

相当于:

var entitiesTask = DoAnotherWork(entities);
entities = entitiesTask.Result;

还有这段代码:

Task.Run(async () => entities = await DoAnotherWork(entities)).Wait();

相当于:

async Task LambdaAsMethod()
{
  entities = await DoAnotherWork(entities);
}
var runTask = Task.Run(LambdaAsMethod);
runTask.Wait();

哪个代码合适和/或安全(= 不会导致死锁)?

You should avoid Task.Run 在 ASP.NET 环境中,因为它会干扰 ASP.NET 对线程池的处理,并在不需要时强制线程切换。

如果是死锁,在什么情况下会导致?

common deadlock scenario 需要两件事:

  1. 阻塞异步代码而不是正确使用await的代码。
  2. 强制同步的上下文(即,一次只允许一个代码块“进入”上下文)。

最好的解决办法是去掉第一个条件;换句话说,使用"async all the way"。要在此处应用它,最好的解决方案是完全消除阻塞:

public Task async ProcessAsync(IEnumerable<EventData> messages)
{
    ...
    var entities = await ConvertToEntityAsync(messages);
    ...
}

public async Task<List<Entity>> ConvertToEntityAsync(IEnumerable<EventData> messages)
{
    var serializedMessages = Serialize(messages);
    var entities = autoMapper.Map<Entity[]>(serializedMessages);

    entities = await DoAnotherWork(entities);

    return entities;
}

为什么 Task.Run() 解决了我的死锁? (详见下文)

.NET Core does not have a "context" at all,所以它使用线程池上下文。由于 .NET Core 没有上下文,它消除了死锁的第二个条件,死锁不会发生。 如果您在 ASP.NET Core 项目中运行它。

我的团队在使用 AbcAsync().Result 或 .Wait() 时遇到过几次死锁问题(该方法在 NET Core Web API 方法中被调用,死锁主要发生在我们运行执行该方法的单元测试时)

一些单元测试框架确实提供了一个上下文 - 最著名的是 xUnit。 xUnit 提供的上下文 是一个同步上下文,因此它的行为更像是 UI 上下文或 ASP.NET pre-Core 上下文。所以当你的代码在单元测试中运行时,确实存在死锁的第二个条件,死锁是有可能发生的。

如上所述,最好的解决方案是完全消除阻塞;这将具有使您的服务器更高效的良好副作用。但是如果阻塞必须完成,那么你应该将你的单元测试代码包装在Task.Run中,而不是你的ASP.NET Core代码。

【讨论】:

    猜你喜欢
    • 2017-01-30
    • 2021-05-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-04
    • 1970-01-01
    相关资源
    最近更新 更多