Порты завершения ввода-вывода и API пула потоков

У меня возникла проблема, описанная здесь, и мне предложили использовать порты завершения ввода-вывода или пул потоков.

Я реализовал завершение ввода-вывода, вызывая PostQueuedCompletionStatus для постановки задачи в очередь и GetQueuedCompletionStatus для получения следующей задачи для ее выполнения. Я использую порт завершения ввода-вывода в качестве потокобезопасного контейнера FIFO с несколькими производителями/многопотребителями без явных блокировок. Это дает мне полный контроль над потоками, потому что мне может понадобиться завершить процесс на долгое время и сообщить о них. Также GetQueuedCompletionStatus ожидает вызывающий поток, если не осталось задачи.

Помимо терминации, пул потоков мне подходит: мои задачи выполняются менее чем за миллисекунду, а их много. Также проще вызвать QueueUserWorkItem и позволить ОС выполнять синхронизацию и выполнение.

Есть ли различия между двумя подходами с точки зрения производительности? Есть комментарии по поводу моей реализации?


person ali_bahoo    schedule 22.12.2010    source источник
comment
гм. Порты завершения ввода-вывода ЯВЛЯЮТСЯ механизмом объединения потоков. и, если вы используете другой API пула потоков, вероятно, являются базовым механизмом, используемым API.   -  person Chris Becke    schedule 22.12.2010
comment
@Chris Becke: Под пулом потоков я имею в виду вызов QueueUserWorkItem для постановки задач в очередь.   -  person ali_bahoo    schedule 22.12.2010
comment
Я прочитал вопрос, на который вы ссылались, и рассмотрели ли вы среду выполнения с параллелизмом Visual Studio 2010?   -  person Puppy    schedule 22.12.2010
comment
@DeadMG: не среда выполнения Visual Studio 2010 Concurrency, а API пула потоков. Но если особой разницы нет, я не буду менять свою реализацию. Это уже слишком сложно. И есть проблема с завершением потока. Завершение не рекомендуется из-за утечек стека в любом случае, но в пуле потоков могут быть еще большие проблемы. С портом завершения io вы свободны в потоках, потому что вы управляете ими.   -  person ali_bahoo    schedule 22.12.2010
comment
Вы ничего не делаете с вводом-выводом, так зачем вообще использовать порт завершения ввода-вывода? Если вам нужен полный контроль над потоками, создайте собственный пул потоков (используйте WaitForMultipleObjects и т. д., чтобы ждать и сигнализировать о задаче).   -  person Zach Saw    schedule 22.12.2010
comment
@ZachSaw Целью создания порта завершения является то, что он является механизмом, используемым для реализации пула потоков. Пулы потоков Windows вызывают GetQueuedCompletionStatus и блокируют вызов, который возвращается, когда нужно выполнить работу. Windows назначит работу одному из потоков, простаивающих в GetQueuedCompletionStatus, чтобы передать им работу.   -  person Ian Boyd    schedule 27.10.2013


Ответы (4)


Порты завершения предназначены для того, чтобы избежать ненужного переключения контекста. Когда ваш поток, который вызывает GetQueuedCompletionStatus, завершает обработку рабочего элемента, он может обратно вызвать GetQueuedCompletionStatus, чтобы продолжить выполнение дополнительной работы в рамках текущего кванта времени ЦП.

@Jonathan - Если у вас есть блокирующие вызовы, то они практически никогда не должны выполняться в потоке, извлекающем рабочие элементы. Их следует либо выполнять асинхронно (с вызовом Begin/End или *Async), либо блокировать в другом потоке (пул рабочих потоков). Это гарантирует, что все ваши потоки, обслуживающие порт завершения, действительно выполняют работу, а не тратят время на блокировку, когда доступны другие рабочие элементы.

Небольшое уточнение: если вы управляете своими собственными потоками и вызываете GetQueuedCompletionStatus, то вы создали свой собственный порт завершения отдельно от порта завершения ввода-вывода и связанного с ним пула потоков, используемого ОС для асинхронных вызовов ввода-вывода.

person Karg    schedule 16.02.2011
comment
Да, в цикле while поток вызывает GetQueuedCompletionStatus(), получает задание, выполняет его. Это очень эффективно. Порт завершения ввода-вывода почти работает с 5 потоками из 10 для миллионов задач с загрузкой ЦП ~ 0%. Самый полезный API, который я когда-либо использовал. - person ali_bahoo; 16.02.2011

Порт завершения ввода-вывода (IOCP) обычно используется с пулом потоков для обработки событий/действий ввода-вывода, в то время как пул потоков WinAPI (который вы указываете через QueueUserWorkItem) представляет собой просто реализацию Майкрософт типичного пула потоков, который будет обрабатывать задачи, не связанные с вводом-выводом.

Глядя на ваш связанный поток, похоже, что вы просто удаляете задачу из списка FIFO, который не имеет ничего общего с вводом-выводом. Итак, последнее, скорее всего, то, что вам нужно. Я не думаю, что вас должна беспокоить разница в производительности, в отличие от того, какой API подходит для того, что вы делаете.

РЕДАКТИРОВАТЬ: Если вам нужен полный контроль над созданием и завершением потока (хотя никогда не бывает нормально завершать поток, поскольку стек не раскручивается), вам лучше создать свой собственный пул потоков с помощью WaitForSingleObject (или, скорее, MultipleObjects для сигнала выхода также) и SetEvent. Пул потоков WinAPI - это, по сути, автоматическое создание и завершение потоков Microsoft в зависимости от загрузки потоков.

person Zach Saw    schedule 22.12.2010
comment
Прежде всего, я предпочитаю полагаться на хорошо протестированный, задокументированный, спроектированный (я надеюсь на это :)) специфичный для ОС API. Вторая реализация пользовательского пула потоков очень сложная. Кроме того, со всеми событиями, семафорами или даже критическими разделами явная блокировка замедлит работу вашей реализации. Меньше блокировок, переключателей потоков приведет к большей эффективности ИМХО. Все эти API управляют этими вещами. - person ali_bahoo; 22.12.2010
comment
@sad_man: Неправда. Пул потоков чрезвычайно прост. Создайте 10 потоков, подождите WaitForSingleObject и выполните задачу Signal с помощью SetEVent. Это поток. Пул потоков WinAPI требует, чтобы вы выполняли ту же синхронизацию и блокировку, если это требуется вашим дизайном. Ни на секунду не думайте, что ОС справится с этим за вас. - person Zach Saw; 22.12.2010
comment
@sad_man: Как я уже сказал в своем ответе, если вам нужен полный контроль, единственный способ - создать свой собственный пул потоков. Если вы хотите полагаться на пул потоков WinAPI, вам придется смириться с его дизайном и реализацией. - person Zach Saw; 22.12.2010
comment
WaitForSingleObject и все, что я упомянул, были WinAPI - они настолько же надежны, насколько и WinAPI. - person Zach Saw; 23.12.2010

Если вы используете порты завершения ввода-вывода и создаете свои собственные потоки X, которые вызывают GetQueuedCompletionStatus(), и у вас есть задачи X, которые занимают много времени (например, чтение из сети), тогда все потоки будут заняты, и дальнейшие запросы будут голодать. AFAIU, в этом случае пул потоков будет вращать другой поток.

Кроме того, никогда не используйте TerminateThread()! При выделении памяти из кучи поток временно получает CRITICAL_SECTION этой кучи. Поэтому, если поток завершается в середине этого, другие потоки, пытающиеся выделить из той же кучи, будут зависать. И у вас нет возможности узнать, выделяется ли поток или нет.

person Jonathan    schedule 23.12.2010
comment
На самом деле мои задачи не занимают много времени, но в моем приложении пользователи могут писать свои проприетарные коды, и меня беспокоит, что, если код пользователя войдет в бесконечный цикл или цикл, для выхода из которого требуется очень много времени. Я не могу рисковать другими рабочими кодами. Так что как-то мне нужно заставить эту нить закончиться. Можете ли вы дать какие-либо материалы о потоках, занимающих CRITICAL_SECTION кучи? - person ali_bahoo; 23.12.2010

Разница между QueueUserWorkItem и IOCompletionPorts заключается в том, что QueueUserWorkItem — это простая в использовании абстракция более высокого уровня. Легко увидеть, как QueueUserWorkItem может быть реализован поверх IOCompletionPorts (я не знаю, что это такое).

Практическая разница в том, что QueueUserWorkItem создает (и управляет) потоками в пуле потоков. Таким образом, он не знает заранее, сколько потоков ему понадобится: он начинает с нескольких (возможно, только одного), а затем создает дополнительные потоки пула через определенные промежутки времени, если в пуле потоков нет свободных потоков для обработки элементов очереди.

Это означает, что использование QueueUserWorkItem может привести к значительной задержке обработки элементов, если добавляется много элементов, а пул мал, следовательно, всплески добавляемых элементов могут привести к тому, что пул станет больше, чем необходимо.

person Chris Becke    schedule 22.12.2010