Темы IOCP - Разъяснение?

Прочитав эту статью, в которой говорится:

После завершения работы устройства (операция ввода-вывода) - он уведомляет CPU через прерывание.

.........

Однако это только статус завершения существует на уровне ОС; процесс имеет собственное пространство памяти, которое должно получать уведомления

.........

Поскольку библиотека /BCL использует стандартный P/Invoke с перекрытой системой ввода/вывода, он уже зарегистрировал Порт завершения ввода-вывода (IOCP), который является частью пула потоков.

.........

Таким образом, поток пула потоков ввода-вывода заимствован кратко для выполнения APC, который уведомляет задачу о ее завершении.

Мне была интересна смелая часть:

Если я правильно понял, после завершения операции ввода-вывода, он должен уведомить фактический процесс, который выполнил операцию ввода-вывода.

Вопрос №1:

Означает ли это, что он захватывает новый поток пула потоков для каждой завершенной операции ввода-вывода? Или это выделенное количество потоков для этого?

Вопрос № 2:

Глядя на:

for (int i=0;i<1000;i++)
    {
      PingAsync_NOT_AWAITED(i); //notice not awaited !
    }

Означает ли это, что у меня будет 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 запросами.

Ответ 3

Означает ли это, что у меня будет 1000 потоков ниток IOCP одновременно (вроде) работает здесь, когда все закончено?

Нет, совсем нет. То же, что и рабочие потоки, доступные в ThreadPool, мы также имеем "потоки портов завершения".

Эти потоки предназначены для ввода/вывода Async. Не будут созданы потоки. Они создаются по запросу в качестве рабочих потоков. Они будут уничтожены в конце концов, когда решат threadpool.

Позаимствованный кратко автор означает, что для уведомления о завершении ввода-вывода для процесса используется какой-то произвольный поток из "Потоков портов завершения" (ThreadPool). Он не будет выполнять какую-либо длительную операцию, но завершение уведомления IO.

Ответ 4

Как мы уже говорили, IOCP и рабочие потоки имеют отдельный ресурс внутри threadpool.

Не считая, если вы await выполняете операцию ввода-вывода или нет, произойдет регистрация на IOCP или с перекрытием IO. await - это механизм более высокого уровня, который не имеет ничего общего с регистрацией этих IOCP.

Простым тестом вы можете видеть, что хотя нет await, IOCP все еще используется приложением:

private static void Main(string[] args)
{
    Task.Run(() =>
    {
        int count = 0;
        while (count < 30)
        {
            int _;
            int iocpThreads;
            ThreadPool.GetAvailableThreads(out _, out iocpThreads);
            Console.WriteLine("Current number of IOCP threads availiable: {0}", iocpThreads);
            count++;
            Thread.Sleep(10);
        }
    });

    for (int i = 0; i < 30; i++)
    {
        GetUrl(@"http://www.ynet.co.il");
    }

    Console.ReadKey();
}

private static async Task<string> GetUrl(string url)
{
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync(url);
    return await response.Content.ReadAsStringAsync();
}

В зависимости от количества времени, которое требуется для выполнения каждого запроса, вы увидите, что IOCP сужается, когда вы делаете запросы. Более параллельные запросы, которые вы попытаетесь сделать меньше потоков, будут доступны вам.