Pull to refresh
0
0
Marat Abrarov @mabrarov

Пользователь

Send message
Думаю, автор и math_coder имели в виду что-то вроде godbolt.org/z/vWy5eS. Вы правы в том, что копия иногда будет создаваться (зависит от caller — если он будет передавать rvalue reference, то копирование будет заменено на еще одно перемещение), но автор имел в виде не отсутствие копирования, а «семантику», когда само «значение» явно «захватывается» (перемещается когда это возможно сделать безопасно и копируется в остальных случаях) callee и дальше уже callee сам контролирует life time этого значения.

«Передача владения» здесь и вправду звучит несколько неверно, потому что в C++ (и не только) под этим словами обычно понимают иное.
Судя по epoll_ctl manual page EPOLLEXCLUSIVE ввели, начиная с версии ядра 4.5. Не все могут позволить себе такое. Например, весь RHEL 7 — это Kernel Version 3.10.x (хотя можно и обмануть).

Похоже, нужно почитать про epoll — даже что-то вводное типа Вся правда о linux epoll уже говорит о том, что, возможно, с многопоточностью (pre-fork) все работает, если готовить определенным образом. Я не уверен, что такой рецепт подходит для всех — вроде как есть причины, по которым та же Asio все еще не использует edge triggered режим.
У IOCP есть преимущество следующего вида — IOCP «из коробки» поддерживает многопоточность, когда все потоки, читающие из completion port считаются Windows, как единый пул потоков с LIFO планированием. Т.е. создал completion port, навесил на него все сокеты принимаемых входящих соединений (случай сервера), создал пул потоков, где каждый поток читает по одному событию из completion port и получил почти равномерную (LIFO все таки) загрузку всех потоков из пула (при полной загрузке сервера, при частичной загрузке — за счет LIFO часть потоков может и простаивать, если задач — completion packets — им попросту «не достается»).

При попытке выстроить подобную архитектуру с единственным epoll instance придется использовать mutex. Это убивает всякое планирование потоков (и оно вовсе не LIFO, которое Windows IOCP как бы обещает где-то в документации) и серьезно просаживает производительность. Поэтому с epoll поступают иначе — создают несколько epoll instances (например, по кол-ву ядер CPU) и с каждым epoll instance работает только один поток без каких-либо mutex.

Вот только наличие нескольких epoll instance уже требует распределения (а распределение может требовать и учета — зависит от алгоритма) сокетов по этим нескольким epoll instance. Неудачное распределение может привести к перекосам в нагрузке потоков (Why does one NGINX worker take all the load?) — когда одни потоки перегружены, а другие простаивают (в то время как в других epoll instance есть необработанные события).

Насчет эмуляции реактора в IOCP (для тех, кто не хочет выделать буфер заранее) — тот же Boost.Asio реализвал это поверх IOCP через null_buffers (Reactor-Style Operations).

Понимаю, что статья не о том (она о том, что «под капотом» и о том, что стоит все равно держать в голове), но не могу не удержаться и не порекомендовать 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.

Information

Rating
Does not participate
Location
Казань, Татарстан, Россия
Date of birth
Registered
Activity