Pull to refresh

Как я воркеров в сервере организовывал

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

Дисклеймер

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

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

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

По сути, все, что нужно для решения задачи, это конечный автомат и пул потоков.

Конечный автомат

У конечного автомата довольно сложное, на мой взгляд, определение. Я постараюсь объяснить легче.

Есть некий объект. У этого объекта есть состояние, при чем в каждый момент выполнения программы объект может находится только в одном состоянии. Состояний этих может быть сколько угодно(но количество их конечно).

В нашем случае объектом является клиент. У клиента есть четыре состояния - read_request, generate_response, send_response, close_connection. В завимости от состояния клиента, клиент обрабатывается соответствующим методом. Клиент инициализируется с состоянием read_request, и добавляется в очередь(что за очередь — чуть ниже). Клиент обрабатывается потоком (далее — воркер), переходит на следующее состояние и так по кругу. На этапе send_response клиент получает состояние либо close_connection(если он закрыл с нами соединение), либо read_request(если соединение осталось открытым).

Из описания видно, что важно обрабатывать состояния клиента последовательно - мы не можем отправить ответ, если он еще не сгенерирован, и не можем его сгенерировать, если не знаем запроса. В вашей реализации состояний у клиента может быть больше (или меньше), или вообще другие. К примеру, парсинг запроса можно вынести в отдельное состояние. Так же важно понимать, что клиент не обрабатывает сам себя! Мы делегируем эту обязанность на воркеров. Для чего это нужно — разберем ниже.

Пул потоков

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

А теперь подставьте вместо офиса HTTP сервер, вместо рабочего — воркер(что в целом одно и тоже), вместо доски — очередь, а вместо начальника — процесс, и вы получите описание работы воркеров в нашем сервере. В целом, опуская некоторые детали, все так оно и есть. Процесс добавляет новых клиентов в очередь, воркеры проверяют очередь на наличие новых задач(клиентов) и, если таковая есть, извлекают их, обрабатывают и, в зависимости от состояния, добавляют в очередь обратно.

А теперь о трудностях

Понятно, что все, что написано сверху, звучит легко(я надеюсь). В целом, так и есть. Но пару нюансов присутствует.

  1. Не забывайте, что работаете с потоками. А это значит, что можно забыть о синхронности. Я две недели думал над тем, как "подружить" select и воркеров при отключении клиента из-за некорректного запроса. Клиент получал ответ, сокет клиента закрывался, но соединение в telnet оставалось. Все дело в том, что(сейчас может быть тяжело) воркер менял состояние клиента на close_connection уже ПОСЛЕ ТОГО, как основной процесс проверял состояние клиента и снова добавлял его в set для функции select. Решил я это добавив pipe, через который воркер оповещал процесс, что нужно проверить состояния клиентов еще раз.

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

Вот, в целом, и все

Я надеюсь, что данная информация поможет разобраться с этой темой. Ниже я оставлю ссылки, которые когда-то помогли мне. Надеюсь, помогут и вам:)

Simple Thread Pool Based on pthread implementation
Алгоритм Round-Robin (тоже, кстати, интересный способ организации воркеров, но, как мне кажется, чуть тяжелее)
Описание архитектуры nginx

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.