Какова цель методов * Async в .NET Framework, учитывая возможность запуска любого метода асинхронно с помощью Task.Run?
Короткий вопрос:
Почему .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.
Ответы
Ответ 1
Маркировка методов async означает, что мы можем использовать ключевое слово ожидания, чтобы в основном планировать тяжелую обработку в другом потоке и освобождать поток пользовательского интерфейса до завершения обработки.
Это совсем не так, как работает async
. Смотрите async
intro.
Вы можете сказать, что методы * Async используют специальную магию (например, обработку внешних сигналов), которые делают их более эффективными, чем их синхронные копии. Дело в том, что я не вижу этого в этом.
В чистом асинхронном коде нет нити (как я расскажу в своем блоге). Фактически, на уровне драйвера устройства все (нетривиальные) операции ввода/вывода являются асинхронными. Это синхронные API (на уровне ОС), которые являются абстракционным слоем над естественными асинхронными API-интерфейсами.
Посмотрите, например, на Stream.ReadAsync.
Stream
- необычный случай. Как базовый класс, он должен максимально предотвращать нарушения. Поэтому, когда они добавили виртуальный метод ReadAsync
, им пришлось добавить реализацию по умолчанию. Эта реализация должна использовать неидеальную реализацию (Task.Run
), что является неудачным. В идеальном мире ReadAsync
будет (или вызывать) абстрактную асинхронную реализацию, но это нарушит каждую существующую реализацию Stream
.
Для более правильного примера сравните разницу между WebClient
и HttpClient
.
Ответ 2
Сделайте реалистичный тест: естественно асинхронный WebRequest.GetResponseAsync
vs неестественно синхронный WebRequest.GetResponse
.
Во-первых, мы расширяем стандартные пределы ThreadPool
:
ThreadPool.SetMaxThreads(MAX_REQS * 2, MAX_REQS * 2);
ThreadPool.SetMinThreads(MAX_REQS, MAX_REQS);
Примечание. Я запрашиваю то же количество workerThreads
и completionPortThreads
. Затем мы выполним MAX_REQS
= 200 параллельных запросов на bing.com, используя каждый API.
Код (автономное консольное приложение):
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using System.Net;
namespace Console_21690385
{
class Program
{
const int MAX_REQS = 200;
// implement GetStringAsync
static async Task<string> GetStringAsync(string url)
{
using (var response = await WebRequest.Create(url).GetResponseAsync())
using (var stream = response.GetResponseStream())
using (var reader = new System.IO.StreamReader(stream))
{
return await reader.ReadToEndAsync();
}
}
// test using GetStringAsync
static async Task TestWithGetStringAsync()
{
var tasks = Enumerable.Range(1, MAX_REQS).Select((i) =>
GetStringAsync("http://www.bing.com/search?q=item1=" + i));
Console.WriteLine("Threads before completion: " + Process.GetCurrentProcess().Threads.Count);
await Task.WhenAll(tasks);
Console.WriteLine("Threads after completion: " + Process.GetCurrentProcess().Threads.Count);
}
// implement GetStringSync
static string GetStringSync(string url)
{
using (var response = WebRequest.Create(url).GetResponse())
using (var stream = response.GetResponseStream())
using (var reader = new System.IO.StreamReader(stream))
{
return reader.ReadToEnd();
}
}
// test using GetStringSync
static async Task TestWithGetStringSync()
{
var tasks = Enumerable.Range(1, MAX_REQS).Select((i) =>
Task.Factory.StartNew(
() => GetStringSync("http://www.bing.com/search?q=item1=" + i),
CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default));
Console.WriteLine("Threads before completion: " + Process.GetCurrentProcess().Threads.Count);
await Task.WhenAll(tasks);
Console.WriteLine("Threads after completion: " + Process.GetCurrentProcess().Threads.Count);
}
// run either of the tests
static void RunTest(Func<Task> runTest)
{
Console.WriteLine("Threads at start: " + Process.GetCurrentProcess().Threads.Count);
var stopWatch = new Stopwatch();
stopWatch.Start();
var testTask = runTest();
while (!testTask.IsCompleted)
{
Console.WriteLine("Currently threads: " + Process.GetCurrentProcess().Threads.Count);
Thread.Sleep(1000);
}
Console.WriteLine("Threads at end: " + Process.GetCurrentProcess().Threads.Count + ", time: " + stopWatch.Elapsed);
testTask.Wait();
}
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(MAX_REQS * 2, MAX_REQS * 2);
ThreadPool.SetMinThreads(MAX_REQS, MAX_REQS);
Console.WriteLine("Testing using GetStringAsync");
RunTest(TestWithGetStringAsync);
Console.ReadLine();
Console.WriteLine("Testing using GetStringSync");
RunTest(TestWithGetStringSync);
Console.ReadLine();
}
}
}
Вывод:
Testing using GetStringAsync
Threads at start: 3
Threads before completion: 3
Currently threads: 25
Currently threads: 84
Currently threads: 83
Currently threads: 83
Currently threads: 83
Currently threads: 83
Currently threads: 83
Currently threads: 84
Currently threads: 83
Currently threads: 83
Currently threads: 84
Currently threads: 84
Currently threads: 84
Currently threads: 83
Currently threads: 83
Currently threads: 84
Currently threads: 83
Currently threads: 82
Currently threads: 82
Currently threads: 82
Currently threads: 83
Currently threads: 25
Currently threads: 25
Currently threads: 26
Currently threads: 25
Currently threads: 25
Currently threads: 25
Currently threads: 23
Currently threads: 23
Currently threads: 24
Currently threads: 20
Currently threads: 20
Currently threads: 19
Currently threads: 19
Currently threads: 19
Currently threads: 19
Currently threads: 18
Currently threads: 19
Currently threads: 19
Currently threads: 19
Currently threads: 18
Currently threads: 18
Currently threads: 18
Currently threads: 19
Currently threads: 19
Currently threads: 18
Currently threads: 19
Currently threads: 19
Currently threads: 18
Currently threads: 18
Currently threads: 17
Threads after completion: 17
Threads at end: 17, time: 00:00:51.2605879
Testing using GetStringSync
Threads at start: 15
Threads before completion: 15
Currently threads: 55
Currently threads: 213
Currently threads: 213
Currently threads: 213
Currently threads: 213
Currently threads: 213
Currently threads: 213
Currently threads: 213
Currently threads: 213
Currently threads: 212
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 210
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 209
Currently threads: 205
Currently threads: 201
Currently threads: 196
Currently threads: 190
Currently threads: 186
Currently threads: 182
Threads after completion: 178
Threads at end: 173, time: 00:00:47.2603652
Результат:
Оба теста занимают около 50 секунд, но GetStringAsync
достигает максимума в 83 потоках, а GetStringSync
- в 213. Чем выше значение MAX_REQS
, тем больше потоков тратится на блокирующий WebRequest.GetResponse
API.
@Арк-кун, надеюсь, вы сейчас видите это.