Если я правильно понял, после завершения операции ввода-вывода, он должен уведомить фактический процесс, который выполнил операцию ввода-вывода.
Означает ли это, что он захватывает новый поток пула потоков для каждой завершенной операции ввода-вывода? Или это выделенное количество потоков для этого?
Означает ли это, что у меня будет 1000 потоков ниток IOCP одновременно (вроде), запущенных здесь, когда все будет завершено?
Ответ 1
Это немного широк, поэтому позвольте мне просто рассмотреть основные моменты:
Потоки IOCP находятся в отдельном пуле потоков, так сказать, - о настройке потоков ввода-вывода. Поэтому они не сталкиваются с потоками нитей нитей пользователя (например, те, что у вас есть в обычных операциях await
или ThreadPool.QueueWorkerItem
).
Как и обычный пул потоков, он будет медленно выделять новые потоки с течением времени. Таким образом, даже если есть пик асинхронных ответов, которые происходят все сразу, у вас не будет 1000 потоков ввода-вывода.
В правильно асинхронном приложении у вас не будет больше, чем количество ядер, дайте или возьмите, как с рабочими потоками. Это потому, что вы либо выполняете значительную работу с ЦП, и отправляете его на обычный рабочий поток, либо работаете с I/O, и вы должны делать это как асинхронную операцию.
Идея заключается в том, что вы тратите очень мало времени на обратный вызов ввода-вывода - вы не блокируете, и вы не выполняете много работы с ЦП. Если вы нарушите это (скажем, добавьте Thread.Sleep(10000)
к вашему обратному вызову), то да,.NET будет создавать тонны и тонны потоков ввода-вывода с течением времени, но это просто неправильное использование.
Теперь, как отличны потоки ввода-вывода от обычных потоков ЦП? Они почти одинаковы, они просто ждут другого сигнала - оба являются (упрощение) только циклом while
над методом, который дает управление, когда новый рабочий элемент помещается в очередь другой частью приложения (или ОПЕРАЦИОННЫЕ СИСТЕМЫ). Основное отличие состоит в том, что потоки ввода-вывода используют очередь IOCP (управляемая ОС), тогда как обычные рабочие потоки имеют собственную очередь, полностью управляемую .NET и доступную программисту приложений.
В качестве примечания, не забывайте, что ваш запрос мог быть выполнен синхронно. Возможно, вы читаете из потока TCP в цикле while, 512 байт за раз. Если в буфере сокета имеется достаточно данных, несколько ReadAsync
могут немедленно возвращаться без каких-либо переключений потоков. Это обычно не проблема, потому что I/O имеет тенденцию быть наиболее трудоемким материалом, который вы делаете в типичном приложении, поэтому не нужно ждать ввода-вывода, как правило, хорошо. Однако неправильный код в зависимости от какой-либо части, выполняемой асинхронно (даже если это не гарантируется), может легко разорвать ваше приложение.
Ответ 2
Означает ли это, что он захватывает новый поток пула потоков для каждого завершена операция ввода-вывода? Или это выделенное количество потоков для это?
Было бы ужасно неэффективно создавать новый поток для каждого запроса ввода-вывода, чтобы победить цель. Вместо этого среда выполнения запускается с небольшим количеством потоков (точное число зависит от вашей среды) и добавляет и удаляет рабочие потоки по мере необходимости (точный алгоритм для этого также зависит от вашей среды). Когда-либо крупная версия .NET видела изменения в этой реализации, но основная идея остается прежней: среда выполнения делает все возможное, чтобы создавать и поддерживать только столько потоков, сколько необходимо для эффективного обслуживания всех операций ввода-вывода. В моей системе (Windows 8.1,.NET 4.5.2) новое консольное приложение имеет только 3 потока при вводе Main
, и это число не увеличивается до тех пор, пока не будет запрошена фактическая работа.
Означает ли это, что у меня будет 1000 потоков ниток IOCP одновременно (вроде) работает здесь, когда все закончено?
Нет. Когда вы выдаете запрос ввода-вывода, поток будет ждать в порт завершения, чтобы получить результат, и вызвать любой обратный вызов, который был зарегистрирован для обработки результата (будь то с помощью метода BeginXXX
или продолжения задачи). Если вы используете задачу и не ждете ее, эта задача просто заканчивается там, и поток возвращается в пул потоков.
Что, если вы этого дожидаете? Результаты 1000 запросов ввода-вывода не будут поступать в одно и то же время, поскольку прерывания не все поступают в одно и то же время, но пусть говорят, что интервал намного короче времени, необходимого для их обработки. В этом случае пул потоков будет продолжать разворачивать потоки, чтобы обрабатывать результаты, пока не достигнет максимума, и любые дальнейшие запросы в очереди будут завершены в порт завершения. В зависимости от того, как вы его настроите, эти потоки могут занять некоторое время.
Рассмотрим следующую (преднамеренно ужасную) игрушечную программу:
static void Main(string[] args) {
printThreadCounts();
var buffer = new byte[1024];
const int requestCount = 30;
int pendingRequestCount = requestCount;
for (int i = 0; i != requestCount; ++i) {
var stream = new FileStream(
@"C:\Windows\win.ini",
FileMode.Open, FileAccess.Read, FileShare.ReadWrite,
buffer.Length, FileOptions.Asynchronous
);
stream.BeginRead(
buffer, 0, buffer.Length,
delegate {
Interlocked.Decrement(ref pendingRequestCount);
Thread.Sleep(Timeout.Infinite);
}, null
);
}
do {
printThreadCounts();
Thread.Sleep(1000);
} while (Thread.VolatileRead(ref pendingRequestCount) != 0);
Console.WriteLine(new String('=', 40));
printThreadCounts();
}
private static void printThreadCounts() {
int completionPortThreads, maxCompletionPortThreads;
int workerThreads, maxWorkerThreads;
ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads);
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
Console.WriteLine(
"Worker threads: {0}, Completion port threads: {1}, Total threads: {2}",
maxWorkerThreads - workerThreads,
maxCompletionPortThreads - completionPortThreads,
Process.GetCurrentProcess().Threads.Count
);
}
В моей системе (которая имеет 8 логических процессоров) вывод выглядит следующим образом (результаты могут отличаться в вашей системе):
Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 0, Completion port threads: 8, Total threads: 12
Worker threads: 0, Completion port threads: 9, Total threads: 13
Worker threads: 0, Completion port threads: 11, Total threads: 15
Worker threads: 0, Completion port threads: 13, Total threads: 17
Worker threads: 0, Completion port threads: 15, Total threads: 19
Worker threads: 0, Completion port threads: 17, Total threads: 21
Worker threads: 0, Completion port threads: 19, Total threads: 23
Worker threads: 0, Completion port threads: 21, Total threads: 25
Worker threads: 0, Completion port threads: 23, Total threads: 27
Worker threads: 0, Completion port threads: 25, Total threads: 29
Worker threads: 0, Completion port threads: 27, Total threads: 31
Worker threads: 0, Completion port threads: 29, Total threads: 33
========================================
Worker threads: 0, Completion port threads: 30, Total threads: 34
Когда мы выдаем 30 асинхронных запросов, пул потоков быстро делает 8 потоков доступными для обработки результатов, но после этого он только разворачивает новые потоки с неспешным темпом около 2 в секунду. Это демонстрирует, что если вы хотите правильно использовать системные ресурсы, вы должны убедиться, что ваша обработка ввода-вывода завершается быстро. В самом деле, позвольте нам изменить наш делегат на следующий, который представляет собой "правильную" обработку запроса:
stream.BeginRead(
buffer, 0, buffer.Length,
ar => {
stream.EndRead(ar);
Interlocked.Decrement(ref pendingRequestCount);
}, null
);
Результат:
Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 0, Completion port threads: 1, Total threads: 11
========================================
Worker threads: 0, Completion port threads: 0, Total threads: 11
Опять же, результаты могут отличаться в вашей системе и в разных сценариях. Здесь мы едва можем увидеть потоки портов завершения в действии, в то время как 30 запросов, которые мы выпустили, завершены без разворота новых потоков. Вы должны обнаружить, что вы можете изменить "30" на "100" или даже "100000": наш цикл не может запускать запросы быстрее, чем они завершаются. Обратите внимание, однако, что результаты сильно искажены в нашу пользу, потому что "I/O" снова и снова считывает одни и те же байты и будет обслуживаться из кеша операционной системы, а не путем чтения с диска. Это не означает демонстрацию реальной пропускной способности, конечно, только разницу в накладных расходах.
Чтобы повторить эти результаты с потоками рабочих потоков, а не потоками порт завершения, просто измените FileOptions.Asynchronous
на FileOptions.None
. Это делает синхронный доступ к файлам, а асинхронные операции будут выполняться в рабочих потоках, а не через порт завершения:
Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 8, Completion port threads: 0, Total threads: 15
Worker threads: 9, Completion port threads: 0, Total threads: 16
Worker threads: 10, Completion port threads: 0, Total threads: 17
Worker threads: 11, Completion port threads: 0, Total threads: 18
Worker threads: 12, Completion port threads: 0, Total threads: 19
Worker threads: 13, Completion port threads: 0, Total threads: 20
Worker threads: 14, Completion port threads: 0, Total threads: 21
Worker threads: 15, Completion port threads: 0, Total threads: 22
Worker threads: 16, Completion port threads: 0, Total threads: 23
Worker threads: 17, Completion port threads: 0, Total threads: 24
Worker threads: 18, Completion port threads: 0, Total threads: 25
Worker threads: 19, Completion port threads: 0, Total threads: 26
Worker threads: 20, Completion port threads: 0, Total threads: 27
Worker threads: 21, Completion port threads: 0, Total threads: 28
Worker threads: 22, Completion port threads: 0, Total threads: 29
Worker threads: 23, Completion port threads: 0, Total threads: 30
Worker threads: 24, Completion port threads: 0, Total threads: 31
Worker threads: 25, Completion port threads: 0, Total threads: 32
Worker threads: 26, Completion port threads: 0, Total threads: 33
Worker threads: 27, Completion port threads: 0, Total threads: 34
Worker threads: 28, Completion port threads: 0, Total threads: 35
Worker threads: 29, Completion port threads: 0, Total threads: 36
========================================
Worker threads: 30, Completion port threads: 0, Total threads: 37
Пул потоков объединяет один рабочий поток в секунду, а не два, которые он запускал для потоков портов завершения. Очевидно, что эти числа зависят от реализации и могут меняться в новых версиях.
Наконец, давайте продемонстрируем использование ThreadPool.SetMinThreads
, чтобы обеспечить минимальное количество потоков для завершения запросов. Если мы вернемся к FileOptions.Asynchronous
и добавим ThreadPool.SetMinThreads(50, 50)
в Main
нашей игрушечной программы, результат:
Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 0, Completion port threads: 31, Total threads: 35
========================================
Worker threads: 0, Completion port threads: 30, Total threads: 35
Теперь вместо того, чтобы терпеливо добавлять один поток каждые две секунды, пул потоков продолжает разворачивать потоки до тех пор, пока не будет достигнут максимум (чего не происходит в этом случае, поэтому окончательный счет остается на уровне 30). Конечно, все эти 30 потоков застряли в бесконечных ожиданиях - но если бы это была настоящая система, то эти 30 потоков теперь, вероятно, будут полезны, если не очень эффективная работа. Я бы не попробовал это с 100000 запросами.