Короткий вопрос:
Почему .Net Framework добавила много асинхронных версий метода вместо разработчиков, просто используя Task.Run
для асинхронного запуска синхронных методов?
Подробный вопрос:
- Я понимаю концепцию асинхроничности.
- Я знаю о
Tasks
- Я знаю о ключевыхх async/wait.
- Я знаю, что * методы Async в .NET Framework делают.
То, что я не понимаю, является целью методов * Async в библиотеке.
Предположим, что у вас есть две строки кода:
F1();
F2();
В отношении потока данных/управления существует только два случая:
-
F2
необходимо выполнить после завершенияF1
. -
F2
не нужно ждать завершенияF1
.
Я не вижу других случаев. Я не вижу никакой общей необходимости знать конкретный поток, который выполняет какую-то функцию (кроме пользовательского интерфейса). Базовый режим выполнения кода в потоке является синхронным. Для parallelism требуется несколько потоков. Асинхронизация основана на parallelism и переупорядочении кода. Но база все еще синхронна.
Разница не имеет значения, когда рабочая нагрузка F1
мала. Но когда A занимает много времени, чтобы закончить, нам может понадобиться посмотреть на ситуацию, и, если F2
не нужно ждать завершения F1
, мы можем запустить F1
параллельно с F2
.
Давным-давно мы сделали это с помощью потоков/потоков. Теперь имеем Tasks
.
Если мы хотим параллельно запустить F1
и F2
, мы можем написать:
var task1 = Task.Run(F1);
F2();
задачи классные, и мы можем использовать await
в тех местах, где нам, наконец, нужно завершить задачу.
До сих пор я не вижу необходимости создавать метод F1Async()
.
Теперь посмотрим на некоторые особые случаи.
Единственный реальный частный случай, который я вижу, - это пользовательский интерфейс. Поток пользовательского интерфейса является особенным и останавливается, что заставляет замораживать пользовательский интерфейс, который является плохим.
Как я вижу, Microsoft советует нам отмечать обработчики событий пользовательского интерфейса async
. Маркировка методов async
означает, что мы можем использовать ключевое слово await
, чтобы в основном планировать тяжелую обработку на другом потоке и освобождать поток пользовательского интерфейса до завершения обработки.
То, что я не получаю снова, - это то, зачем нам нужны любые методы Async, чтобы их можно было ждать. Мы всегда можем просто написать await Task.Run(F1);
. Зачем нам нужно F1Async
?
Вы можете сказать, что методы * Async используют специальную магию (например, обработку внешних сигналов), которые делают их более эффективными, чем их синхронные копии. Дело в том, что я не вижу этого в этом.
Посмотрим, например, на Stream.ReadAsync
. Если вы посмотрите на исходный код, ReadAsync
просто тратит несколько сотен строк кода колоколов и свистков, чтобы создать задачу, которая просто вызывает синхронный метод Read
. Зачем тогда это нужно? Почему бы просто не использовать Task.Run
с Stream.Read
?
Вот почему я не понимаю необходимости раздувать библиотеки, создавая тривиальные * Async копии синхронных методов. MS могла бы даже добавить синтаксический сахар, чтобы мы могли написать await async Stream.Read
вместо await Stream.ReadAsync
или Task.Run(Stream.Read)
.
Теперь вы можете спросить: "Почему бы не сделать методы * Async единственными и удалить синхронные методы?". Как я сказал ранее, режим выполнения базового кода является синхронным. Легко запускать синхронный метод асинхронно, но не наоборот.
Итак, какова цель методов * Async в .NET Framework, учитывая возможность запуска любого метода асинхронно с помощью Task.Run?
P.S. Если незамерзание пользовательского интерфейса так важно, почему бы не просто запустить обработчики async по умолчанию и предотвратить любую возможность замораживания?
Аргумент "без потоков":
Люди, отвечающие на этот вопрос, похоже, подразумевают, что преимущество методов Async состоит в том, что они эффективны, потому что они не создают новые потоки. Проблема в том, что я не вижу такого поведения. Параллельные асинхронные задачи ведут себя так же, как я думал, - поток создается (или берется из пула потоков) для каждой параллельной задачи (не все задачи выполняются параллельно).
Вот мой тестовый код:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication32167 {
class Program {
static async Task TestAsync() {
var httpClient = new HttpClient() { Timeout = TimeSpan.FromMinutes(20) };
var tasks = Enumerable.Range(1, 100).Select((i) =>
httpClient.GetStringAsync("http://localhost/SlowWebsite/"));
Console.WriteLine("Threads before completion: " + Process.GetCurrentProcess().Threads.Count);
await Task.WhenAll(tasks);
Console.WriteLine("Threads after completion: " + Process.GetCurrentProcess().Threads.Count);
}
static void Main(string[] args) {
Console.WriteLine("Threads at start: " + Process.GetCurrentProcess().Threads.Count);
var timer = new Stopwatch();
timer.Start();
var testTask = TestAsync();
var distinctThreadIds = new HashSet<int>();
while (!testTask.IsCompleted) {
var threadIds = Process.GetCurrentProcess().Threads.OfType<ProcessThread>().Select(thread => thread.Id).ToList();
distinctThreadIds.UnionWith(threadIds);
Console.WriteLine("Current thread count: {0}; Cumulative thread count: {1}.", threadIds.Count, distinctThreadIds.Count);
Thread.Sleep(250);
}
testTask.Wait();
Console.WriteLine(timer.Elapsed);
Console.ReadLine();
}
}
}
Этот код пытается запустить 100 HttpClient.GetStringAsync
задач, отправляющих запросы на веб-сайт, на которые требуется 1 минута ответа. В то же время он подсчитывает количество активных потоков и кумулятивное число разных, созданных процессом. Как я и предсказывал, эта программа создает много новых потоков. Результат выглядит следующим образом:
Current thread count: 4; Cumulative thread count: 4.
....
Current thread count: 25; Cumulative thread count: 25.
....
Current thread count: 7; Cumulative thread count: 63.
Current thread count: 9; Cumulative thread count: 65.
00:10:01.9981006
Это означает, что:
- В ходе выполнения async-задачи создаются 61 новый поток.
- Максимальное число новых активных потоков - 21.
-
Выполнение занимает 10 раз больше времени (10 минут вместо 1).Это было вызвано локальными ограничениями IIS.