Порты завершения ввода-вывода по сравнению с RegisterWaitForSingleObject?

В чем разница между использованием портов завершения ввода-вывода и просто использованием RegisterWaitForSingleObject заставить поток пула ожидать завершения ввода-вывода?

Является ли один из них быстрее, и если да, то почему?


person user541686    schedule 26.04.2013    source источник
comment
Быстрее — это термин, относящийся к поставленной задаче. Что вы планируете делать (или уже делаете)? Эти две технологии заметно различаются, поэтому знание того, чего вы пытаетесь достичь, будет иметь важное значение в том, что лучше подходит для вашей проблемы.   -  person WhozCraig    schedule 26.04.2013
comment
@WhozCraig: я выполняю большое количество операций чтения с диска и обработки данных по мере их поступления; узким местом является ввод-вывод, поэтому цель состоит в том, чтобы достичь максимальной пропускной способности (данные с разных дисков будут считываться параллельно, а данные с одного и того же диска будут считываться последовательно). Я чувствую, что они оба подойдут для моего дизайна, я просто не знаю, какой из них использовать.   -  person user541686    schedule 26.04.2013


Ответы (1)


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

Простым примером этого является сервер, который отвечает за обслуживание файлов с диска. IOCP обычно состоит из трех основных элементов:

  1. Пул из N потоков для обслуживания запросов IOCP.
  2. Ограничение в M потоков (M всегда ‹ N) сообщает IOCP, сколько одновременных неблокированных потоков можно разрешить.
  3. Цикл состояния завершения, в котором выполняются все потоки.

Разница между N и M здесь очень важна. Общая философия заключается в том, чтобы настроить M как количество ядер на машине, а N — как большее число. Насколько больше, зависит от количества времени, которое ваши рабочие потоки проводят в заблокированном состоянии. Если вы читаете файлы с диска, ваши потоки будут привязаны к скорости канала дискового ввода-вывода. Когда вы делаете этот вызов ReadFile(), вы только что ввели блокирующий вызов. Если M == N, то, как только вы нажмете на все потоки, читающие файлы с диска, вы полностью остановитесь, и все потоки будут на канале дискового ввода-вывода.

Но что, если бы у какого-нибудь хитроумного планировщика был способ «узнать», что этот поток (а) участвует в пуле потоков IOCP и (б) просто застопорился, потому что выдал вызов API, который займет много времени? Что, если, когда это произойдет, этот причудливый планировщик сможет временно «переместить» этот поток в специальную группу «работающих, но остановленных», а затем «освободить» лишний поток, который вызвался работать, пока есть остановленные потоки?

Это именно то, что предлагает IOCP. Когда N больше, чем M, IOCP переведет поток, который только что вызвал остановку, в специальное состояние «выполняется, но остановлен», а затем временно «заимствует» дополнительный поток из вашего пула N. Это будет продолжаться до тех пор, пока пул N не будет исчерпан или потоки, которые были остановлены, не начнут возвращаться из своих блокирующих запросов.

Таким образом, в этом свете IOCP, сконфигурированный так, чтобы, скажем, 8 потоков одновременно выполнялись на 8-ядерной машине, на самом деле может иметь несколько сотен потоков в реальном пуле. Только 8 из них когда-либо будут "разрешены" для одновременной работы в неблокированном состоянии, хотя вы можете временно отключить это, когда заблокированные потоки возвращаются из своих блоков, а у вас уже есть заимствованные потоки, обслуживающие дополнительные запросы.

Наконец, хотя это и не так важно для вашего дела, это все же важно: поток IOCP НЕ будет блокироваться и не будет переключаться контекст, если в очереди есть ожидающая работа, когда он завершает свою текущую работу и выдает следующий вызов GetQueueCompletionStatus(). Если есть ожидающая работа, он подхватит ее и продолжит выполнение без обязательного вытеснения. Конечно, планировщик ОС в любом случае может выполнять вытеснение, но только как часть общего планировщика; не из-за конкретного вызова GetQueueCompletionStatus(). Единственным исключением из этого является то, что уже запущено более M потоков и они не заблокированы. В этом случае GetQueueCompletionStatus() будет блокировать вызывающий поток до тех пор, пока он снова не понадобится для бездействия, когда достаточное количество потоков снова будет заблокировано.

Описание, которое вы дали, указывает на то, что вы будете сильно привязаны к диску. Для абсолютно критичных к производительности архитектур серверов ввода-вывода практически невозможно превзойти преимущества IOCP, особенно обнаружение блоков на уровне ОС, которое позволяет планировщику знать, что он может временно освободить дополнительные потоки от ваш мастер-пул, чтобы поддерживать работу, пока другие потоки останавливаются.

Вы просто не можете воспроизвести эту специфическую функцию IOCP, используя пулы потоков Windows. Если бы все ваши потоки были обработчиками чисел с небольшим количеством операций ввода-вывода или вообще без них, я бы сказал, что пулы потоков подходят лучше, но ваша специфика дискового ввода-вывода подсказывает мне, что вместо этого вам следует использовать IOCP.

person WhozCraig    schedule 26.04.2013
comment
Одним из дополнений к этому является то, что IOCP используют несправедливое планирование (спящие потоки, как правило, остаются спящими, работающие потоки, как правило, остаются в памяти), что означает, что вы можете сделать N настолько большим, насколько вам нравится, не беспокоясь о перегрузке. планировщик и менеджер памяти. - person HerrJoebob; 27.04.2013
comment
Я только что понял, что в документации QueueUserWorkItem для WT_EXECUTEDEFAULT говорится, что функция обратного вызова ставится в очередь потока, который использует порты завершения ввода-вывода, тогда как документация RegisterWaitForSingleObject для того же флага не включает это предложение. Вы знаете, использует ли RegisterWaitForSingleObject IOCP для WT_EXECUTEDEFAULT или нет? Если да, то будет ли мне так же эффективно использовать RegisterWaitForSingleObject? (Согласно WINE, они оба используют RtlQueueWorkItem, так что вполне вероятно, что они будут одинаковыми...) - person user541686; 27.04.2013
comment
@Mehrdad Я в это не верю, но BindIoCompletionCallback() делает (название несколько выдает). Существует важное предупреждение о том, что вы должны проверить результат своего запроса ввода-вывода перед настройкой привязки. Он должен вернуть ERROR_IO_PENDING, иначе вы будете отправлять запрос обратного вызова, который никогда не придет. - person WhozCraig; 27.04.2013
comment
@WhozCraig: О, спасибо за предупреждение. И я только что быстро разобрал RtlRegisterWait и обнаружил, что он использует NtQueueApcThread в потоке пула потоков вместо IOCP, по крайней мере, в Windows XP. :) Спасибо! - person user541686; 27.04.2013
comment
@WhozCraig: Интересно, я только что обнаружил, что APC, похоже, не выполняют переключение контекста, когда в этом нет необходимости: pastebin.com/NdXip9kb Это, конечно, не надежно, но достаточно убедительно. - person user541686; 27.04.2013
comment
... о да, но тот факт, что они используют APC, означает, что поток определяется во время постановки в очередь, а не во время удаления из очереди. Думаю, тогда IOCP побеждает их безоговорочно! - person user541686; 27.04.2013
comment
@Mehrdad Я совсем не удивлен. Проектирование системы на основе IOCP немного отличается от традиционных изменяемых конфигураций, но похоже, что вы определенно способны с этим справиться. Я использую их для многих вещей, включая, казалось бы, простые вещи, такие как обычные однопроцессные рабочие очереди. Они невероятно гибкие, и когда я ехал домой в своем ответе, это низкоуровневое обнаружение блоков — просто кошачья пижама. Я желаю вам хорошо провести время в программировании и предлагаю сначала немного поиграть с тестовыми моделями. Вы многому научитесь с практическим IOCP. - person WhozCraig; 27.04.2013
comment
@WhozCraig: Ха-ха, да, они кажутся потрясающими только для обычных очередей производителей / потребителей! и спасибо! :) - person user541686; 27.04.2013
comment
@WhozCraig, конечно, в приведенном выше примере вы бы просто использовали чтение с перекрытием для чтения файлов, и вся ваша блокировка исчезла бы ?? - person Len Holgate; 28.04.2013
comment
@LenHolgate Абсолютно, Лен, особенно если это были файлы размером больше системной страницы. В этом случае я, вероятно, поступил бы именно так, как вы описали, и спасибо, что указали на это. Очень одобряю. - person WhozCraig; 24.07.2013