Нет продолжения с использованием захваченного графического интерфейса, но почему существует тупик? - PullRequest
0 голосов
/ 21 апреля 2019

Мне интересно, почему в следующем случае существует тупик, в котором нет продолжения, использующего захваченный контекст графического интерфейса.

public Form1()
{
    InitializeComponent();
    CheckForIllegalCrossThreadCalls = true;
}

async Task DelayAsync()
{
    // GUI context is captured here (right before the following await)
    await Task.Delay(3000);//.ConfigureAwait(false);
    // As no  code follows the preceding await, there is no continuation that uses the captured GUI context. 
}

private async void Button1_Click(object sender, EventArgs e)
{
    Task t = DelayAsync();

    t.Wait();
}

Редактировать:

Я знаю, что тупик может быть решен любым

  • с использованием await Task.Delay(3000).ConfigureAwait(false); или
  • с заменой t.Wait(); на await t;.

Но это не вопрос.Вопрос

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

1 Ответ

4 голосов
/ 21 апреля 2019

TL; DR: async работает с ожидающими, а не с заданиями.Из-за этого в конце метода требуется дополнительный бит логики для перевода статуса ожидающего в задачу.


Ваше предположение, что продолжения нет, неверно.Было бы верно, если бы вы только что вернули задачу:

Task DelayAsync()
{
    return Task.Delay(3000);
}

Однако все становится сложнее, когда вы помечаете метод как async.Одним важным свойством метода async является способ обработки исключений.Рассмотрим эти методы, например:

Task NoAsync()
{
    throw new Exception();
}

async Task Async()
{
    throw new Exception();
}

Что теперь произойдет, если вы вызовете их?

var task1 = NoAsync(); // Throws an exception
var task2 = Async(); // Returns a faulted task

Разница в том, что асинхронная версия переносит исключение в возвращаемую задачу.

Какое это имеет отношение к нашему случаю?

Когда вы await метод, компилятор фактически вызывает GetAwaiter() для объекта, который вы ожидаете.Ожидающий определяет 3 члена:

  • Свойство IsCompleted
  • Метод OnCompleted
  • Метод GetResult

Как вы можете видеть, нет члена, возвращающего исключение.Как узнать, виноват ли виновник?Чтобы знать это, вам нужно вызвать метод GetResult, который вызовет исключение.

Вернемся к вашему примеру:

async Task DelayAsync()
{
    await Task.Delay(3000);
}

Если Task.Delay выдает исключение, async машина должна установить статус возвращенной задачи как неисправной.Чтобы узнать, выдало ли Task.Delay исключение, необходимо вызвать GetResult на приемнике после завершения Task.Delay.Следовательно, у вас есть продолжение, хотя это не очевидно при просмотре кода.Под капотом асинхронный метод выглядит следующим образом:

Task DelayAsync()
{
    var tcs = new TaskCompletionSource<object>();

    try
    {
        var awaiter = Task.Delay(3000).GetAwaiter();

        awaiter.OnCompleted(() =>
        {
            // This is the continuation that causes your deadlock
            try
            {
                awaiter.GetResult();
                tcs.SetResult(null);
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }
        });
    }
    catch (Exception ex)
    {
        tcs.SetException(ex);
    }

    return tcs.Task;
}

Фактический код более сложен и использует AsyncTaskMethodBuilder<T> вместо TaskCompletionSource<T>, но идея та же.

...