async / await deadlock при использовании WindowsFormsSynchronizationContext в консольном приложении - PullRequest
6 голосов
/ 20 ноября 2019

В качестве учебного упражнения я пытаюсь воспроизвести асинхронный / ожидающий тупик, который возникает в обычной форме Windows, но с использованием консольного приложения. Я надеялся, что приведенный ниже код вызовет это, и это действительно так. Но тупик также возникает неожиданно при использовании await.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
static class Program
{
    static async Task Main(string[] args)
    {
        // no deadlocks when this line is commented out (as expected)
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext()); 
        Console.WriteLine("before");
        //DoAsync().Wait(); // deadlock expected...and occurs
        await DoAsync(); // deadlock not expected...but also occurs???
        Console.WriteLine("after");
    }
    static async Task DoAsync()
    {
        await Task.Delay(100);
    }
}

Мне очень любопытно, если кто-нибудь знает, почему это происходит?

Ответы [ 3 ]

3 голосов
/ 20 ноября 2019

WindowsFormsSynchronizationContext отправит всех своих делегатов в цикл сообщений WinForms, который обслуживается потоком пользовательского интерфейса. Однако вы никогда не устанавливаете один из них, и нет никакой темы пользовательского интерфейса, поэтому все, что вы публикуете, просто исчезнет.

Итак, ваш await захватывает SynchronizationContext, который никогда не выполнит никаких завершений.

Что происходит:

  1. Ваш Task возвращаетсяс Task.Delay
  2. Основной поток начинает синхронно ожидать завершения этого Task, используя спин-блокировку (в Task.SpinThenBlockingWait)
  3. Время блокировки спинаout, и основной поток создает событие для ожидания, которое задается продолжением Задачи
  4. Задача завершается (вы можете видеть, что она имеет значение, потому что ее Состояние равно RanToCompletion)
  5. Задача пытается завершить продолжение, которое выпустит событие, ожидающее основной поток (Task.FinishContinuations). Это заканчивается вызовом TaskContinuation.RunCallback (хотя я еще не отслеживал этот путь вызова), который вызывает ваш WindowsFormSynchronizationContext.Post.
  6. Однако Post ничего не делает, и возникает тупик.

Чтобы получить эту информацию, я сделал следующие вещи:

  1. Попробуйте позвонить new WindowsFormsSynchronizationContext.Post(d => ..., null), убедитесь, что делегат не вызван.
  2. Создайте свой собственный SynchronizationContext и установите его, посмотрите, когда будет вызван Post.
  3. Сломайте отладчик во время тупика, посмотрите на Threads и посмотрите на Call Stackосновной поток.
  4. Захватите ожидаемую задачу в переменной, посмотрите на нее в окне наблюдения, щелкните правой кнопкой мыши -> Создать идентификатор объекта, затем поместите этот идентификатор объекта в окно просмотра. Пусть он заблокируется, прервется и осмотрит задачу в окне наблюдения по идентификатору объекта.
3 голосов
/ 20 ноября 2019

Это происходит потому, что WindowsFormsSynchronizationContext зависит от существования стандартного цикла сообщений Windows. Консольное приложение не запускает такой цикл, поэтому сообщения, отправленные на WindowsFormsSynchronizationContext, не обрабатываются, продолжения задачи не вызываются, и поэтому программа зависает на первом await. Вы можете подтвердить отсутствие цикла сообщений, запросив логическое свойство Application.MessageLoop.

Получает значение, указывающее, существует ли цикл сообщений в этом потоке.

Чтобы сделать WindowsFormsSynchronizationContext функциональным, вы должны запустить цикл обработки сообщений. Это можно сделать так:

static void Main(string[] args)
{
    EventHandler idleHandler = null;
    idleHandler = async (sender, e) =>
    {
        Application.Idle -= idleHandler;
        await MyMain(args);
        Application.ExitThread();
    };
    Application.Idle += idleHandler;
    Application.Run();
}

Метод MyMain - это ваш текущий метод Main, переименованный.


Обновление: На самом делеМетод Application.Run автоматически устанавливает WindowsFormsSynchronizationContext в текущем потоке, поэтому вам не нужно делать это явно. Если вы хотите запретить эту автоматическую установку, настройте свойство WindowsFormsSynchronizationContext.AutoInstall перед вызовом Application.Run.

Свойство AutoInstall определяет, является ли WindowsFormsSynchronizationContextустанавливается при создании элемента управления или при запуске цикла сообщений.

2 голосов
/ 20 ноября 2019

Я полагаю, это потому, что async Task Main - не более чем синтаксический сахар. На самом деле это выглядит так:

static void Main(string[] args) => MainAsync(args).GetAwaiter().GetResult();

Т.е. все еще блокируется. Продолжение DoAsync пытается выполнить в исходном потоке, потому что контекст синхронизации не нулевой. Но поток застрял, потому что он ждет, когда задача завершена. Вы можете исправить это так:

static class Program
{
    static async Task Main(string[] args)
    {
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
        Console.WriteLine("before");
        await DoAsync().ConfigureAwait(false); //skip sync.context
        Console.WriteLine("after");
    }
    static async Task DoAsync()
    {
        await Task.Delay(100).ConfigureAwait(false); //skip sync.context
    }
}
...