Попытка понять Task.Run + asyn c + Wait () / Result - PullRequest
2 голосов
/ 04 мая 2020

Я пытаюсь понять, как Task.Run + Wait () + asyn c + ожидают работы.
Я прочитал эту страницу: Понимание использования Task.Run + Wait () + asyn c + ждут в одной строке , но не очень хорошо понимают.

В моем коде я получаю события от Microsoft EventHub и обрабатываю их, используя класс, реализующий IEventProcessor. Я вызываю метод DoAnotherWork(), который является методом async, в ConvertToEntity(), который является методом syn c. Так как метод 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() (метод вызывался в * 1045 Основные методы и взаимоблокировки веб-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)...
}

1 Ответ

3 голосов
/ 04 мая 2020

В чем разница между ними?

Task.Run начинает выполнение делегата в потоке пула потоков; прямой вызов метода запускает запуск делегата в текущем потоке .

При изучении 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();

Какой код является подходящий и / или безопасный (= не вызовет взаимоблокировку)?

Вам следует избегать Task.Run в среде ASP. NET, поскольку это будет мешать с обработкой ASP. NET пула потоков и принудительным переключением потоков, когда ничего не требуется.

В какой ситуации возникает тупик, если он есть?

Сценарий общего тупика требует двух вещей:

  1. Код, который блокирует асинхронный код вместо правильного использования await.
  2. Контекст, который обеспечивает синхронизация (т. е. позволяет только один блок кода "в" контексте за один раз).

The лучшее решение - убрать первое условие; другими словами, используйте "async полностью" . Чтобы применить это здесь, лучшим решением будет полное снятие блокировки:

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 Ядро вообще не имеет "контекста" , поэтому оно использует контекст пула потоков. Поскольку. NET Ядро не имеет контекста, оно удаляет второе условие тупика, и тупик не возникает. Если вы выполняете это в ASP. NET Базовом проекте.

Моя команда несколько раз сталкивалась с проблемами взаимоблокировки, когда мы использовали AbcAsyn c () .Result или .Wait () (метод вызывался в NET Базовых методах веб-API и взаимоблокировки возникали в основном при запуске модульных тестов, выполняющих метод)

Некоторые инфраструктуры модульных тестов do предоставляет контекст - прежде всего xUnit. Контекст, предоставляемый xUnit , является синхронизирующим контекстом, поэтому он действует скорее как контекст пользовательского интерфейса или ASP. NET pre-Core контекст. Поэтому, когда ваш код выполняется в модульном тесте, у него есть второе условие для взаимоблокировки, и может произойти взаимоблокировка.

Как отмечалось выше, лучшее решение состоит в том, чтобы удалить блокировка полностью; это будет иметь приятный побочный эффект от повышения эффективности вашего сервера. Но если блокировка должна быть выполнена, вам следует заключить код unit-test в Task.Run, а не ASP. NET код ядра.

...