Основная проблема возникающая при реализации сервера, когда мы начинаем обрабатывать множество клиентов - это недопустимость долгой обработки клиентского запроса, что такое долго это отдельный разговор, но точно мы не должны допускать блокирующих системных вызовов, ведь тогда любой нерасторопный клиент повесит весь сервер. К сожалению, многие программисты не слышали о событийно-управляемой парадигме программирования, но знакомы с параллельной и для того, чтобы избежать блокировок на каждое новое соединения они порождают новый тред.
При кажущейся простоте параллельное программирование таит в себе множество коварных вещей по той причине, что оно страшно недетерминировано и работа программиста превращается в отстригание этого недетерминизма. Программист помимо того, чтобы просто реализовать взаимное исключение, что на самом деле задача далеко нетривиальная, должен реализовать его так, чтобы недопустить состояния "голодания", когда один процесс не может захватить ресурс.
Сервер в этом примере имеет один счетчик, который может изменяться в зависимости от того, что прислал клиент: up - добавить единицу, down - отнять, show - показать значение счетчика.
Если начать реализовывать в параллельном стиле, как говорилось выше, мы можем на каждое новое соединение создавать отдельный тред и сразу же отвязывать его. Для работы внутри треда нам понадобится: клиентский дескриптор, указатель на счетчик и, конечно же, мьютекс счетчика. Мы передали необходимые данные в тред, скопировали дескриптор, с помощью мьютекса реализовали раздельный доступ к счетчику и все это будет работать и возможно даже долго, но программа будет реализована неправильно. Здесь нужно остановиться и подумать, что могло пойти не так. Зачастую, в параллельном программировании мы можем обращатся к разделяемым данным и даже не понимать этого, и что самое противное выстрелит это неправильное обращение тогда, когда мы этого совсем не ждали, вдобавок ко всему ловить этот баг будет ещё то удовольствие. Из-за небольшого количества данных, передаваемых в тред, легко догадаться, что пока мы будем копировать значение клиентского дескриптора, наш тред может быть прерван и в этот момент придет новый клиент, который изменит значение дескриптора, а дальше может произойти всякое. Придется повесить на клиентский дескриптор семафор, чтобы главный тред засыпал на нем, пока номер дескриптора не будет скопирован порожденным тредом. Даже в таком, казалось бы простом примере, у нас уже появляются подводные камни. В данной задаче у нас всего один счетчик, но могут быть гораздо более сложные структуры данных и огранизовывать к каждой из них правильное взаимное исклюючение может быть очень непростой задачей.
Не вижу смысла усложнять итак нелегкую жизнь простого программиста и посмотреть на эту задачу с другой стороны. Избежать блокировок мы сможем, если будем точно знать, когда системный вызов нас не заблокирует, например, когда мы сможем прочитать хотя бы один байт от клиента. Когда клиентов у нас много, мы можем выбрать только тех, которые нам что-то прислали и обработать полученные данные. Главный цикл программы может быть реализован примерно так: для начала нам нужно будет определить от каких клиентов мы ожидаем получить данные в данный момент, после этого дождаться этих данных, и обработать их. В данном случае проблем с разделяемыми данными не будет, ведь мы будем опрашивать клиентов поочередно. Конечно, нам придется хранить для каждого клиента состояние сеанса, которое на самом деле неявно хранилось в каждом треде в виде этапа его выполения. Вдобавок ко всему, нужно будет реализовать список клиентов, который опять был неявно представлен тредами.
По итогу, реализовав сервер в событийно-управляемом стиле, мы смогли избежать проблем с разделяемыми данными, сэкономили используемую память - треды далеко не такие легкие как кажется на первый взгляд, где-то надо хранить контекст выполнения и избежали постоянных переключений между тредами, что тоже далеко не бесплатно.