Когда «ожидают» немедленно покидают метод, а когда нет? - PullRequest
0 голосов
/ 23 июня 2019

В программах, использующих async-await, мое понимание таково:

  • асинхронный метод, который НЕ ожидается, будет работать в фоновом режиме (?), А остальная часть кода продолжит выполняться доэтот не ожидаемый метод завершает
  • ожидаемый до завершения метода асинхронный метод, прежде чем перейти к следующим строкам кода

Приложение ниже было написано мнойпроверьте правильность приведенных выше утверждений.

using System;
using System.Threading.Tasks;

namespace ConsoleApp3
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            DoJob();

            var z = 3;

            Console.ReadLine();
        }

        static async Task DoJob()
        {
            var work1 = new WorkClass();
            var work2 = new WorkClass();

            while (true)
            {
                await work1.DoWork(500);
                await work2.DoWork(1500);
            }
        }
    }

    public class WorkClass
    {
        public async Task DoWork(int delayMs)
        {
            var x = 1;

            await Task.Delay(delayMs);

            var y = 2;
        }
    }
}

Вот некоторые из моих наблюдений:

  • Вызов DoJob(); не ожидается.Тем не менее, отладчик показывает мне, что код внутри DoJob выполняется, как если бы это был обычный не асинхронный метод.
  • Когда выполнение кода достигает await work1.DoWork(500);, я бы подумал: «Хорошо, такможет быть, теперь метод DoJob будет оставлен, а var z = 3; будет выполнен? В конце концов, 'await' должен покинуть метод. "В действительности, он просто входит в DoWork и не уходит DoJob - var z = 3; все еще не выполняется.
  • Наконец, когда выполнение достигает await Task.Delay(delayMs);, DoJob остается, иvar z = 3; достигнуто.После этого выполняется код после Delay.

Вещи, которые я не понимаю:

  • Почему await Task.Delay(delayMs); покидает метод DoJob, но await work1.DoWork(500); нет?
  • Я вижу, что DoJob работает нормально.Я думал, что это будет сделано в фоновом режиме (может быть, один из потоков пула потоков?).Похоже, он мог бы заблокировать поток, если бы это был какой-то длительный метод, я прав?

Ответы [ 6 ]

4 голосов
/ 23 июня 2019

Почему ждет Task.Delay (delayMs); оставьте метод DoJob, но ждите work1.DoWork (500); нет?

Потому что этот код:

await work1.DoWork(500);

такой же, как этот код:

var task = work1.DoWork(500);
await task;

Итак, ваш код вызывает метод сначала , а затем ожидает возвращенную задачу. Обычно говорят о await как о «ожидании вызова метода», но на самом деле это не то, что происходит - технически сначала вызывается метод (синхронно), а затем ожидается возвращаемая задача.

Я вижу, что DoJob работает нормально. Я думал, что это будет сделано в фоновом режиме (может быть, одним из потоков пула потоков?).

Нет; при истинных асинхронных операциях нет потока , заблокированного для этой операции.

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

Да.

мое понимание таково

Я рекомендую прочитать мое async intro для лучшего понимания. В итоге:

  1. async включает ключевое слово await. Он также генерирует конечный автомат, который обрабатывает создание возвращаемого значения Task и тому подобного.
  2. await оперирует «ожидаемым» (обычно заданием). Сначала проверяется, завершено ли оно; если это так, метод async продолжает выполняться синхронно.
  3. Если ожидаемое еще не завершено, то await (по умолчанию) захватывает его контекст и планирует продолжение метода async для запуска в этом контексте после завершения ожидаемого.
2 голосов
/ 23 июня 2019

Компилятор разбивает код в методе async на куски.1 перед первым await и 1 между каждым await и 1 после последнего await.

Выполнение вернется к вызывающей стороне в первом незавершенном ожидании или в конце метода.

Этот метод возвращает только завершенное Task после полного выполнения:

async Task M1() => await Task.CompletedTask;

Этот метод возвращает только неполное Task, которое завершится, когда Task вернул Task.Dealy(1000) завершено:

async Task M2() => await Task.Delay(1000);

Вот небольшой пример:

static async Task Main(string[] args)
{
    var t = TwoAwaits();
    Console.WriteLine("Execution returned to main");
    await t;
}
private static async Task TwoAwaits()
{
    Console.WriteLine("Before awaits");
    await Task.CompletedTask;
    Console.WriteLine("Between awaits #1");
    await Task.Delay(1000);
    Console.WriteLine("Between awaits #2");
    await Task.Delay(1000);
    Console.WriteLine("After awaits");
}
/*
Before awaits
Between awaits #1
Execution returned to main
Between awaits #2
After awaits
*/
2 голосов
/ 23 июня 2019

До Async & Await было два типа методов. Те, кто возвращал результат напрямую, и те, кто получил функцию обратного вызова в качестве параметра. В последнем случае метод вызывался в том же потоке синхронно и не возвращал значения, а позже в том же или другом потоке ваша функция обратного вызова была бы вызвана с результатом. Исторически все операции ввода-вывода (диск, сеть, даже память) работали с обратными вызовами (фактически - с прерываниями), но языки среднего и высокого уровня, такие как C #, маскировали бы все это внутренне, поэтому конечным пользователям не нужно изучать / писать код низкого уровня.

До определенного момента это работало довольно хорошо, за исключением того, что эта оптимизация тратила некоторые физические ресурсы. Например, Node.js превзошел некоторые другие языковые / серверные платформы по своему ограничению, что вынуждает разработчиков использовать модель обратного вызова вместо «управляемого» режима.

Это подтолкнуло C # и другие языки к возврату к модели обратного вызова, но читаемость кода действительно пострадала (spaguetti обратного вызова кода). Так были введены Async и Await.

Асинхронизация и ожидание позволят вам написать в «модели обратного вызова» с «управляемым» синтаксисом. Все обратные вызовы обрабатываются компилятором.

Каждый раз, когда вы пишете 'await' в асинхронном методе, ваш метод фактически разделяется на два метода, связанных обратным вызовом.

Теперь вы можете написать асинхронный метод, который выполняет обычный код sync, без ожидания, без переключения потоков или ввода / вывода. Этот «асинхронный» метод фактически будет работать синхронно. Таким образом, на самом деле это то же самое для await method1() или вызова без await. Зачем? потому что ваш асинхронный вызов ничего не ожидает, поэтому ваш асинхронный код по-прежнему является частью непрерывного кода.

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

TL; DR;

  • Асинхронный / ожидающий метод не гарантирует многопоточность или параллельную обработку. Это будет зависеть от полезной нагрузки (вызываемый асинхронный метод). Например, загрузка http, как правило, будет парализована, если вы управляете ожиданиями, потому что это функции, которые в основном являются официантами внешнего ответа. С другой стороны, интенсивная обработка ЦП, такая как сжатие файла, потребует другой формы управления процессором / потоком, не предоставляемой async / await.

  • Если вы не ожидаете асинхронный метод, ваш код обязательно будет работать синхронно до первого ожидания вызываемого метода, если он есть. Но позже он может запустить синхронизацию или нет.

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

Давайте рассмотрим четыре возможности:

(1)

void Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static async Task DoJob()
{
    Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(2000);
    Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
}

Это выводит:

Main 0 - 14
DoJob 0 - 14
DoJob 1 - 14
Main 1 - 14

У него есть 2-секундная пауза после DoJob 0.

(2)

async Task Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    await DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static async Task DoJob()
{
    Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(2000);
    Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
}

Опять это выводит:

Main 0 - 14
DoJob 0 - 14
DoJob 1 - 14
Main 1 - 14

(3)

async Task Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    await DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static Task DoJob()
{
    return Task.Run(() =>
    {
        Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(2000);
        Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
    });
}

Это имеет другой выход, потому что этоизменил поток:

Main 0 - 15
DoJob 0 - 13
DoJob 1 - 13
Main 1 - 13

И наконец:

async Task Main()
{
    Console.WriteLine($"Main 0 - {Thread.CurrentThread.ManagedThreadId}");
    DoJob();
    Console.WriteLine($"Main 1 - {Thread.CurrentThread.ManagedThreadId}");
}

public static Task DoJob()
{
    return Task.Run(() =>
    {
        Console.WriteLine($"DoJob 0 - {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(2000);
        Console.WriteLine($"DoJob 1 - {Thread.CurrentThread.ManagedThreadId}");
    });
}

У него снова другой вывод:

Main 0 - 13
Main 1 - 13
DoJob 0 - 12
DoJob 1 - 12

В этом последнем случае он не ждетDoJob, поскольку DoJob работает в другом потоке.

Поэтому, если вы следуете логике, проблема в том, что async / await не создает (или не использует) другой поток.Вызванный метод должен сделать это.

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

почему await Task.Delay(delayMs); выходит из метода DoJob, а await work1.DoWork(500); - нет?

Потому что до и до фактического асинхронного вызова он все еще находится втот же контекст.Если бы DoWork было просто:

public async Task DoWork(int delayMs)
{
    var x = 1;
    var y = 2;

    return Task.CompletedTask;
}

, в продолжении не было бы необходимости, и, следовательно, вы бы отлаживали все пути, не «возвращаясь» к исходному вызову await.

0 голосов
/ 23 июня 2019

Вот как ваше приложение может быть переделано, если вы по какой-то причине были вынуждены избегать асинхронизации / ожидания.Посмотрите, как сложно воспроизвести логику внутри цикла.Async / await действительно подарок от небес!

using System;
using System.Threading.Tasks;

namespace ConsoleApp3
{
    class Program
    {
        static Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            DoJob();

            var z = 3;

            Console.ReadLine();
            return Task.CompletedTask;
        }

        static Task DoJob()
        {
            var work1 = new WorkClass();
            var work2 = new WorkClass();

            var tcs = new TaskCompletionSource<bool>();
            Loop();
            return tcs.Task;

            void Loop()
            {
                work1.DoWork(500).ContinueWith(t1 =>
                {
                    if (t1.IsFaulted) { tcs.SetException(t1.Exception); return; }
                    work2.DoWork(1500).ContinueWith(t2 =>
                    {
                        if (t2.IsFaulted) { tcs.SetException(t2.Exception); return; }
                        if (true) { Loop(); } else { tcs.SetResult(true); }
                        // The 'if (true)' corresponds to the 'while (true)'
                        // of the original code.
                    });
                });
            }
        }
    }

    public class WorkClass
    {
        public Task DoWork(int delayMs)
        {
            var x = 1;

            int y;

            return Task.Delay(delayMs).ContinueWith(t =>
            {
                if (t.IsFaulted) throw t.Exception;
                y = 2;
            });
        }
    }
}
...