Много потоков или как можно меньше потоков? - PullRequest
10 голосов
/ 17 декабря 2008

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

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

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

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

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

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

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

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

Спасибо!


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

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

Ответы [ 5 ]

7 голосов
/ 17 декабря 2008

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

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

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

5 голосов
/ 17 декабря 2008

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

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

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

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

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

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

4 голосов
/ 17 декабря 2008

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

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

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

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

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

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

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

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

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

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

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

2 голосов
/ 17 декабря 2008

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

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

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

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

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

0 голосов
/ 17 декабря 2008

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

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

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

...