Что является более эффективным - Task.Run с двумя ожидаемыми задачами ввода-вывода или классический подход Fork / Join? - PullRequest
2 голосов
/ 08 ноября 2019

Предположим, что у нас есть 2 задачи, связанные с вводом / выводом, которые необходимо обработать, для N элементов. Мы можем назвать 2 задачи A и B . B можно запустить только после того, как A дал результат.

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

Задача. Способ запуска:

List<Task> workers = new List<Task>();
for (int i = 0; i < N; i++)
{
    workers.Add(Task.Run(async () =>
    {
        await A(i);
        await B(i);
    }
}
await Task.WhenAll(workers);

Классическая вилка / соединение:

List<Task> workersA = new List<Task>();
List<Task> workersB = new List<Task>();
for (int i = 0; i < N; i++)
{
    workersA.Add(A(i));
}

await Task.WhenAll(workersA);

for (int i = 0; i < N; i++)
{
    workersB.Add(B(i));
}

await Task.WhenAll(workersB);

В качестве альтернативы это можно сделатьтакже следующим образом:

List<Task> workers = new List<Task>();

for (int i = 0; i < N; i++)
{
    workers.Add(A(i));
}

for (int i = 0; i < N; i++)
{
    await workers[i];
    workers[i] = B(i);
}

await Task.WhenAll(workers);

Меня беспокоит то, что в следующих документах MSDN указано, что мы никогда не должны использовать Task.Run для операций ввода-вывода.

Принимая это во внимание, каков наилучший подход к рассмотрению этого случая?

Поправьте меня, если я ошибаюсь, но мы хотим избежать использования Task.Run, потому что мы эффективно ставим потоки в очередь для обработки работы, где, если мы просто используем await, там не будетнить . (Из-за операций ввода-вывода.)

Я действительно хочу пойти по маршруту Task.Run, но если он в конечном итоге использует потоки без видимой причины / делает дополнительные издержки, то это неидти.

Ответы [ 2 ]

4 голосов
/ 08 ноября 2019

Я действительно хочу пойти по маршруту Task.Run

Почему?

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

Документация гласит:

Поставляет в очередь заданную работу для выполнения в ThreadPool

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

Вы не можете контролировать, сколько потоков создано для выполнения всей этой работы. Но рекомендация не использовать Task.Run для операций ввода-вывода является обоснованной. Это ненужные накладные расходы без выгоды. Это будет менее эффективным.

Любое из ваших других решений будет работать нормально. Ваше последнее решение может закончиться быстрее, так как вы начинаете звонки на B() раньше (вы ждете, пока завершится первый A(), прежде чем начинать звонить B(), вместо того, чтобы ждать их завершения).

Обновление на основе ответа Теодора: Мы оба правы :) Важно знать, что весь код в асинхронном методе до первого ждут (и код после, если толькоВы указываете иначе) будет работать в том же контексте, с которого он был запущен. В настольном приложении это поток пользовательского интерфейса. ожидание является асинхронным. Таким образом, поток пользовательского интерфейса освобождается во время ожидания . Но если в этом методе есть какая-то нагрузка на процессор, он заблокирует поток пользовательского интерфейса.

Поэтому Теодор говорит, что вы можете использовать Task.Run, чтобы как можно скорее вывести его из потока пользовательского интерфейса и гарантировать, что он никогда не будетзаблокировать поток пользовательского интерфейса. Хотя это правда, вы не можете слепо использовать этот совет везде. Во-первых, вам может потребоваться сделать что-то в пользовательском интерфейсе после операции ввода-вывода, и что должно быть выполнено в потоке пользовательского интерфейса. Если вы запустили его с Task.Run, вы должны обязательно выполнить маршалл обратно в поток пользовательского интерфейса для этой работы.

Но если вызываемый вами асинхронный метод имеет достаточную работу с привязкой к процессору, он зависаетпользовательский интерфейс, то это не строго операция ввода-вывода, а рекомендация «Использовать Task.Run для работы с процессором и async / await для ввода-вывода» по-прежнему подходит.

ВсеЯ могу сказать это: Попробуйте . Если вы обнаружите, что все, что вы делаете, замораживает пользовательский интерфейс, используйте Task.Run. Если вы обнаружите, что это не так, то Task.Run - это ненужные накладные расходы (не так много, заметьте, но все равно не нужно, но становится хуже, если вы делаете это в цикле, как вы).

И все это действительно относится к настольным приложениям. Если вы находитесь в ASP.NET, Task.Run ничего не сделает для вас, если вы не попытаетесь сделать что-то параллельно. В ASP.NET нет «потока пользовательского интерфейса», поэтому не имеет значения, над каким потоком вы работаете. Вам просто нужно убедиться, что вы не блокируете поток во время ожидания (поскольку в ASP.NET имеется ограниченное количество потоков).

2 голосов
/ 08 ноября 2019

Если у вас есть работа I / O-bound , используйте async и await без Task.Run. Вы не должны использовать параллельную библиотеку задач. Причина этого изложена в статье Async in Depth .

Этот совет, хотя взят с сайта Microsoft , вводит в заблуждение,Отговаривая Task.Run для операций ввода-вывода, автор, вероятно, имел в виду следующее:

var data = await Task.Run(() =>
{
    return webClient.DownloadString(url); // Blocking call
});

... что действительно плохо, поскольку блокирует поток пула потоков. Но использование Task.Run с асинхронным делегатом совершенно нормально:

var data = await Task.Run(async () =>
{
    return await webClient.DownloadStringTaskAsync(url); // Async call
});

На самом деле , на мой взгляд , это предпочтительный способ инициирования асинхронных операций из обработчиков событий приложения пользовательского интерфейса,потому что это гарантирует, что поток пользовательского интерфейса будет немедленно освобожден. Если вместо этого вы будете следовать советам статьи и пропустите Task.Run:

private async void Button1_Click(object sender, EventArgs args)
{
    var data = await webClient.DownloadStringTaskAsync(url);
}

... тогда вы рискуете, что асинхронный метод может не быть на 100% асинхронным и может заблокировать поток пользовательского интерфейса. Это небольшая проблема для встроенных асинхронных методов, таких как DownloadStringTaskAsync, которая написана экспертами, но становится все более важной для сторонних асинхронных методов и еще более важной для асинхронных методов, написанныхСами разработчики!

Итак, что касается вариантов вашего вопроса, я считаю, что первый (способ Task.Run) является самым безопасным и наиболее эффективным. Второй будет ожидать отдельно все задачи A и B, поэтому продолжительность будет в лучшем случае Max (A) + Max (B). Который по статистике должен быть длиннее, чем Макс (A + B).

...