Новый Windows API SetFileCompletionNotificationModes()
с флагом FILE_SKIP_COMPLETION_PORT_ON_SUCCESS очень полезен для оптимизации цикла порта завершения ввода / вывода, потому что вы получите меньше завершений ввода / вывода для того же HANDLE.
Но это также нарушает весь цикл портов завершения ввода / вывода, потому что вам приходится много чего менять, поэтому я подумал, что было бы лучше открыть новый пост обо всех этих вещах, которые нужно изменить.
Прежде всего, когда вы устанавливаете флаг FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, это означает, что вы больше не будете получать завершения ввода / вывода для этой HANDLE / SOCKET до тех пор, пока все байты не будут прочитаны (или записаны), так что пока не будет больше ввода / вывода для делайте, как в Unix, когда вы получили EWOULDBLOCK.
Когда вы снова получите ERROR_IO_PENDING (так что новый запрос будет ожидаться), это все равно что получить EWOULDBLOCK в unix.
Сказал, что я столкнулся с некоторыми трудностями при адаптации этого поведения к моему циклу событий iocp, потому что обычный цикл событий iocp просто ждет вечно, пока не будет обработан какой-либо пакет OVERLAPPED, пакет OVERLAPPED будет обработан, вызывая правильный обратный вызов, который в свою очередь будет уменьшите атомный счетчик, и цикл снова начнет ждать, пока не придет следующий пакет.
Теперь, если я установлю FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, когда пакет OVERLAPPED возвращается для обработки, я обрабатываю его, выполняя некоторый ввод-вывод (с ReadFile()
или же WSARecv()
или что-то еще), и это может быть ожидание снова (если я получу ERROR_IO_PENDING) или это не может, если мой API ввода-вывода завершается немедленно. В первом случае мне нужно просто подождать следующего ожидающего ПЕРЕКРЫТИЯ, но во втором случае что мне делать?
Если я попытаюсь выполнить ввод / вывод до тех пор, пока не получу ERROR_IO_PENDING, он пойдет в бесконечном цикле, он никогда не вернет ERROR_IO_PENDING (до тех пор, пока коллега HANDLE / SOCKET не прекратит чтение / запись), поэтому другие OVERLAPPED будут ждать бесконечно долго. Так как я тестирую это с локальным именованным каналом, который пишет или читает навсегда, он идет бесконечным циклом.
Поэтому я подумал сделать ввод-вывод, пока не будет определенное количество байтов X, точно так же, как планировщик назначает временные интервалы, и если я получу ERROR_IO_PENDING до X, это нормально, OVERLAPPED снова будет поставлен в очередь в цикле событий iocp, но как насчет Я не получил ERROR_IO_PENDING?
Я попытался поместить свой OVERLAPPED, который еще не завершил свой ввод / вывод, в список / очередь для последующей обработки, позже вызывая API ввода / вывода (всегда с максимальным количеством байтов X), после обработки других ожидающих OVERLAPPED, и я установил GetQueuedCompletionStatus[Ex]()
тайм-аут до 0, поэтому в основном цикл будет обрабатывать перечисленные / поставленные в очередь OVERLAPPED, которые не завершили ввод-вывод, и в то же время немедленно проверять наличие новых OVERLAPPED, не переходя в спящий режим.
Когда список / очередь незавершенных OVERLAPPED станет пустым, я могу снова установить для GQCS [Ex] значение INFINITE. И так далее.
Теоретически это должно работать отлично, но я заметил странную вещь: GQCS [Ex] с таймаутом, установленным в 0, возвращает те же OVERLAPPED, которые еще не полностью обработаны (поэтому они находятся в списке / очереди, ожидающих последующей обработки) снова и опять.
Вопрос 1: так что, если я правильно понял, пакет OVERLAPPED будет удален из системы только после обработки всех данных?
Допустим, это нормально, потому что если я получаю одни и те же OVERLAPPED снова и снова, мне не нужно помещать их в список / очередь, но я обрабатываю их только как другие OVERLAPPED, и если я получаю ERROR_IO_PENDING, хорошо, иначе я обработаю их снова позже.
Но есть недостаток: когда я вызываю функцию обратного вызова для обработки пакетов OVERLAPPEDs, я уменьшаю атомарный счетчик ожидающих операций ввода-вывода. С установленным FILE_SKIP_COMPLETION_PORT_ON_SUCCESS я не знаю, был ли вызван обратный вызов для обработки реальной ожидающей операции, или просто OVERLAPPED, ожидающий больше синхронный I / O.
Вопрос 2: Как я могу получить эту информацию? Я должен установить больше флагов в структуре, которую я получаю из OVERLAPPED?
В основном я увеличиваю атомный счетчик для ожидающих операций до призвание ReadFile()
или же WSARecv()
или что-то еще, и когда я вижу, что он вернул что-то отличное от ERROR_IO_PENDING или успеха, я уменьшаю его снова.
С установленным FILE_SKIP_COMPLETION_PORT_ON_SUCCESS я должен снова уменьшить его также, когда API ввода-вывода завершится успешно, потому что это означает, что я не получу завершение.
Увеличение и уменьшение атомарного счетчика — пустая трата времени, когда ваш API ввода-вывода, скорее всего, выполнит немедленное и синхронное завершение. Разве я не могу просто увеличить атомарный счетчик ожидающих операций только при получении ERROR_IO_PENDING? Я не делал этого раньше, потому что думал, что если запланирован другой поток, завершающий мой ожидающий ввод-вывод до вызывающий поток может проверить, является ли ошибка ERROR_IO_PENDING, и, таким образом, увеличить атомный счетчик ожидающих операций, я испорчу атомный счетчик.
Вопрос 3: это реальная проблема? Или я могу просто пропустить это и увеличить счетчик атомов только когда я получу ERROR_IO_PENDING? Это очень сильно упростит ситуацию.
Только флаг, и много дизайна, чтобы переосмыслить.
о чем ты думаешь?
Как отмечает в комментариях Реми: ваше понимание того, что FILE_SKIP_COMPLETION_PORT_ON_SUCCESS
делает это неправильно. Все, что он делает, это позволяет вам обрабатывать завершенную операцию «в очереди», если вы сделали звонок (скажем, WSARecv()
возвращает 0
Таким образом, предполагая, что у вас есть функция handleCompletion (), которую вы вызываете после извлечения завершения из IOCP с помощью GQCS (), вы можете просто вызвать эту функцию сразу после успешного завершения. WSARecv()
,
Если вы используете счетчик для каждой операции, чтобы отследить, когда последняя операция завершается в соединении (и я делаю это для управления временем жизни данных для каждого соединения, которые я связываю как ключ завершения), то вы все равно делаете это точно так же и ничего не меняется …
Вы не можете увеличивать ТОЛЬКО на ERROR_IO_PENDING
потому что тогда у вас есть условие гонки между завершением операции и происходящим приращением. Вы ВСЕГДА должны увеличивать значение перед API, что может привести к уменьшению (в обработчике), потому что в противном случае планирование потоков может вас испортить. Я действительно не вижу, как пропуск инкремента вообще «упростит» вещи …
Больше ничего не меняется. Кроме…
Конечно, теперь вы можете иметь рекурсивные вызовы в вашем обработчике завершения (в зависимости от вашего дизайна), и это было то, что раньше было невозможно. Например; Теперь вы можете иметь WSARecv()
вызов завершен с кодом возврата 0 (поскольку данные доступны), и ваш код обработки завершения может выдать другой WSARecv()
который также может завершиться с кодом возврата 0, и тогда ваш код обработки завершения будет вызван снова, возможно, рекурсивно (в зависимости от дизайна вашего кода).
Отдельные занятые соединения могут теперь препятствовать тому, чтобы другие соединения получали любое время обработки. Если у вас есть 3 одновременных соединения, и все одноранговые узлы отправляют данные так быстро, как могут, и это быстрее, чем ваш сервер может обработать данные, и у вас есть, например, 2 потока ввода-вывода, вызывающих GQCS()
затем с FILE_SKIP_COMPLETION_PORT_ON_SUCCESS
Вы можете обнаружить, что два из этих соединений монополизируют потоки ввода / вывода (все WSARecv()
вызовы возвращают успех, что приводит к встроенной обработке всех входящих данных). В этой ситуации у меня, как правило, есть счетчик «макс. Последовательных операций ввода-вывода на соединение», и как только этот счетчик достигает настраиваемого предела, я отправляю следующее встроенное завершение в IOCP и позволяю ему получить вызов GQCS () как это дает шанс другим соединениям.
Других решений пока нет …