Async / Await: почему код, следующий за await, также выполняется в фоновом потоке, а не в исходном первичном потоке? - PullRequest
0 голосов
/ 29 июня 2019

Ниже мой код:

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        string message = await DoWorkAsync();
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }

    static async Task<string> DoWorkAsync()
    {
        return await Task.Run(() =>
        {
            Thread.Sleep(3_000);
            return "Done with work!";
        });
    }
}

и вывод

1

// через 3 секунды

3

Готово с работой!

так что вы можете видеть, как основной поток (идентификатор равен 1) заменен на рабочий поток (идентификатор равен 3), так почему же основной поток просто исчезает?

Ответы [ 3 ]

2 голосов
/ 29 июня 2019

Асинхронная точка входа - просто хитрость компилятора.За кулисами компилятор генерирует эту реальную точку входа:

private static void <Main>(string[] args)
{
    _Main(args).GetAwaiter().GetResult();
}

Если вы измените свой код так:

class Program
{
    private static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    }


    static async Task MainAsync(string[] args)
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        string message = await DoWorkAsync();
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }

    static async Task<string> DoWorkAsync()
    {
        await Task.Delay(3_000);
        return "Done with work!";
    }
}

Вы получите это:

1
4
Done with work!
1

Как и ожидалось, основной поток ожидает выполнения работы.

1 голос
/ 30 июня 2019

Это результат выбранного вами типа приложения. Консольные приложения и приложения с графическим интерфейсом ведут себя по-разному в отношении SynchronizationContext. При использовании await текущий SynchronizationContext захватывается и передается в фоновый поток.
Идея состоит не в том, чтобы блокировать основной поток, просто ожидая завершения фонового потока. Оставшийся код ставится в очередь, а текущий контекст сохраняется в SynchronizationContext, который будет захвачен фоновым потоком. Когда фоновый поток завершает работу, он возвращает захваченный SynchronizationContext, чтобы оставшийся в коде код мог возобновить выполнение. Вы можете получить текущий контекст, обратившись к свойству SynchronizationContext.Current. Код, ожидающий завершения await (оставшийся код после await), будет помещен в очередь как продолжение и выполнен на захваченном SynchronizationContext.

Значение по умолчанию SynchronizationContext.Current - это поток пользовательского интерфейса для приложений с графическим интерфейсом, таких как WPF или NULL, для консольных приложений. Консольные приложения не имеют SynchronizationContext, поэтому для использования async фреймворк использует ThreadPool SynchronizationContext. Правила поведения SynchronizationContext таковы:

  1. Если SynchronizationContext.Current возвращает NULL, продолжение потока будет по умолчанию потоком пула потоков
  2. Если SynchronizationContext.Current не равен NULL, продолжение будет выполнен в захваченном контексте.
  3. И: если await используется в фоновом потоке (следовательно, новый фоновый поток запускается из фонового потока), затем SynchronizationContext всегда будет потоком пула потоков.

Сценарий 1, консольное приложение:
Применяется правило 1): поток 1 вызывает await, который попытается захватить текущий контекст. await будет использовать фоновый поток поток 3 из ThreadPool для выполнения асинхронного делегата.
После завершения делегата оставшийся код вызывающего потока будет выполнен в захваченном контексте. Поскольку в консольных приложениях этот контекст равен NULL, по умолчанию вступит в силу SynchronizationContext (первое правило). Поэтому планировщик решает продолжить выполнение в потоке ThreadPool, потоке , потоке 3 (для эффективности. Переключения контекста дороги).

Сценарий 2, приложение с графическим интерфейсом:
применяется правило 2): поток 1 вызывает await, который попытается захватить текущий контекст (UI SynchronizationContext). await будет использовать фоновый поток поток 3 из ThreadPool для выполнения асинхронного делегата.
После завершения делегата оставшийся код вызывающего потока будет выполнен в захваченном контексте, UI SynchronizationContext thread 1 .

Сценарий 3, приложение с графическим интерфейсом и Task.ContinueWith:
применяется правило 2) и правило 3): поток 1 вызывает await, который будет пытаться захватить текущий контекст (UI SynchronizationContext). await будет использовать фоновый поток поток 3 из ThreadPool для выполнения асинхронного делегата. После завершения делегата продолжение TaskContinueWith. Так как мы все еще находимся в фоновом потоке, новый TreadPool поток поток 4 используется с захваченным SynchronizationContext из потоком 3 . После завершения продолжения контекст возвращается к потоку 3 , который будет выполнять оставшийся код вызывающего абонента в захваченном SynchronizationContext, который является потоком пользовательского интерфейса потоком 1 .

Сценарий 4, приложение с графическим интерфейсом и Task.ConfigureAwait(false) (await DoWorkAsync().ConfigureAwait(false);):
применяется правило 1): поток 1 вызывает await и выполняет асинхронный делегат в ThreadPool фоновом потоке поток 3 . Но поскольку задача была настроена с Task.ConfigureAwait(false) , поток 3 не захватывает SynchronizationContext вызывающего (UI SynchronizationContext). Поэтому свойство SynchronizationContext.Current потока 3 будет иметь значение NULL, и по умолчанию применяется SynchronizationContext: контекст будет представлять собой поток ThreadPool. Из-за оптимизации производительности (переключение контекста дорого) контекст будет текущим SynchronizationContext из потока 3 . Это означает, что после завершения thread 3 оставшийся код вызывающего абонента будет выполняться по умолчанию SynchronizationContext thread 3 . Значение по умолчанию Task.ConfigureAwait равно true, что позволяет захватывать вызывающего абонента SynchronizationContext.

Сценарий 5, приложение с графическим интерфейсом и Task.Wait, Task.Result или Task.GetAwaiter.GetResult:
правило 2 применяется, но приложение будет в тупике. Текущий SynchronizationContext из резьбы 1 фиксируется. Но поскольку асинхронный делегат выполняется синхронно (Task.Wait, Task.Result или Task.GetAwaiter.GetResult превратит асинхронную операцию в синхронное выполнение делегата), поток 1 будет блокироваться до тех пор, пока не завершится теперь синхронный делегат .
Поскольку код выполняется синхронно, оставшийся код потока 1 не был поставлен в очередь как продолжение потока 3 и поэтому будет выполняться в потоке 1 после завершения делегата. Теперь, когда делегат в потоке 3 завершает работу, он не может вернуть SynchronizationContext из нити 1 в нити 1 , поскольку нити 1 является все еще блокирует (и таким образом блокирует SynchronizationContext). Поток 3 будет бесконечно ждать потока 1 , чтобы снять блокировку на SynchronizationContext, что, в свою очередь, заставляет поток 1 бесконечно ждать потока 3 вернуть -> тупик.

Сценарий 6, консольное приложение и Task.Wait, Task.Result или Task.GetAwaiter.GetResult:
правило 1 применяется. Текущий SynchronizationContext из резьбы 1 фиксируется. Но поскольку это консольное приложение, контекст имеет значение NULL и применяется значение по умолчанию SynchronizationContext. Асинхронный делегат выполняется синхронно (Task.Wait, Task.Result или Task.GetAwaiter.GetResult превратит асинхронную операцию в синхронную операцию) в ThreadPool фоновом потоке , потоке 3 и , потоке 1 будет блокироваться, пока делегат в потоке 3 не завершится. Поскольку код выполняется синхронно, оставшийся код не был поставлен в очередь как продолжение потока 3 и поэтому будет выполняться в потоке 1 после завершения делегата. Нет ситуации взаимоблокировки в случае применения консоли, поскольку SynchronizationContext из потока 1 был равен NULL и потока 3 должен использовать контекст по умолчанию.

Код вашего примера соответствует сценарию 1. Это потому, что вы работаете с консольным приложением и применяется SynchronizationContext по умолчанию, которое применяется, потому что SynchronizationContext консольных приложений всегда равно NULL. Когда захваченный SynchronizationContext равен NULL, Task использует контекст по умолчанию, который является потоком ThreadPool. Поскольку асинхронный делегат уже выполнен в потоке ThreadPool, TaskScheduler решает остаться в этом потоке и, следовательно, выполнить оставшийся в очереди оставшийся код потока вызывающего потока 1 в потока 3 .

В приложениях с графическим интерфейсом рекомендуется всегда использовать Task.ConfigureAwait(false) везде, кроме случаев, когда вы явно хотите захватить SynchronizationContext вызывающей стороны. Это предотвратит случайные блокировки в вашем приложении.

1 голос
/ 30 июня 2019

В вашем коде ваш основной поток заканчивается, когда он вызывает await здесь:

string message = await DoWorkAsync();

Если выполнение будет расходиться, поскольку DoWorkAsync() создает задачу, и весь код после этого вызова будет выполнен в рамках новой созданной задачи, то после вызова await DoWorkAsync(); основному потоку нечего делать, так что это будет сделано.

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