Pull to refresh

Comments 12

UFO just landed and posted this here
Блин, забыл изменить тип публикации. Добавил пометку о переводе и ссылку на оригинал в текст (Хабр не позволяет изменить тип уже опубликованной публикации)

Думаю, главное, что ссылка на оригинал теперь есть. Спасибо за качественный перевод.

Реаллоцировать буфер, если понадобилось отправить больше данных, необязательно.
Но только если использовать WSASend, где этих буферов можно дать сразу несколько штук.


Паттерн "получили пакет; решили что надо ещё и сразу запустили второй recv" усложняется только если каждый раз дёргать сокет операциями с iocp. Но на самом деле никто не мешает сперва вызвать на неблокирующем сокете "голый" recv. Если там есть свежие данные (т.е. пока мы разбирались с протоколом, они уже нас ждали), то мы сразу их и получим. Если нет — вызываем WSARecv с overlapped и iocp. В случае шустрой работы сети это позволит в определённых ситуациях вообще не касаться пулера.

Сами технологии отличаются способами обработки событий. Эти отличия столь существенны, что вряд ли стоит пытаться писать их на какой-то общей базе (количество одинакового кода будет минимально).

Вот все варианты асинхронщины под одним интерфейсом
think-async.com/Asio/asio-1.10.6/doc/asio/overview/implementation.html
Под одним интерфейсом — это не то же самое что и на общей кодовой базе.
Если операция ввода/вывода была запланирована — её можно отменить вызовом функции CancelIo(). Что хуже, вызвать эту функцию может лишь тот же поток, который запустил первоначальную операцию. Все идеи организации отдельного управляющего потока разбиваются об это ограничение.
CancelIoEx() умеет то, что надо.
Кроме того, даже после вызова CancelIo() мы не можем быть уверены, что операция будет немедленно отменена (возможно, она уже выполняется, использует структуру OVERLAPPED и переданный буфер для чтения/записи).
Ровно так же как и для любых других операций: ждём уведомления.

Разница в типе нотификаций делает возможным (и достаточно тривиальным) эмуляцию IOCP с помощью epoll. Например, проект Wine именно так и делает. Однако, проделать обратное не так просто. Даже если у вас получится — это, вероятно, приведёт к потере производительности.
Тут дело скорее в том, что Overlapped IO само по себе и механизм уведомлений Completion Port более низкоуровневая абстракция, нежли механизмы epoll. Собственно OVL/IOCP суть отражение поведения ядра Windows на юзерспейс.
epoll говорит вам когда дескриптор готов к тому, чтобы с ним можно было что-то сделать — «а сейчас вы можете начать читать данные»
IOCP говорит вам когда запрошенная операция выполнена — «вы просили прочитать данные и вот они прочитаны»
А с записью ситуация зеркальная и не очень понятно, что лучше, что хуже. С epoll() механизм управления буферами и буферизации реализован в ядре и это хорошо, но если вы вдруг завтра придумаете более эффективный способ — то придётся править ядро. А для Windows вы и не сможете поправить ядро. Поэтому вот.

А ещё из iocp невозможно удалить дескриптор. Только если его закрыть.
Хотя… Возможно, но, извините, через задницу. И решение не на поверхности, ещё поискать надо...


/*
Hackers way to unbind from IOCP:

Call NtSetInformationFile with the FileReplaceCompletionInformationenumerator value for 
FileInformationClass and a pointer to a FILE_COMPLETION_INFORMATION structure for the FileInformation parameter. 
In this structure, set the Port member to NULL (or nullptr, in C++) to disassociate the file from the port it's currently attached to (I guess if it isn't attached to any port, nothing would happen), or set Port to a valid HANDLE to another completion port to associate the file with that one instead.
However it 1-st, require win >=8.1, and also invoke DDK stuff which is highly non-desirable. 
*/
Понимаю, что статья не о том (она о том, что «под капотом» и о том, что стоит все равно держать в голове), но не могу не удержаться и не порекомендовать Asio (Boost.Asio). И в epoll, и в IOCP столько подводных камней (тот же баг с IOCP на старых Windows с залипанием асинхронных операций), что не стоит повторять путь автора Asio и разгребать это все самому. К тому же, в Asio уже есть удобные таймеры (несколько оптимизированные по сравнению с наивной реализацией «в лоб») и концепция Strand (не без изъянов, конечно, но за все надо чем-то платить). Networking TS (published) для C++ построена как раз на базе Asio.

В Asio как раз есть возможность использования реактивного подхода (то, что дает epoll) на базе IOCP.

Субъективно (есть тесты, но неважные и все на Asio — так что «чистыми» их не назовешь) IOCP хоть и более тесно интегрирован с ядром ОС (где-то даже встречал обещание LIFO при планировании потоков, вызывающих GetQueuedCompletionStatus), но выигрывает у epoll при использовании в многопоточном приложении в варианте «один пул потоков обслуживает все соединения (сокеты)», так как в случае IOCP ОС (Windows) как-то эффективнее работает с блокировками, нежели чем простой mutex на единственном epoll instance.

В случае Asio мне пришлось изменить приложение так, чтобы можно было конфигурировать приложение работать в одном из двух вариантов (по умолчанию на Windows используется первый, а на non-Windows — второй):
  1. Один экземпляр boost::asio::io_service (т.е. один IOCP / epoll instance) и один пул из нескольких потоков по количеству logical CPU
  2. По одному экземпляру boost::asio::io_service на каждый logical CPU (т.е. один IOCP / epoll instance на каждый logical CPU) и по одному потоку на каждый экземпляр boost::asio::io_service

Понятно, что во втором варианте пришлось как-то раскидывать / распределять нагрузку (количество обслуживаемых соединений / сокетов) по всем экземплярам boost::asio::io_service, что уже не так удобно и гибко, как первый вариант.

IOCP интересен еще и тем, что его можно использовать с нулевым буфером сокета — тогда данные будут попадать в запрошенный (при инициировании операции чтения) буфер, минуя промежуточный слой в виде буфера сокета (опять же и здесь есть свои нюансы).

Ну и вот тут есть интересный комментарий по теме (по проблеме) необходимости преаллоцированных буферов в случае IOCP.
Есть еще одна алтернатива дла Windows. Она подходит для более интерактивного приложения чем сервис и гораздо гибче select.
Это возмозность связать сокет и Event. При етом на событие можно повесить несколько сокетов, а можно и один.

Выгладит ето так:
socketsEvent = ::WSACreateEvent();

res = ::WSAEventSelect(
                        socket, 
                        socketsEvent, 
                        FD_READ | FD_ACCEPT | FD_CLOSE);
 
waitResult = ::WaitForMultipleObjectsEx(... list of all events ...);

//Now check the socekt event

if(waitResult  == SocketEventNumber)
{
      //You have to check all sockets attached to the event
      WSANETWORKEVENTS lpEvents;
      ::memset(&lpEvents, 0, sizeof(lpEvents));
      const int wsaRes = ::WSAEnumNetworkEvents(socket, socketsEvent, &lpEvents);
}


Здесь удобно то что ожидание сокетов осуществляется совместно со всеми возможными объектами ожидания, включая события, потоки, порцессы и все остальное.
Можно интегрировать свои источники данных через события.
Существует так же версия ожиданиа совмещеанная с окнонными сообщениями.

И да, в отличие от select все это можно остановить.

А что с масштабируемостью?
Привяжу к ивенту 50 тыс. сокетов (да, сразу первая досада — маска событий одна на всех; уже не выйдет какие-то только на запись, а другие — только на чтение).
Ну, дождётся он события. И дальше что? Перебирать всю кучу и искать, где поменялось?
Дык для этого как раз poll есть.
А смысл epoll/kqueue/iocp именно в том, что неважно, сколько у тебя коннектов. Они вернут из ожидания только те, где случилось запрошенное событие.
И обрабатывая десяток прилетевших коннектов я даже и знать не буду, что их там "в ожидании" на самом деле десятки тысяч.

Sign up to leave a comment.