Поток за соединение против шаблона реактора (с пулом потоков)?
Я хочу написать простую многопользовательскую игру как часть моего учебного проекта на С++.
Поэтому я подумал, что, поскольку я нахожусь в этом, я хотел бы сделать это правильно, а не просто делать это.
Если я правильно понял: Apache использует архитектуру Thread-per-connection, а nginx использует цикл событий, а затем выделяет рабочего [x] для входящего подключение. Думаю, nginx мудрее, так как он поддерживает более высокий уровень concurrency. Правильно?
Я также столкнулся с этой умной аналогией, но я не уверен, можно ли ее применить к моей ситуации. Аналогия также кажется очень идеалистической. Я редко видел, что мой компьютер работает на 100% -ном процессоре (даже с открытыми вкладками Chrome, Photoshop и тем, что не работает одновременно)
Кроме того, я столкнулся с SO-сообщением (как-то он исчез из моей истории), когда пользователь спросил, сколько потоков они должны использовать, и один из ответов заключался в том, что вполне приемлемо иметь около 700, даже до 10 000 потоки. Однако этот вопрос был связан с JVM.
Итак, давайте оценим вымышленную пользовательскую базу около 5000 пользователей. Какой подход должен быть "наиболее параллельным"?
- Образец реактора работает все в одном потоке.
- Схема реактора с пулом потоков (примерно, насколько велика вы предполагаете, что пул потоков должен быть?
- Создание потока для каждого соединения, а затем уничтожение потока, которое соединение закрывается.
Я признаю, что вариант 2 звучит как лучшее решение для меня, но я очень зеленый во всем этом, поэтому я мог бы быть немного наивным и пропустить какой-то очевидный недостаток. Кроме того, похоже, что это довольно сложно реализовать.
PS: Я рассматриваю возможность использования POCO С++ Libraries. Предлагая любые альтернативные библиотеки (например, boost), я в порядке. Однако многие говорят, что библиотека POCO очень чиста и понятна. Поэтому я бы предпочел использовать этот, поэтому я могу узнать о том, что я использую.
Ответы
Ответ 1
Реактивные приложения, конечно, лучше масштабируются, когда они написаны правильно. Это означает
- Никогда не блокировать реактивную нить:
- Любая блокировка серьезно ухудшит производительность вашего сервера, вы обычно используете небольшое количество реактивных потоков, поэтому блокирование также может быстро вызвать тупик.
- Нет мьютексов, поскольку они могут блокироваться, поэтому нет общего измененного состояния. Если вам требуется совместное использование, вам придется обернуть его актером или похожим, так что только один поток имеет доступ к состоянию.
- Все работы в реактивных потоках должны быть связаны cpu
- Все IO должны быть асинхронными или выполняться в другом пуле потоков, а результаты возвращаются в реактор.
- Это означает использование фьючерсов или обратных вызовов для обработки ответов, этот стиль кода может быстро стать недостижимым, если вы не привыкли к нему и не дисциплинированы.
- Все работы в реактивных потоках должны быть небольшими
- Чтобы поддерживать отзывчивость сервера, все задачи в реакторе должны быть небольшими (ограниченными временем)
- На 8-ядерном компьютере вы не можете не допустить, чтобы 8 длинных задач приходилось в то же самое время, потому что никакая другая работа не начнется, пока они не будут завершены.
- Если задачи могут занять много времени, их необходимо разбить (совместная многозадачность)
Задачи в реактивных приложениях назначаются приложением не операционной системой, поэтому они могут быть быстрее и использовать меньше памяти. Когда вы пишете Reactive приложение, вы говорите, что знаете, что проблемный домен настолько хорошо, что вы можете организовать и запланировать этот тип работы лучше, чем операционная система может планировать потоки, выполняющие ту же самую работу блокирующим образом.
Я большой поклонник реактивной архитектуры, но они приходят с издержками. Я не уверен, что я напишу свое первое приложение на С++ как реактивное, я обычно стараюсь учиться одному за раз.
Если вы решите использовать реактивную архитектуру, используйте хорошую структуру, которая поможет вам разработать и структурировать ваш код, иначе вы получите спагетти. Вещи, которые нужно искать:
- Какая единица работы?
- Как легко добавить новую работу? может ли он поступать только от внешнего события (например, сетевого запроса).
- Как легко разбить работу на более мелкие куски?
- Насколько легко обрабатывать результаты этой работы?
- Насколько легко перемещать блокирующий код в другой пул потоков и обрабатывать результаты?
Я не могу рекомендовать библиотеку С++ для этого, теперь я делаю свою разработку сервера в Scala и Akka, которые обеспечивают все это превосходной сборной библиотекой фьючерсов, чтобы код был чистым.
Желаем удачи, познакомившись с С++ и с которым когда-либо вы делаете выбор.
Ответ 2
Вариант 2 наиболее эффективно займет ваше оборудование. Вот классическая статья, десять лет, но все же хорошая.
http://www.kegel.com/c10k.html
Лучшая комбинация библиотек в эти дни для структурирования приложения с concurrency и асинхронным ожиданием - Boost Thread plus Boost ASIO. Вы также можете попробовать библиотеку С++ 11 std thread
и std mutex
(но Boost ASIO во многих случаях лучше, чем мьютексы, просто всегда обратный вызов в тот же поток, и вам не нужны защищенные области). Держитесь подальше от std future
, вызывают его нарушение:
http://bartoszmilewski.com/2009/03/03/broken-promises-c0x-futures/
Оптимальное количество потоков в пуле потоков - это один поток на ядро процессора. 8 ядер → 8 потоков. Плюс, возможно, несколько дополнительных, если вы считаете, что ваши потоковые потоки иногда могут вызывать блокирующие операции.
Ответ 3
FWIW, Poco поддерживает вариант 2 (ParallelReactor) с версии 1.5.1
Ответ 4
Я думаю, что вариант 2 - лучший. Что касается настройки размера бассейна, я думаю, что пул должен быть адаптивным. Он должен иметь возможность создавать больше потоков (с некоторым высоким жестким пределом) и удалять избыточные потоки во время низкой активности.
Ответ 5
как показывают аналогия, с которой вы связаны (и комментарии). это зависит от приложения. теперь вы строите здесь игровой сервер. пусть проанализирует это.
игровые серверы (как правило) выполняют много операций ввода-вывода и относительно немного вычислений, поэтому они далеки от 100% -ных приложений.
с другой стороны, они также обычно изменяют значения в некоторой базе данных (модель "игрового мира" ). все игроки создают записи и записи в эту базу данных. которая в точности является проблемой пересечения в аналогии.
так что, хотя вы можете получить часть от обработки ввода-вывода в отдельных потоках, вы также проиграете из-за того, что отдельные потоки будут обращаться к одной и той же базе данных и ждут своих блокировок.
поэтому вариант 1 или 2 допустим в вашей ситуации. по причинам масштабируемости я бы не рекомендовал вариант 3.