Понимание C# ожидания оператора - PullRequest
2 голосов
/ 15 апреля 2020

У меня просто есть базовое c понимание асинхронного программирования C#. Как я понимаю, этот код

statement1;
await statement2();
statement3;
statement4;

должен быть логически эквивалентен

statement1;
var awaiter = statement2().GetAwaiter();
awaiter.OnCompletion(() => {
    awaiter.GetResult();
    statement3;
    statement4;
});

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

using System;
using System.Threading.Tasks;

namespace ThreadTesting
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            await DoSomething2(); // <------- B
            Console.WriteLine("Test!"); // <------- C
            Console.ReadKey(); // <------- D
        }

        public static async Task DoSomething2()
        {
            Console.WriteLine("Start DoSomething2"); 
            var i = await DoSomething(); // <------ A
            Console.WriteLine("End DoSomething2");
        }

        public static async Task<int> DoSomething()
        {
            Console.WriteLine("Start DoSomething1");
            await Task.Delay(10000);
            Console.WriteLine("Before returning from DoSomething1");
            return 88;
        }
    }
}

Вывод

Start DoSomething2
Start DoSomething1
Before returning from DoSomething1
End DoSomething2
Test!

Давайте возьмем, к примеру, утверждение А. Здесь его ждет DoSomething(). В ожидании этого логика выполнения c возвращается из DoSomething2() и возвращается к B. Здесь, как я понимаю, выполнение должно go для выполнения оператора C и D при ожидании DoSomething2() (оператор B) , Но результат показывает, что строка "Test!" будет напечатана только в конце программы. Почему это происходит? Правильно ли мое понимание?

Ответы [ 5 ]

3 голосов
/ 15 апреля 2020

Загвоздка в том, что ваш Main возвращает Task, поэтому он также полностью async и будет "остановлен". И ваш порядок выполнения будет выглядеть так:

  1. Main() запускается
  2. Main попадает в оператор await и создает Task с остальной частью метода. Выглядит так, как будто Main() остановился, ожидая запуска await результата
  3. DoSomething2() и записывает в консоль «Start DoSomething2»
  4. Достигает следующего await оператор и создает еще один Task с остальным DoSomething() методом
  5. DoSomething() запускается
  6. записывает в консоль «Start DoSomething1»
  7. Получает в оператор awat и создает Task с остальной частью метода DoSomething(), ожидая задержки.
  8. После этого Task завершается (после задержки) DoSomething() пишет «До возвращаясь из DoSomething1 "в консоль и возвращая 88.
  9. Task, созданный в DoSomething2(), завершает оставшуюся часть метода (назначает 88 для i и пишет" Перед возвращением из DoSomething1 "в консоль
  10. Task, созданный в Main(), завершается, записывая «Test!» На консоль.

Итак, «Test!» В конце, потому что ваш Main() возвращает Task. Если вам нужно такое поведение, вы можете просто поменять Task на void:

public static async void Main(string[] args)
{
    ...
}

. n при достижении оператора await, Main() не будет ждать завершения Task() и продолжит выполнение.

1 голос
/ 15 апреля 2020

Как canbax уже сказал в своем ответе, оператор await делает то, что предлагает его наименование. A синхронно ожидает , что Задача (или лучше любой тип, который может быть ожидаемым ) завершена.

Так что, если у вас есть код вроде

DoSomething1();
await DoSomething2Async();
DoSomething3();

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

В отличие от этого Вы можете создать задачу, вызвав асинхронный метод c, и только позже ожидайте ее, например:

DoSomething1();
Task task = DoSomething2Async();
DoSomething3();
await task;

, этот код более открыт для порядка вызова DoSomething3 и завершения задачи. , Это потому, что вы в основном говорите компилятору "начать делать DoSomething2Async, но мне важно, чтобы он завершился после завершения вызова DoSomething3.

Здесь является живым примером для

using System;
using System.Threading.Tasks;

class Program {
    static async Task Main(string[] args) {
        DoSomething1();
        await DoSomething2Async();
        DoSomething3();

        Console.WriteLine("---");

        DoSomething1();
        var task = DoSomething2Async();
        DoSomething3();
        await task;
    }

    static void DoSomething1()
    {
        Console.WriteLine("1");
    }

    static async Task DoSomething2Async()
    {
        await Task.Run(() => Console.WriteLine("2"));
    }

    static void DoSomething3()
    {
        Console.WriteLine("3");
    }
}

, который, как правило, будет производить продукцию типа

1
2
3
---
1
3
2
1 голос
/ 15 апреля 2020

await и async остановить выполнение кода для вас. Но на самом деле они не останавливают выполнение основного потока .

. В вашем примере все выглядит так, как работает.

Здесь, как я понимаю, выполнение должно go включить выполнение оператора C и D в ожидании DoSomething2 ()

Нет, они не должны, потому что Вы сказали, что хотите подождать , пока это не будет сделано. Даже если он не вернет что-то, он будет ждать из-за await. Внутри DoSomething2() вы делаете то же самое.

await более разумно, когда вы зависите от результата асинхронной функции. Это спасает вас от ада обратного вызова.

Вместо этого

enter image description here

Вы можете использовать это

enter image description here

примечание: скриншоты из https://blog.hellojs.org/asynchronous-javascript-from-callback-hell-to-async-and-await-9b9ceb63c8e8 Коды в JS.

1 голос
/ 15 апреля 2020

Что-то работает так ... Вы должны go пройти через все функции, а затем вернуться туда, где их ожидают вызовы.

using System;
using System.Threading.Tasks;

namespace ThreadTesting
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            await DoSomething2();// <------ Start go to DoSomething2
            Console.WriteLine("Test!"); //F
            Console.ReadKey(); 
        }

        public static async Task DoSomething2()
        {
            Console.WriteLine("Start DoSomething2"); // <------ A  godown
            var i = await DoSomething(); // <------ B  go to DoSomething
            Console.WriteLine("End DoSomething2");// <------ E  back to Main
        }

        public static async Task<int> DoSomething()
        {
            Console.WriteLine("Start DoSomething1");// <------ C godown
            await Task.Delay(10000);
            Console.WriteLine("Before returning from DoSomething1");// <------ D back to DoSomething2
            return 88;
        }
    }
}
0 голосов
/ 15 апреля 2020

Я обнаружил проблему с моим кодом. Как я уже описал в этом вопросе, этот код

statement1;
await statement2();
statement3;
statement4;

примерно эквивалентен

statement1;
var awaiter = statement2().GetAwaiter();
awaiter.OnCompletion(() => {
    awaiter.GetResult();
    statement3;
    statement4;
});

Это правда. Таким образом, для кода

await DoSomething2(); // <------- B
Console.WriteLine("Test!"); // <------- C
Console.ReadKey(); // <------- D

он примерно эквивалентен

statement1;
var awaiter = DoSomething2().GetAwaiter(); // <------- B
awaiter.OnCompletion(() => {
    awaiter.GetResult();
    Console.WriteLine("Test!"); // <------- C
    Console.ReadKey(); // <------- D
});

То есть оператор C & D будет выполняться только после выполнения задачи DoSomething2(). Вот почему Test! будет напечатан только в конце программы. Если я уберу await до DoSomething2():

using System;
using System.Threading.Tasks;

namespace ThreadTesting
{
    class Program
    {
        public static void Main(string[] args)
        {
            DoSomething2(); // <------- B (await removed)
            Console.WriteLine("Test!"); // <------- C
            Console.ReadKey(); // <------- D
        }

        public static async Task DoSomething2()
        {
            Console.WriteLine("Start DoSomething2"); 
            var i = await DoSomething(); // <------ A
            Console.WriteLine("End DoSomething2"); // <------ I
        }

        public static async Task<int> DoSomething()
        {
            Console.WriteLine("Start DoSomething1");  // <------ E
            await Task.Delay(10000); // <------ F
            Console.WriteLine("Before returning from DoSomething1"); // <------ G
            return 88; // <------ H
        }
    }
}

Программа выдаст такой результат:

Start DoSomething2
Start DoSomething1
Test!
Before returning from DoSomething1
End DoSomething2

Это разумно и именно то, что я хочу. Логику выполнения c можно логически понимать как:

  • Начните с main и запустите DoSomething2()
  • Go до DoSomething2 и напечатайте «Start DoSomething2»
  • Выполнить DoSomething() (оператор A) и вывести «Start DoSomething1» (оператор E)
  • Выполнить Task.Delay(10000) (оператор F). Поскольку это занимает некоторое время и возвращает задачу (ожидаемую), добавьте следующие операторы (оператор G & H) в качестве продолжения (C1) и вернитесь к оператору A.
  • Поскольку оператор A является оператором ожидания, он добавляет следующий оператор (оператор I) в качестве продолжения (c2) и возвращается к оператору B.
  • Выполнение переходит к B, и поток не блокируется. Код продолжает выполнять оператор C и D.
  • Как только оператор F завершен, продолжение C1 начинает выполняться.
  • После завершения C1 продолжение C2 начинает выполняться.

Это мое понимание логики c. Пожалуйста, исправьте меня, если найдете ошибку. Я новичок в асинхронном программировании.

У меня неправильное представление о том, что я должен ждать всех асин c методов. Это не правда.

...