Pull to refresh

Async/await, There is no thread – легко обмануть тех, кто сам обманываться рад

Level of difficultyMedium
Reading time3 min
Views14K

Когда нам показывают на некотором примере, что асинхронная операция не создает потока, нам пытаются внушить, что асинхронная операция НИКОГДА не создает потока и в принципе не может его создать, но это не правда! Простой пример с работающим кодом доказывает обратное. Давайте разберем этот пример.

Логика тех, кто поддается такому внушению мне вполне понятна, они хотят упростить себе жизнь, сократить объем теории, с которой надо разбираться.

Интересно было бы понять логику тех, кто поддерживает такое внушение, выдавая обрезанную теорию за полноценную, вполне осознавая, что все не так просто, как хотелось бы.


Итак, пример кода, который будет создавать дополнительный поток для кода асинхронной операции в консольном (это важно! почему, мы увидим дальше) приложении выглядит так:

        static async Task Main()
        {
            SomeMethodAsync1();
            Console.WriteLine($"ThrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MAIN Iter={i}");
                Thread.Sleep(100);
            }
        }

        static async Task SomeMethodAsync1()
        {
            await Task.Yield();
            Console.WriteLine($"TrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MethodIter={i}");
                Thread.Sleep(100);
            }
        }

Этот код генерирует следующий вывод:

Собственно, уже из этого вывода видно, что две функции выполняются в разных потоках с идентификаторами: ID=1 и ID=5.

 Можно рассмотреть эти потоки N1 и N5 более пристально в режиме отладки:

Not Flagged                12268  1          Main Thread   Main Thread            System.Private.CoreLib.dll!System.Threading.Thread.Sleep

Not Flagged    >           26956  5          Worker Thread           .NET ThreadPool Worker            PrimMath.dll!PrimMath.Program.SomeMethodAsync1

Мы видим, что потоки у нас берутся из Тред-пула, то есть, формально говоря, наша функция SomeMethodAsync1() не создает поток, она берет существующий из Тред-пула. Но никак нельзя отрицать тот факт, что асинхронная операция все-таки использует дополнительный поток, то есть утверждение There is no thread оказывается ложным для этого кода, потому что мы явно видим, что The thread with ID5 is definitely present.

Но может быть я где-то, как-то вас тоже обманываю? Если вы не верите мне и моему примеру и своим глазам, вы все также можете обратиться к незабвенному Stephen Toub и к его уже изрядно всем надоевшей (надеюсь это не так :) работе How Async/Await Really Works in C#. Параграф:

SynchronizationContext and ConfigureAwait

Там есть пример консольного кода, который далее в том Посте упоминается как our timing sample и который тоже создает дополнительный поток для async lambda функции. Собственно то, что асинхронный метод создает дополнительный поток, и является проблемой, которую автор Поста далее успешно решает, когда подменяет SynchronizationContext для этого исходного примера. Собственно, из того примера Стивена Тоуба вы и должны были сделать вывод, что наличие или отсутствие потока внутри асинхронного метода управляется как раз с помощью SynchronizationContext, который, в некотором смысле, можно считать дополнительным планировщиком (scheduler), который предоставлен нам средой исполнения .NET, и который вы можете переопределить под свои задачи в любой момент.

А вот теперь вооружившись этим знанием о том, что наличием или отсутствием потока
для асинхронных операций управляет контекст синхронизации (SynchronizationContext),
давайте взглянем на пример, который разбирают в статье There is no thread. (Там нет потока):

private async void Button_Click(object sender, RoutedEventArgs e)
{
  byte[] data = ...
  await myDevice.WriteAsync(data, 0, data.Length);
}

Я хочу обратить ваше внимание на название функции Button_Click(), которая однозначно указывает нам какой SynchronizationContext применяется в этом примере, это SynchronizationContext текущего окна (или всего UI). UI SynchronizationContext действительно не создает потоки, он упаковывает асинхронные вызовы в сообщения для единственного UI-потока, ставит их в очередь на исполнение в этом единственном потоке.

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

 Ну вот, надеюсь я ответил на вопрос от @Grave18

Может ли магия async/await сама создавать потоки или это все делается явно тем, кто асинхронные функции пишет?

Еще раз благодарю за конструктивный вопрос.

Tags:
Hubs:
Total votes 18: ↑8 and ↓10+1
Comments56

Articles