Многие потоки или как можно меньше потоков?

В качестве побочного проекта я в настоящее время пишу сервер для вековой игры, которую я играл. Я пытаюсь сделать сервер как можно более гибким, но мне интересно, что будет хорошим дизайнерским решением для многопоточности. В настоящее время у меня есть следующая последовательность действий:

  • Запуск (создает) →
  • Сервер (прослушивает для клиентов, создает) →
  • Клиент (прослушивает команды и отправляет данные периода)

Я предполагаю, что в среднем 100 клиентов, так как это был максимум в любой момент времени для игры. Каким было бы правильное решение, как для нарезки всего этого? Моя текущая настройка выглядит следующим образом:

  • 1 поток на сервере, который прослушивает новые подключения, в новом соединении создает клиентский объект и начинает прослушивание снова.
  • Клиентский объект имеет один поток, прослушивает входящие команды и отправляет периодические данные. Это делается с использованием неблокирующего сокета, поэтому он просто проверяет, доступны ли данные, об этом и затем отправляет сообщения, которые он поставил в очередь. Вход выполняется до начала цикла отправки-получения.
  • Один поток (на данный момент) для самой игры, поскольку я считаю, что это отдельно от всей клиент-серверной части, архитектурно говоря.

Это приведет к тому, что в общей сложности будет 102 потока. Я даже рассматриваю возможность предоставления клиенту 2 потока, один для отправки и один для получения. Если я это сделаю, я могу использовать блокирующий ввод-вывод в потоке получателя, что означает, что поток будет в основном бездействовать в средней ситуации.

Моя главная проблема заключается в том, что, используя эти много нитей, я буду использовать ресурсы. Я не беспокоюсь о гоночных условиях или тупиках, так как мне все равно придется иметь дело.

Мой дизайн настроен таким образом, что я мог использовать один поток для всех клиентских коммуникаций, независимо от того, 1 или 100. Я отделил логику связи от самого объекта клиента, поэтому я мог бы реализовать его, не переписывая много кода.

Основной вопрос: неправильно ли использовать более 200 потоков в приложении? Имеет ли он преимущества? Я собираюсь запустить это на многоядерной машине, будет ли это иметь много преимуществ в таких ядрах?

Спасибо!


Из всех этих потоков большинство из них будут заблокированы обычно. Я не ожидаю, что соединения будут более 5 в минуту. Команды от клиента появятся нечасто, я бы сказал, что в среднем 20 в минуту.

Идя по ответам, которые я получаю здесь (переключение контекста было хитом производительности, о котором я думал, но я не знал этого, пока вы не отметили это, спасибо!) Я думаю, что я пойду за подход с одним слушатель, один приемник, один отправитель и некоторые другие вещи; -)

Ответ 1

Я пишу в .NET, и я не уверен, способ, которым я кодирую, из-за ограничений .NET и их дизайна API, или если это стандартный способ делать что-то, но так я сделал это в прошлом:

  • Объект очереди, который будет использоваться для обработки входящих данных. Это должно быть синхронизировано с синхронизацией между потоком очереди и рабочим потоком, чтобы избежать условий гонки.

  • Рабочий поток для обработки данных в очереди. В потоке, который ставит очередь в очередь данных, используется семафор, чтобы уведомить об этом поток для обработки элементов в очереди. Этот поток запускается перед любым другим потоком и содержит непрерывный цикл, который может работать до тех пор, пока он не получит запрос на завершение. Первая инструкция в цикле является флагом для приостановки/продолжения/завершения обработки. Флаг будет первоначально установлен на паузу, чтобы поток находился в состоянии ожидания (вместо непрерывного цикла), в то время как обработка не выполняется. Поток очереди будет менять флаг, когда в очереди будут обрабатываться элементы. Затем этот поток обрабатывает один элемент в очереди на каждой итерации цикла. Когда очередь пуста, она вернет флаг в паузу, чтобы на следующей итерации цикла он подождал, пока процесс очередности не уведомит об этом, что предстоит еще большая работа.

  • Один поток прослушивателя подключений, который прослушивает входящие запросы на соединение и передает их на...

  • поток обработки соединения, который создает соединение/сеанс. Наличие отдельного потока из потока прослушивания подключений означает, что вы уменьшаете вероятность пропущенных запросов на подключение из-за сокращения ресурсов, пока этот поток обрабатывает запросы.

  • Входящий поток прослушивателя данных, который прослушивает входящие данные о текущем соединении. Все данные передаются в очередь очередей, которые должны быть поставлены в очередь для обработки. Ваши потоки слушателей должны делать как можно меньше вне основного прослушивания и передачи данных для обработки.

  • Линия очереди, которая упорядочивает данные в правильном порядке, чтобы все можно было обработать правильно, этот поток поднимает семафор в очередь обработки, чтобы сообщить ему, какие данные должны обрабатываться. Наличие этого потока отдельно от приемника входящих данных означает, что вы с меньшей вероятностью пропустите входящие данные.

  • Некоторый объект сеанса, который передается между методами, так что каждый сеанс пользователя сам содержится во всей модели потоковой передачи.

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

Он также помогает с TDD (Test Driven Development), так что каждый поток обрабатывает одну задачу и намного легче тестировать код. Наличие сотен потоков может быстро стать кошмаром для распределения ресурсов, в то время как один поток становится кошмаром для обслуживания.

Намного проще сохранить один поток на одну логическую задачу таким же образом, как и один метод для каждой задачи в среде TDD, и вы можете логически отделить то, что каждый должен делать. Это легче обнаружить потенциальные проблемы и гораздо легче их исправить.

Ответ 2

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

в общем, гораздо более активные потоки, чем у вас есть ядра, будут тратить время на переключение контекста

если ваша игра состоит из множества коротких действий, круговая/рециркуляционная очередь событий даст лучшую производительность, чем фиксированное количество потоков

Ответ 3

Чтобы ответить на вопрос просто, совершенно неправильно использовать 200 потоков на сегодняшнем оборудовании.

Каждый поток занимает 1 Мб памяти, поэтому вы делаете 200 МБ файла страницы, прежде чем начинаете делать что-нибудь полезное.

В любом случае, разложите свои операции на маленькие кусочки, которые можно безопасно запускать в любом потоке, но помещайте эти операции в очереди и фиксируйте ограниченное количество рабочих потоков, обслуживающих эти очереди.

Обновление: Имеет смысл потратить 200 МБ? На 32-битной машине это 10% всего теоретического адресного пространства для процесса - никаких дополнительных вопросов. На 64-битной машине это звучит как падение в океане того, что может быть теоретически доступно, но на практике все еще очень большой кусок (а точнее, большое количество довольно больших кусков) хранилища, который бесцельно зарезервирован приложение, которое затем должно управляться ОС. Это приводит к тому, что окружающая каждая клиентская ценная информация имеет множество бесполезных дополнений, которые разрушают локальность, нанося поражение ОС и попыткам ЦП сохранять часто используемые материалы в самых быстрых слоях кеша.

В любом случае, потеря памяти - это всего лишь одна часть безумия. Если у вас нет 200 ядер (и ОС, способных использовать), то у вас на самом деле нет 200 параллельных потоков. У вас есть (скажем) 8 ядер, каждый из которых бешено переключается между 25 потоками. Наивно вы можете подумать, что в результате этого каждый поток испытывает эквивалент работы на ядре, который в 25 раз медленнее. Но на самом деле это намного хуже - ОС тратит больше времени на один поток с ядра и накладывает на него еще один ( "переключение контекста" ), чем фактически позволяет ваш код работать.

Просто посмотрите, как любой известный успешный проект решает эту проблему. Прекрасным примером служит пул потоков CLR (даже если вы его не используете). Он начинается с предположения, что достаточно одного потока на ядро. Это позволяет создавать больше, но только для того, чтобы в конечном итоге завершить работу плохо спроектированных параллельных алгоритмов. Он отказывается создавать более 2 потоков в секунду, поэтому эффективно наказывает алгоритмы, жадные алгоритмы, замедляя их.

Ответ 4

Какая ваша платформа? Если в Windows я предлагаю просмотреть операции асинхронного ввода и пулы потоков (или порты ввода-вывода ввода-вывода напрямую, если вы работаете на уровне API Win32 на C/С++).

Идея состоит в том, что у вас есть небольшое количество потоков, связанных с вашим I/O, и это позволяет вашей системе масштабировать до большого количества параллельных подключений, потому что нет никакой связи между количеством подключений и количеством используемых потоков посредством процесса, служащего им. Как и ожидалось,.NET изолирует вас от деталей, а Win32 - нет.

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

У меня есть бесплатный код, демонстрирующий различные серверные проекты на С++, используя IOCP здесь.

Если вы используете unix или должны быть перекрестной платформой, и вы находитесь на С++, вам может потребоваться усиление ASIO, которое обеспечивает функции асинхронного ввода-вывода.

Ответ 5

Я думаю, что вопрос, который вы должны задать, - это не то, что 200 как общий номер потока хорош или плох, а скорее, сколько из этих потоков будет активным.

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

Однако, если все из этих 200 потоков активны, у вас будет много времени на то, что ваш процессор будет тратить время на переключение контекста потока между всеми этими потоками ~ 200.