Может кто-нибудь объяснить это тупиковое поведение? - PullRequest
1 голос
/ 23 сентября 2019

TLDR: в моем примере (ASP.NET) ниже, почему task1.Result и task2.Result приводят к взаимоблокировке, а task3.Result не приводит?

В нашем коде есть класс, где яне может легко пометить метод как асинхронный и предотвратить блокировку кода в ожидании завершения асинхронной задачи.Причина в том, что наша структура не поддерживает это.Поэтому я попытался найти решение задачи. В результате получил несколько тупиков.К счастью, я нашел решение (см. Task3).Теперь я попытался выяснить, в чем разница между task1, task2 и task3, чтобы понять, почему первые два приводят к тупику, а третий - нет.

С помощью отладчика я не увидел никакой разницы, как task3выполняется до вызова task3.Result.Он все еще находится в состоянии WaitingForActivation, как и два других.Кто-нибудь может мне объяснить, как может работать task3?

    public class HomeController : Controller
    {
        public ActionResult GetSomething()
        {
            var task1 = GetSomethingAsync();
            var task2 = Task.Run(async () => await task1);
            var task3 = Task.Run(async () => await GetSomethingAsync());
            return Content(task1.Result);
//            return Content(task2.Result);
//            return Content(task3.Result);
        }

        private static async Task<string> GetSomethingAsync()
        {
            return await Task.Run(() => "something");
        }
    }

Ответы [ 2 ]

2 голосов
/ 23 сентября 2019

Корень проблемы здесь await:

private static async Task<string> GetSomethingAsync()
{
    return await Task.Run(() => "something");
}

Контекст фиксируется перед входом в ожидание, и контекст является основным потоком.Поэтому после ожидания контекст должен быть восстановлен, поэтому продолжение планируется запустить в главном потоке.Так как основной поток заблокирован в этот момент, он не может обработать продолжение, следовательно, тупик.

Чтобы предотвратить захват контекста, вы можете настроить этот await с помощью ConfigureAwait(false).


Обновление: Под продолжение Я имею в виду код внутри GetSomethingAsync, который следует после ожидания.Хотя нет никакого кода после этого, кажется, что компилятор не удосужился создать фактическое продолжение этого не-оп части методы (в противном случае тупиковый не должен происходить в вашем примере).

1020 * Должно бытьотметил, что компилятор преобразует метод async в Task, который состоит из нескольких мини-задач.Каждый await, встречающийся на пути выполнения, вызывает создание мини-задачи, которая является продолжением ожидаемой задачи.Все эти мини-задачи выполняются одна за другой, и завершение последней сигнализирует о завершении «мастер-задачи», возвращаемой методом async.
1 голос
/ 23 сентября 2019

Что бы ни делал GetSomethingAsync(), это делается вызывающим потоком до тех пор, пока некоторая операция (OP) не может быть завершена сразу (например, io), после чего поток управления передается вызывающей функции, но.Если затем вы получите доступ к свойству Result возвращенного объекта Task, поток будет заблокирован.

Это приводит к проблеме, заключающейся в том, что даже после завершения операции поток не узнает об этом, потому что он занят ожиданием завершения Task

Если, однако, вы позволите GetSomethingAsync() может быть вызван каким-либо потоком пула потоков (что делает Task.Run(...)) поток пула потоков может завершить OP, и вызывающий поток может быть уведомлен о завершении Task.

Обновление:

Ваш второй подход не работает, потому что задача все еще была запущена в главном потоке.Если у вас есть этот метод

public static async Task DoStuffAsync()
{
    Console.WriteLine($"Doing some stuff1 on thread {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(50);
    Console.WriteLine($"Doing some stuff2 on thread {Thread.CurrentThread.ManagedThreadId}");
}

и вы запустите этот код в приложении с SynchronizationContext

var task = DoStuffAsync();
Console.WriteLine($"Doing main stuff on thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Run(async () => await task);

Он выведет что-то вроде:

Doing some stuff1 on thread 1
Doing main stuff on thread 1
Doing some stuff2 on thread 1

Таким образом, с помощью строки кода Task.Run(async () => await task) вы добились только того, что поток пула потоков ожидает завершения вашего исходного Task, но это, в свою очередь, создает новый Task, который, если его не обработать, ожидая его, вызывает тупик.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...