3

В продолжение вот этого вопроса.

Итак, пусть есть несколько асинхронных методов

async Task DoFoo() { ... }
async Task DoBar() { ... }
async Task DoBaz() { ... }

которые в другом асинхронном методе

async Task DoJob()
{
    //TODO: perform DoFoo, DoBar and DoBaz in parallel
}

требуется выполнить параллельно.

Если следовать ответам в упомянутом вопросе, то для параллельности выполнения нескольких асинхронных методов нужно вызвать их так:

async Task DoJob()
{
    Task t1 = DoFoo();
    Task t2 = DoBar();
    Task t3 = DoBaz();

    await Task.WhenAll(t1, t2, t3);
}

В документации по TaskScheduler, однако, читаю:

Top-level tasks, which are tasks that are not created in the context of another task, are put on the global queue just like any other work item. However, nested or child tasks, which are created in the context of another task, are handled quite differently. A child or nested task is put on a local queue that is specific to the thread on which the parent task is executing. The parent task may be a top-level task or it also may be the child of another task. When this thread is ready for more work, it first looks in the local queue.

Применительно к ситуации, если я правильно понимаю, задачи t1, t2, t3 не являются задачами корневого уровня (т.к. они вложены в DoJob), и потому, стартуя они попадают не в global queue, которая обрабатывается множеством потоков пула, а - в local queue какого-то одного конкретного потока.

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

Да, может произойти work stealing, но стоит ли на него надеяться, или для достижения параллелизма лучше все методы обернуть в Task.Run(...) (который отправит задачи исполняться на пуле потоков, т.е., по-видимому, в global queue)

async Task DoJob()
{
    Task t1 = Task.Run(DoFoo);
    Task t2 = Task.Run(DoBar);
    Task t3 = Task.Run(DoBaz);

    await Task.WhenAll(t1, t2, t3);
}

?

i-one
  • 8,531
  • 2
  • 22
  • 35

1 Answers1

10

Прям вот жирным выделю:

Асинхронность != многопоточность. Таски != многопоточность.

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

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

"Обычный" асинхронный метод состоит из некоторого синхронного пролога, одного или несколько асинхронных вызовов с синхронным кодом между ними, и синхронного эпилога. В таком методе время выполнение синхронного кода и асинхронного соотносятся как, например, 1 к 100 (потому что IO медленный, очень медленный).

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

Так что да, параллельно запущенные асинхронные методы могут выполняться и без использования дополнительных потоков, но такое выполнение по-прежнему имеет смысл. Особенно в том случае, когда эти методы являются настоящими асинхронным методами.

По теме:

andreycha
  • 25,167
  • 4
  • 46
  • 82
  • Ну и надо еще добавить, что nested or child tasks - это совсем другой механизм, не связанный с приведенным автором кодом. – Pavel Mayorov Feb 12 '18 at 16:17
  • @PavelMayorov, Task t1 = DoFoo(); разве не порождает detached child task? – i-one Feb 12 '18 at 16:30
  • @andreycha, В вопросе я вроде бы не уравнивал вещи, выделенные !=. Единственное, что кажется я уравнял - это многопоточность и параллельность, но таково положение вещей и в исходном вопросе. – i-one Feb 12 '18 at 16:32
  • @i-one в первую очередь речь идет об асинхронности. В таком случае вообще не нужно ничего сравнивать с многопоточностью. А параллельная она или последовательная, это неважно. – andreycha Feb 12 '18 at 16:48
  • @i-one нет, это совершенно другой тип задачи. К примеру, такую задачу вы никогда не "поймаете" через Task.CurrentId – Pavel Mayorov Feb 13 '18 at 06:33
  • @PavelMayorov, а чему, кстати, эквивалентно Task t1 = DoFoo();, это как-то представимо через Task.Factory.StartNew(...) или var t = new Task(...); t.Start();? – i-one Feb 13 '18 at 06:39
  • @i-one нет, это работает через TaskCompletionSource – Pavel Mayorov Feb 13 '18 at 06:41
  • @PavelMayorov, хорошо, но в итоге задачи, порождённые таким способом, исполняются на TaskScheduler.Default или как-то иначе? Из описания класса TaskScheduler сложилось именно такое впечателние. И даже если Task t1 = DoFoo(); нельзя назвать child, очень похоже, однако, что процитированное имеет место быть, t1, t2, t3 попадают по-видимому именно в local queue, т.е. имеет место быть однопоточное последовательное либо квазипараллельное (одним потоком) исполнение методов. – i-one Feb 13 '18 at 07:01
  • "Т.о., запуская параллельно N асинхронных методов..." так в том то и дело, что исходная конструкция, по-видимому, не запускает асинхронные методы параллельно. Например, если второй метод содержит в своём прологе cpu-intensive часть, то третий метод не стартует, пока она не исполнится. Мой вопрос как раз в большей степени про параллельность/многопоточность, чем про асинхронность. – i-one Feb 13 '18 at 07:09
  • 2
    @i-one задачи порожденные таким методом исполняются там где вы их исполняете – Pavel Mayorov Feb 13 '18 at 07:18
  • @i-one нет никакого TaskScheduler.Default, забудьте про него. "t1, t2, t3 попадают по-видимому именно в local queue" - нет, не попадают. – Pavel Mayorov Feb 13 '18 at 07:20