В чем разница между началом и ожиданием задачи? - PullRequest
0 голосов
/ 21 октября 2018

Какая разница между стартом и ожиданием?Код ниже взят из блога Стивена Клири (включая комментарии)

public async Task DoOperationsConcurrentlyAsync()
{
  Task[] tasks = new Task[3];
  tasks[0] = DoOperation0Async();
  tasks[1] = DoOperation1Async();
  tasks[2] = DoOperation2Async();

  // At this point, all three tasks are running at the same time.

  // Now, we await them all.
  await Task.WhenAll(tasks);
}

Я думал, что задачи начинают выполняться, когда вы их ожидаете ... но комментарии в коде, по-видимому, подразумевают иное.Кроме того, как могут выполняться задачи после того, как я просто приписал их массиву типа Task.Разве это не просто атрибуция, по природе не связанная с действием?

Ответы [ 4 ]

0 голосов
/ 22 октября 2018

Вот краткий ответ:

Чтобы ответить на этот вопрос, вам просто нужно понять, что делают ключевые слова async / await.

Мы знаем, что один поток может делать только одну вещь за один раз, и мы также знаем, что один поток перенаправляет во всем приложении различные вызовы методов и события, ETC.Это означает, что когда поток должен идти дальше, он, скорее всего, запланирован или поставлен в очередь где-то за кулисами (но здесь я не буду объяснять эту часть). Когда поток вызывает метод, этот метод запускается до завершения перед любымдругие методы могут быть запущены, поэтому предпочтительнее отправлять долго работающие методы другим потокам, чтобы предотвратить зависание приложения.Чтобы разбить один метод на отдельные очереди, нам нужно выполнить какое-то необычное программирование ИЛИ вы можете поставить подпись async на метод.Это говорит компилятору, что в какой-то момент метод может быть разбит на другие методы и помещен в очередь для последующего запуска.

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

Как await использует Task, чтобы сообщить компилятору запланировать оставшуюся часть метода?Когда вы вызываете await Task, компиляторы вызывают для вас метод Task.GetAwaiter() для этого Task.GetAwaiter() вернуть TaskAwaiter.TaskAwaiter реализует два интерфейса ICriticalNotifyCompletion, INotifyCompletion.У каждого есть один метод, UnsafeOnCompleted(Action continuation) и OnCompleted(Action continuation).Затем компилятор оборачивает оставшуюся часть метода (после ключевого слова await) и помещает его в Action, а затем вызывает методы OnCompleted и UnsafeOnCompleted и передает это Action в качестве параметра.Теперь, когда Task завершен, в случае успеха он вызывает OnCompleted, а если нет, он вызывает UnsafeOnCompleted, и он вызывает те из контекста потока, который использовался для запуска Task.Он использует ThreadContext для отправки потока в исходный поток.

Теперь вы можете понять, что ни async, ни await не выполняют никаких Task с.Они просто говорят компилятору использовать какой-то заранее написанный код для планирования всего этого за вас.По факту;вы можете await a Task, который не запущен, и он будет await до тех пор, пока Task не будет выполнен и завершен, или пока приложение не завершится.

Зная это;давайте разберемся и поймем это глубже, выполнив то, что асинхронное ожидание делает вручную.


Использование асинхронного ожидания

using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Test();
            Console.ReadKey();
        }

        public static async void Test()
        {
            Console.WriteLine($"Before Task");
            await DoWorkAsync();
            Console.WriteLine($"After Task");
        }

        static public Task DoWorkAsync()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
                Task.Delay(1000).Wait();
                Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
            });
        }
    }
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task

Делая то, чтокомпилятор делает это вручную (вроде)

Примечание. Хотя этот код работает, он предназначен для того, чтобы помочь вам понять асинхронное ожидание с точки зрения сверху вниз.Он НЕ охватывает и не выполняет то же самое, что и компилятор дословно.

using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Test();
            Console.ReadKey();
        }

        public static void Test()
        {
            Console.WriteLine($"Before Task");
            var task = DoWorkAsync();
            var taskAwaiter = task.GetAwaiter();
            taskAwaiter.OnCompleted(() => Console.WriteLine($"After Task"));
        }

        static public Task DoWorkAsync()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
                Task.Delay(1000).Wait();
                Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
            });
        }
    }
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task

РЕЗЮМЕ УРОКА:

Обратите внимание, что метод в моем примере DoWorkAsync() это просто функция, которая возвращает Task.В моем примере Task работает, потому что в методе, который я использую return Task.Run(() =>….Использование ключевого слова await не меняет эту логику.Это точно так же;await делает только то, что я упомянул выше.

Если у вас есть какие-либо вопросы, просто задавайте, и я буду рад ответить на них.

0 голосов
/ 22 октября 2018

A Task возвращает "горячий" (то есть уже запущен).await асинхронно ожидает завершения Task.

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

await DoOperation0Async(); // start DoOperation0Async, wait for completion, then move on
await DoOperation1Async(); // start DoOperation1Async, wait for completion, then move on
await DoOperation2Async(); // start DoOperation2Async, wait for completion, then move on

В отличие отto:

tasks[0] = DoOperation0Async(); // start DoOperation0Async, move on without waiting for completion
tasks[1] = DoOperation1Async(); // start DoOperation1Async, move on without waiting for completion
tasks[2] = DoOperation2Async(); // start DoOperation2Async, move on without waiting for completion

await Task.WhenAll(tasks); // wait for all of them to complete

Обновление

"не await делает операцию async ... ведет себя как синхронизация, в этом примере (и не только)?Потому что мы не можем (!) Запускать что-либо еще параллельно с DoOperation0Async() в первом случае. Для сравнения: во втором случае DoOperation0Async() и DoOperation1Async() работают параллельно (например, параллелизм, основные преимущества async?) "

Это большой вопрос, и вопрос стоит задать, поскольку он является отдельной темой для SO, поскольку он отличается от первоначального вопроса о разнице между запуском и ожиданием задач - поэтому я буду продолжатьэтот ответ короток, но при необходимости направляет вас к другим ответам.

Нет, await операция async не заставляет ее вести себя как синхронизация;эти ключевые слова позволяют разработчикам писать асинхронный код, который напоминает синхронный рабочий процесс (подробнее см. этот ответ Эрика Липперта).

Вызов await DoOperation0Async() не будет блокировать поток, выполняющий этот поток кода, тогда как синхронная версия DoOperation0 (или что-то вроде DoOperation0Async.Result) заблокирует поток до завершения операции.

Подумайте об этом в веб-контексте.Допустим, запрос поступает в серверное приложение.В рамках подготовки ответа на этот запрос вам необходимо выполнить длительную операцию (например, запросить внешний API, чтобы получить какое-то значение, необходимое для получения вашего ответа).Если выполнение этой длительной операции было синхронным, поток, выполняющий ваш запрос, заблокировал бы , поскольку ему пришлось бы ждать завершения длительной операции.С другой стороны, если выполнение этой длительной операции было асинхронным, поток запроса мог быть освобожден, чтобы он мог выполнять другие действия (например, обслуживать другие запросы), пока продолжительная операция все еще выполнялась.Затем, когда длительная операция в конечном итоге завершится, поток запроса (или, возможно, другой поток из пула потоков) может получить его с того места, где он остановился (так как длительная операция будет завершена и ее результат будет теперь доступен) и выполните всю оставшуюся работу для получения ответа.

В примере серверного приложения также рассматривается вторая часть вашего вопроса об основных преимуществах async - async / await isвсе об освобождении темы .

0 голосов
/ 22 октября 2018

Разве это не просто атрибуция, по природе не связанная с действием?

Вызывая асинхронный метод, вы выполняете код внутри.Обычно по цепочке один метод создает задачу и возвращает ее либо с помощью return, либо в ожидании.

Запуск задачи

Вы можете запустить задачу с помощью Task.Run(...).Это планирует некоторую работу с пулом потоков задач.

Ожидание задачи

Чтобы получить задачу, вы обычно вызываете некоторый (асинхронный) метод, который возвращает задачу.Метод async ведет себя как обычный метод до тех пор, пока вы не await (или не используете Task.Run()).Обратите внимание, что если вы ожидаете цепочку методов и «конечный» метод выполняет только Thread.Sleep() или синхронную операцию - тогда вы заблокируете исходный вызывающий поток, потому что ни один метод никогда не использовал пул потоков задачи.

Вы можете выполнить некоторые фактические асинхронные операции различными способами:

  • с использованием Task.Run
  • с использованием Task.Delay
  • с использованием Task.Yield
  • вызов библиотеки, которая предлагает асинхронные операции

Это те, которые приходят мне в голову, вероятно, есть и другие.

В качестве примера

Предположим, что ID потока 1 - это основной поток, из которого вы звоните MethodA().Идентификаторы нитей 5 и выше являются потоками для запуска задач (для этого System.Threading.Tasks предоставляет планировщик по умолчанию).

public async Task MethodA()
{
    // Thread ID 1, 0s passed total
    var a = MethodB(); // takes 1s
    // Thread ID 1, 1s passed total
    await Task.WhenAll(a); // takes 2s
    // Thread ID 5, 3s passed total

    // When the method returns, the SynchronizationContext
    // can change the Thread - see below
}

public async Task MethodB()
{
    // Thread ID 1, 0s passed total
    Thread.Sleep(1000); // simulate blocking operation for 1s
    // Thread ID 1, 1s passed total

    // the await makes MethodB return a Task to MethodA
    // this task is run on the Task ThreadPool
    await Task.Delay(2000); // simulate async call for 2s
    // Thread ID 2 (Task's pool Thread), 3s passed total
}

Мы можем видеть, что MethodA был заблокирован на MethodB до тех пор, пока мынажмите оператор ожидания.

Await, SynchronizationContext и Console Apps

Вам следует знать об одной особенности задач.Они обязательно возвращаются к SynchronizationContext, если он есть (в основном не консольные приложения).Вы можете легко зайти в тупик при использовании .Result или .Wait() в Задаче, если вызываемый код не принимает мер.См. https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/

async / await как синтаксический сахар

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

Это не преобразованный код с использованием async / await.Ожидается метод Something, поэтому весь следующий код (Bye) будет запущен после завершения Something.

public async Task SomethingAsync()
{
    Hello();
    await Something();
    Bye();
}

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

public class Worker
{
    private Action _action;

    public event DoneHandler Done;
    // skipping defining DoneHandler delegate

    // store the action
    public Worker(Action action) => _action = action;

    public void Run()
    {
        // execute the action
        _action();

        // notify so that following code is run
        Done?.Invoke();
    }
}

Теперь наш преобразованный код не использует async / await

public Task SomethingAsync()
{
    Hello(); // this remains untouched

    // create the worker to run the "awaited" method
    var worker = new Worker(() => Something());

    // register the rest of our method
    worker.Done += () => Bye();

    // execute it
    worker.Run();

    // I left out the part where we return something
    // or run the action on a threadpool to keep it simple        
}
0 голосов
/ 21 октября 2018

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

При ожидании вы ожидаете, пока одна задача фактически завершит , прежде чем продолжить.

Не существует такой вещи, как «Огонь и незабудка».Вам всегда нужно возвращаться, реагировать на исключения или делать что-то с результатом асинхронной операции (результат запроса к базе данных или WebQuery, операция FileSystem завершена, отправка Dokument в ближайший пул принтеров).

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

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