Не ожидание асинхронного вызова все еще асинхронно, верно?

Извините, если это глупый вопрос (или дубликат).

У меня есть функция A:

public async Task<int> A(/* some parameters */)
{
    var result = await SomeOtherFuncAsync(/* some other parameters */);

    return (result);
}

У меня есть другая функция B, вызывающая A, но не использующая возвращаемое значение:

public Task B(/* some parameters */)
{
    var taskA = A(/* parameters */); // #1

    return (taskA);
}

Обратите внимание, что B не объявлен async и не ожидает вызова A. Вызов на A не является вызовом "забыл и забыл" - B вызывается C следующим образом:

public async Task C()
{
    await B(/* parameters */);
}

Обратите внимание, что на # 1 нет await. У меня есть сотрудник, который утверждает, что это делает вызов A синхронным, и он продолжает придумывать журналы Console.WriteLine, которые, казалось бы, подтверждают его точку зрения.

Я попытался указать, что только потому, что мы не ждем результатов внутри B, цепочка задач ожидается, и характер кода внутри A не меняется только потому, что мы его не ждем. Поскольку возвращаемое значение из A не требуется, нет необходимости ждать задачи на месте вызова, пока кто-то в цепочке ожидает ее (что происходит в C).

Мой коллега очень настойчив, и я начал сомневаться в себе. Мое понимание неверно?

Ответ 1

  Извините, если это глупый вопрос

Это не глупый вопрос. Это важный вопрос.

У меня есть коллега, который утверждает, что это делает вызов A синхронным, и он продолжает придумывать журналы Console.WriteLine, которые, по-видимому, подтверждают его точку зрения.

Это фундаментальная проблема, и вам нужно обучить своих коллег, чтобы они перестали вводить в заблуждение себя и других. Асинхронного вызова не существует. Вызов не является асинхронным, никогда. Скажи это со мной. Вызовы не асинхронные в С#. В С# при вызове функции эта функция вызывается сразу после вычисления всех аргументов.

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

Итак, ваш коллега прав? Конечно они есть. Вызов A является синхронным, поскольку все вызовы функций являются синхронными. Но тот факт, что они считают, что существует такая вещь, как "асинхронный вызов", означает, что они сильно ошибаются в отношении того, как асинхронность работает в С#.

Если конкретно ваш коллега считает, что await M() каким-то образом делает вызов M() "асинхронным", то у вашего коллеги есть большое недоразумение. await является оператором. Конечно, это сложный оператор, но это оператор, который работает со значениями. await M() и var t = M(); await t; - это одно и то же. Ожидание происходит после вызова, потому что await работает с возвращаемым значением. await - это НЕ инструкция для компилятора для "генерации асинхронного вызова M()" или чего-либо подобного; нет такого понятия, как "асинхронный вызов".

Если это характер их ложного убеждения, тогда у вас есть возможность обучить своего коллегу тому, что означает await. await означает что-то простое, но мощное. Это значит:

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

Это все, что await делает. Он просто проверяет содержимое задачи, и, если задача не завершена, он говорит: "Ну, мы не можем добиться какого-либо прогресса в этом рабочем процессе, пока эта задача не будет завершена, поэтому вернитесь к моей вызывающей стороне, которая найдет что-то еще для этого ЦП. делать ".

природа кода внутри A не меняется только потому, что мы его не ждем.

Это правильно. Мы синхронно вызываем A, и он возвращает Task. Код после вызова сайта не выполняется, пока не вернется A. Интересная вещь в A заключается в том, что A разрешено возвращать неполный Task вызывающей стороне, и эта задача представляет узел в асинхронном рабочем процессе. Рабочий процесс уже асинхронный, и, как вы заметили, для A не имеет значения, что вы делаете с его возвращаемым значением после его возвращения; A не знает, собираетесь ли вы await вернуть Task или нет. A просто выполняется так долго, как может, и затем либо возвращает завершенную обычно задачу, либо завершенную исключительно задачу, либо возвращает незавершенную задачу. Но то, что вы делаете на сайте вызовов, не меняет этого.

Поскольку возвращаемое значение из A не требуется, нет необходимости ждать задачи на сайте вызова

Правильно.

нет необходимости ждать задачи на сайте вызова, пока кто-то в цепочке ожидает ее (что происходит в C).

Теперь ты потерял меня. Почему кто-то должен ждать Task, возвращенного A? Скажите, почему вы считаете, что кто-то должен await это Task, потому что у вас может быть ложное убеждение.

Мой коллега очень настойчив, и я начал сомневаться в себе. Мое понимание неверно?

Ваш коллега почти наверняка не прав. Ваш анализ кажется правильным вплоть до того момента, когда вы говорите, что существует требование, чтобы каждый Task был await издан, что не соответствует действительности. Странно не await a Task, потому что это означает, что вы написали программу, в которой вы начали операцию, и не заботитесь о том, когда или как она завершается, и, конечно, плохо писать такую программу, но не является требованием к await каждому Task. Если вы верите, что есть, снова скажите, что это за убеждение, и мы разберемся с ним.

Ответ 2

Ты прав. Создание задачи делает только это, и ей все равно, когда и кто будет ждать ее результата. Попробуйте вставить await Task.Delay(veryBigNumber); в SomeOtherFuncAsync, и вывод консоли должен быть таким, как вы ожидаете.

Это называется eliding, и я предлагаю вам прочитать этот пост, где вы можете понять, почему вы должны или не должны делать это.

Также приведен минимальный (немного запутанный) пример копирования вашего кода, подтверждающий вашу правоту:

class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine($"Start of main {Thread.CurrentThread.ManagedThreadId}");
            var task = First();
            Console.WriteLine($"Middle of main {Thread.CurrentThread.ManagedThreadId}");
            await task;
            Console.WriteLine($"End of main {Thread.CurrentThread.ManagedThreadId}");
        }

        static Task First()
        {
            return SecondAsync();
        }

        static async Task SecondAsync()
        {
            await ThirdAsync();
        }

        static async Task ThirdAsync()
        {
            Console.WriteLine($"Start of third {Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(1000);
            Console.WriteLine($"End of third {Thread.CurrentThread.ManagedThreadId}");
        }
    }

Это пишет Middle of main до End of third, доказывая, что это на самом деле асинхронный. Более того, вы можете (скорее всего) увидеть, что концы функций выполняются в другом потоке, чем остальная часть программы. Оба начала и середина main всегда будут выполняться в одном и том же потоке, потому что они на самом деле являются синхронными (main запускает, вызывает цепочку функций, третьи возвращает (может возвращаться в строке с ключевым словом await), а затем main продолжается как если асинхронная функция никогда не использовалась. Окончания после ключевых слов await в обеих функциях могут выполняться в любом потоке в ThreadPool (или в используемом вами контексте синхронизации).

Теперь интересно отметить, что если Task.Delay в Third не займет очень много времени и фактически завершится синхронно, все это будет выполняться в одном потоке. Более того, даже если он будет работать асинхронно, он может работать в одном потоке. Нет правила, утверждающего, что асинхронная функция будет использовать более одного потока, она вполне может просто выполнить некоторую другую работу, ожидая завершения какой-либо задачи ввода-вывода.