Во время учебы передо мной встала задача — написать HTTP сервер на плюсах. Задача в целом не сложная, интересна была бонусная часть — добавление многопоточности с одним условием - a worker should not be spawn for each client. На мое удивление я плохо пользуюсь гуглом информации по такому серверу в интернете оказалось мало. Все статьи про многопоточность были противоположны условию. В итоге, не без помощи своих иностранных коллег, я пришел к тому решению, которое изложу вам ниже. Приятного чтения!

Дисклеймер
Эта статья создана в первую очередь для моих коллег по учебе и тех, кто осваивает программирование самостоятельно. Все, что написано ниже, не претендует на идеальное решение и смело
требуетподвергается критики и улучшению.Я надеюсь, что у читателя есть понимание в таких терминах как событийно-ореинтированная архитектура, сокет, select, треды и все, что с ними связано.
Я намеренно опускаю технические подробности, дабы дать читателю возможность самому погрузиться в реализацию описанного ниже:)
По сути, все, что нужно для решения задачи, это конечный автомат и пул потоков.
Конечный автомат
У конечного автомата довольно сложное, на мой взгляд, определение. Я постараюсь объяснить легче.
Есть некий объект. У этого объекта есть состояние, при чем в каждый момент выполнения программы объект может находится только в одном состоянии. Состояний этих может быть сколько угодно(но количество их конечно).
В нашем случае объектом является клиент. У клиента есть четыре состояния - read_request, generate_response, send_response, close_connection. В завимости от состояния клиента, клиент обрабатывается соответствующим методом. Клиент инициализируется с состоянием read_request, и добавляется в очередь(что за очередь — чуть ниже). Клиент обрабатывается потоком (далее — воркер), переходит на следующее состояние и так по кругу. На этапе send_response клиент получает состояние либо close_connection(если он закрыл с нами соединение), либо read_request(если соединение осталось открытым).
Из описания видно, что важно обрабатывать состояния клиента последовательно - мы не можем отправить ответ, если он еще не сгенерирован, и не можем его сгенерировать, если не знаем запроса. В вашей реализации состояний у клиента может быть больше (или меньше), или вообще другие. К примеру, парсинг запроса можно вынести в отдельное состояние. Так же важно понимать, что клиент не обрабатывает сам себя! Мы делегируем эту обязанность на воркеров. Для чего это нужно — разберем ниже.
Пул потоков
Представьте себе офис продаж. В офисе идеально налажена коммуникация, несмотря на то, что работники социопаты не общаются между собой. Все дело в начальнике, которым изобрел отличный способ наладить работу в команде. В центре офиса он повесил доску, на которую он периодически вывешивает новые заявки. Работники берут их, и, в зависимости от состояния заявки, решают как ее обработать. Далее они переводят заявку на следующее состояние и вывешивают заявку обратно на доску.
А теперь подставьте вместо офиса HTTP сервер, вместо рабочего — воркер(что в целом одно и тоже), вместо доски — очередь, а вместо начальника — процесс, и вы получите описание работы воркеров в нашем сервере. В целом, опуская некоторые детали, все так оно и есть. Процесс добавляет новых клиентов в очередь, воркеры проверяют очередь на наличие новых задач(клиентов) и, если таковая есть, извлекают их, обрабатывают и, в зависимости от состояния, добавляют в очередь обратно.
А теперь о трудностях
Понятно, что все, что написано сверху, звучит легко(я надеюсь). В целом, так и есть. Но пару нюансов присутствует.
Не забывайте, что работаете с потоками. А это значит, что можно забыть о синхронности. Я две недели думал над тем, как "подружить" select и воркеров при отключении клиента из-за некорректного запроса. Клиент получал ответ, сокет клиента закрывался, но соединение в telnet оставалось. Все дело в том, что(сейчас может быть тяжело) воркер менял состояние клиента на close_connection уже ПОСЛЕ ТОГО, как основной процесс проверял состояние клиента и снова добавлял его в set для функции select. Решил я это добавив pipe, через который воркер оповещал процесс, что нужно проверить состояния клиентов еще раз.
Помните про общую память воркеров. С этим я решил не заморачиваться и каждый обработчик клиента я просто защитил мьютексом. Из-за этого в одном обработчике может находится только один воркер. Это минус. Можете подумать над тем, как распихать мьютексы умнее:)
Вот, в целом, и все
Я надеюсь, что данная информация поможет разобраться с этой темой. Ниже я оставлю ссылки, которые когда-то помогли мне. Надеюсь, помогут и вам:)
Simple Thread Pool Based on pthread implementation
Алгоритм Round-Robin (тоже, кстати, интересный способ организации воркеров, но, как мне кажется, чуть тяжелее)
Описание архитектуры nginx