Я много слышал о том, что новый поток не создается с асинхронным ожиданием. Я решил проверить, что будет, если в основной функции и асинхронной функции есть while(true).
namespace asyncTest
{
public class Program
{
public static void Task1()
{
while (true) {
Console.WriteLine("Task1: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
}
}
public static async void async()
{
Console.WriteLine("async start: " + Thread.CurrentThread.ManagedThreadId);
await Task.Run(Task1);
Console.WriteLine("async end: " + Thread.CurrentThread.ManagedThreadId);
}
static void Main(string[] args)
{
async();
while (true) {
Console.WriteLine("main: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
}
}
}
}
Но по какой-то причине мои функции выполняются в разных потоках:
Я хочу понять, почему они выполняются в разных потоках, если async await не создает поток. И я хочу знать, как высоконагруженные операции выполняются асинхронно, как в этом примере. Кто выполняет асинхронную операцию в дополнительном потоке? Или они выполняются в одном потоке?
По умолчанию async
/await
не создают новые темы. Скорее, они позволяют нам эффективно использовать время простоя существующего потока: время ожидания дискового ввода-вывода, сетевого ввода-вывода, пользовательского ввода, задержки памяти, обновления экрана, ожидания объектов Task и т. д.
Однако очереди Task.Run()
работают в ThreadPool. Это создает новый поток.
Теперь мы знаем достаточно, чтобы следовать этому коду и понять вывод.
Мы видим, что код начинается с вызова метода async()
. Этот метод сначала выводит сообщение на консоль, все еще из исходного потока. Затем он вызывает метод Task1()
в новом потоке через Task.Run()
. Код в методе async()
никогда не достигнет следующей строки для записи конечного сообщения в консоль, потому что он пытается await
получить результат Task1()
, который никогда не выйдет из цикла while (true)
.
Однако мы также продолжаем видеть результаты обоих методов: Main()
и Task1()
. Это связано с тем, что метод async()
объявлен как async
, но не вызывается с помощью await
. Это позволяет методу Main()
, который его вызвал, продолжать работу во время простоя в исходном потоке. Мы также видим "async start"
и "Task1"
в выводе первыми, перед "main"
, потому что Main()
не может это сделать (нет времени простоя), пока мы не достигнем первого Thread.Sleep()
в Task1()
.
Я также хотел бы поздравить вас с созданием действительно хорошего учебного примера, охватывающего большинство основных частей асинхронного кода в одном довольно небольшом примере (поддержал вопрос). Единственное, что я бы сделал по-другому, — это разместил бы результат в виде текста, а не изображения, и использовал бы имя, отличное от async()
... но даже здесь стоит помнить, что async
— это всего лишь условное ключевое слово.
Я много слышал о том, что новый поток не создается с асинхронным ожиданием.
Процитируем классическую Нити нет Стивена Клири (очень рекомендую к прочтению, курсив мой):
Это основная истина асинхронности в ее самой чистой форме: потоков нет.
Противников этой истины множество. «Нет, — кричат они, — если я ожидаю операции, должен быть поток, ожидающий! Вероятно, это поток пула потоков. Или тема ОС! Или что-то с драйвером устройства…»
Не обращайте внимания на эти крики. Если асинхронная операция чистая, то потока нет.
Однако это не означает, что потоки вообще не задействованы.
Прежде всего вам нужно знать, что существует два «основных» асинхронных сценария: с привязкой к вводу-выводу и с привязкой к ЦП. Из Сценарии асинхронного программирования:
Ядром асинхронного программирования являются объекты
Task
иTask<T>
, моделирующие асинхронные операции. Они поддерживаются ключевыми словами async и await. В большинстве случаев модель довольно проста:
- Для кода, связанного с вводом-выводом, вы ожидаете операции, которая возвращает задачу или задачу внутри асинхронного метода.
- Для кода, привязанного к ЦП, вы ожидаете операции, которая запускается в фоновом потоке с помощью метода
Task.Run
.
Ваш пример в основном имитирует сценарий с привязкой к ЦП посредством бесконечного цикла с Thread.Sleep
в Task1
. Для выполнения требуется поток, поскольку Thread.Sleep
не является асинхронной операцией:
Приостанавливает текущий поток на указанное время.
Другой случай, когда тред необходим — для завершения продолжения (т.е. того, что написано после await
), но тред может быть создан, а может и не быть создан здесь — в этом случае этим управляет ThreadPool (см. также управляемый пул потоков документ):
Предоставляет пул потоков, который можно использовать для выполнения задач, публикации рабочих элементов, обработки асинхронного ввода-вывода, ожидания от имени других потоков и таймеров обработки.
который управляет потоками и будет повторно использовать их, если они есть.
Обратите внимание, что в этом случае не будет создан дополнительный поток для выполнения «ожидания» для каждого используемого await
.
Что касается операций, связанных с вводом-выводом, вы можете смоделировать асинхронную операцию, например, с помощью Task.Delay()
. Рассмотрим следующую модификацию вашего кода:
static async Task Main(string[] args)
{
Task1(100);
Task1(155);
Task1(205);
Task1(255);
Task1(305);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("main: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(500);
}
}
public static async Task Task1(int i)
{
while (true) {
Console.WriteLine($"Task1 {i}: " + Thread.CurrentThread.ManagedThreadId);
// Thread.Sleep(1000);
await Task.Delay(1001 + i);
}
}
в зависимости от нескольких факторов вы можете увидеть меньше уникальных потоков на выходе, чем одновременно «выполняемые» асинхронные операции.
Примечания:
Здесь очень много нюансов. Например:
Для операции с привязкой к вводу-выводу она должна быть реализована правильно и базовая система/устройство/драйвер ее поддерживает. Например, драйвера Oracle довольно долгое время не было — см. Может ли управляемый драйвер Oracle правильно использовать async/await?
Выполнение продолжения тоже сложнее — оно включает в себя несколько факторов, в том числе наличие SynchronizationContext . См. также ответы на вопрос В каком потоке выполняется код после ключевого слова `await`?
async void
Постарайтесь избегать этого, в большинстве случаев так и должно быть async Task
(за исключением обработчиков событий, которые обычно используются в настольных/мобильных приложениях).
Смотрите также:
хотя есть поток io, ожидающий. Может быть, это более эффективно, но нить все равно есть.
@IvanPetrov, можешь поподробнее?
@GuruStron, написав полноценный ответ, я бы не сказал, что Threadpool
удается продолжить асинхронную операцию. Для этого есть две основные сущности: исторически впервые возникшая SynchronizationContext
, чья ответственность заключается в буквальном распределении делегатов продолжения (см. раздел Забота о контексте), и возникшая позднее Task-native TaskSchedulers.
Что касается контекста синхронизации, S.Toub однажды опубликовал статью о написании асинхронного канала, который выполняет весь асинхронный стек в одном потоке. Кстати, полезно знать для учебных целей.
@Райан немного изменил формулировку, спасибо!
@GuruStron из гиперболической статьи No Thread: он уже зарегистрировал дескриптор в порте завершения ввода-вывода (IOCP), который является частью пула потоков. Таким образом, поток пула потоков ввода-вывода ненадолго заимствован для выполнения APC, который уведомляет задачу о ее завершении.
Кстати, имена методов в C# — PascalCased.
async()
неправильно.Async()
правильно.