В чем разница между этими двумя ждущими звонками - PullRequest
2 голосов
/ 11 декабря 2019

У меня есть следующий код:

async Task Main()
{
    Stopwatch sw = new Stopwatch();

    // Variante 1
    sw.Start();
    var m1 = await Task1();
    var m2 = await Task2();
    var m3 = await Task3();
    Console.WriteLine(m1);
    Console.WriteLine(m2);
    Console.WriteLine(m3);
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds);

    // Variante 2
    sw.Restart();
    var t1 = Task1();
    var t2 = Task2();
    var t3 = Task3();
    m1 = await t1;
    m2 = await t2;
    m3 = await t3;
    Console.WriteLine(m1);
    Console.WriteLine(m2);
    Console.WriteLine(m3);
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds);
}

// Define other methods, classes and namespaces here
public async Task<string> Task1()
{
    await Task.Delay(5000);
    return "Task1 ready";
}   

public async Task<string> Task2()
{
    await Task.Delay(5000);
    return "Task2 ready";
}

public async Task<string> Task3()
{
    await Task.Delay(5000);
    return "Task3 ready";
}

Первая часть дает почти 15000 мс, а вторая - 5000 мс, но я не понимаю, почему!

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

Чего мне не хватает?

Ответы [ 5 ]

5 голосов
/ 11 декабря 2019

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

4 голосов
/ 11 декабря 2019

Другие ответы частично верны, но я хотел написать свой собственный ответ, чтобы что-то прояснить.

Задачи не запускаются параллельно.

Строгоговоря «параллельно» означает, что две строки кода оцениваются одновременно. Этого здесь не происходит.

Каждый асинхронный метод запускается синхронно. Магия происходит, когда await действует на неполный Task (например, когда запускается запрос ввода-вывода). В этот момент метод возвращает свой собственный неполный Task, и продолжение метода запланировано на более позднее время.

Когда вы сделаете это:

var t1 = Task1();
var t2 = Task2();
var t3 = Task3();

Вот что происходит:

  1. Task1 начинает выполняться.
  2. При первом await, Task1 возвращает Task.
  3. Task2 начинает выполнение.
  4. При первом await, Task2 возвращает Task.
  5. Task3 начинает выполнение.
  6. При первом await, Task3 возвращает Task.

Итак, что делает этот запуск быстрее, так как ваш код использует время, пока Task1 ожидает ответа для запуска запроса в Task2.


продолжение этих задач может выполняться параллельно в зависимости от обстоятельств.

В ситуации, когда существует контекст синхронизации(как ASP.NET), продолжения должны возвращаться в один и тот же контекст, и ничто не будет работать параллельно. Это означает, что выполнение Task1 не будет продолжаться до тех пор, пока ничего не будет запущено в этом контексте. В вашем коде это происходит по адресу:

m1 = await t1;

Только после этой строки текущий контекст освобождается, в этом контексте может выполняться продолжение Task1, а когда это будет сделано, все после await t1добавляется в список «дел» для завершения.

Если вы работаете в ситуации, когда отсутствует контекст синхронизации (например, ASP.NET Core ), или вы указываете .ConfigureAwait(false) чтобы сообщить, что вам не нужно возвращаться в тот же контекст, продолжения задач будут выполняться в потоках ThreadPool. Это означает, что продолжения всех трех задач могут выполняться параллельно в разных потоках. Если это произойдет, то к тому времени, когда вы нажмете await t1, это уже может быть сделано.


Если вы сделаете это:

var m1 = await Task1();
var m2 = await Task2();
var m3 = await Task3();

Вы ждете, пока не закончится Task1полностью, прежде чем даже начать Task2. Это зависит от вашего приложения. Например, в ASP.NET он освобождает поток, который будет использоваться другим не связанным запросом. В настольном приложении он может освободить поток пользовательского интерфейса для ответа на ввод пользователя.

1 голос
/ 11 декабря 2019

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

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

1 голос
/ 11 декабря 2019

A Task запускается, как только вы вызываете метод. Вы звоните Task1, Task2 и Task3, не ожидая ни одного из них, поэтому они все начинают работать одновременно.

Во второй части вы звоните Task1, а затем ожидаете его,перед звонком Task2 и Task3. Это означает, что Task2 не начинается до тех пор, пока не закончится Task1, а Task3 не начинается до тех пор, пока не закончится Task2. Они бегут один за другим, растягивая общее время бега.

0 голосов
/ 11 декабря 2019

Проблема здесь в том, что вы включаете JIT-компиляцию в свои измерения.

Вы пытались переключить первый вариант со вторым, чтобы посмотреть, как он измеряет?

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

Чтобы эффективно измерить производительность, вам нужно использовать что-то вроде BenchMarkDotNet .

...