Серьезное программирование для начинающих: Введение в параллельное программирование

В статье вы познаете базовые основы параллельного программирования, узнаете, зачем оно вообще нужно, и почему сервер на bgt — плохая идея.

Как работает сервер в однопоточном режиме

Нам нужно от чего-то отталкиваться, так что в качестве примера возьмём код эхо-сервера из документации bgt.
Эхо сервер слушает сеть, принимает все входящие подключения, и, получая сообщение от клиента, отправляет его обратно.
Я немного почистил код, чтобы вас ничего не отвлекало, и добавил комментарии на русском.


network host; // Создаём объект, отвечающий за сетевой функционал

void main()
{
show_game_window("Game Server"); // показываем окно сервера. На реальных серверах так делать не стоит, но для удобного запуска на домашнем компе можно оставить
if(host.setup_server(10000, 1, 100)==false) // настраиваем объект сети в режиме сервера и сразу проверяем результат
{
exit(); // если результат отрицательный, выключаемся
}
network_event event; // создаём объект, который будет хранить в себе обрабатываемое событие сервера
while(true) // входим в бесконечный цикл, здесь вся серверная логика
{
event=host.request(); // Проверяем, появились ли новые сетевые события
if(event.type==event_receive) // если сервер получил пакет данных (сообщение) от клиента
{
host.send_unreliable(event.peer_id, event.message, event.channel); // отправляем клиенту его сообщение обратно
}
if(key_down(KEY_LMENU) and key_pressed(KEY_F4)) // обрабатываем нажатие alt+f4. Работать будет только если в начале кода мы вывели окно. На настоящих серверах в терминале работать не будет
{
disconnect_everyone(); // вызываем написанную нами ниже функцию для правильного отключения всех клиентов
exit();
}
wait(5); // после каждой итерации цикла ждём 5 миллисекунд, чтобы не нагружать процессор (так рекомендуют в документации, да и без этой строчки нагрузка реально вырастает).
}
}

// функцию отключения всех клиентов разбирать мы не будем, она для нашего примера не так важна
void disconnect_everyone()
{
timer timeout;
uint[] peer_list=host.get_peer_list();
int expected_disconnects=peer_list.length();
for(uint i=0;i 0)
{
event=host.request();
if(event.type==event_disconnect)
{
expected_disconnects-=1;
}
if(timeout.elapsed>=1000)
{
break;
}
wait(5);
}
}

Разбираем нюансы и минусы

В любой программе инструкции исполняются последовательно (в рамках одного потока, конечно же).
Только когда заканчивает работу предыдущая инструкция, следующая может начать работу.
Так как bgt не поддерживает многопоточность, все его функции являются блокирующими.
Это значит, что, когда мы вызываем некую функцию, она тормозит программу, делает свою работу, возвращает результат и только после этого основной код продолжает работу.
С теорией разобрались; теперь посмотрим на пример сервера, зная про блокирующие функции и последовательное выполнение.
До начала бесконечного цикла while(true) разбирать нет смысла, так как запуск сервера может занимать хоть полсекунды, хоть полминуты. Для клиентов-игроков разницы никакой, они об этом не узнают никогда.
А вот всё, что находится внутри сетевого цикла, мы разберем подробно.
event=host.request();
Эта строка не блокирует выполнение, так как возвращает последнее событие моментально. Если событий не было, возвращает так называемое нулевое событие, которое мы вроде как не должны обрабатывать.
То есть, каждые 5 миллисекунд сервер получает новое событие и его обрабатывает (или игнорирует).
if(event.type==event_receive) …
Проверяем, если событие равно событию получения новых данных, мы эти данные обрабатываем.
В таком простом примере проблема не видна наглядно.
Но представим, что мы написали реальную игру, и вот в этом месте мы получаем от игрока информацию о том, что он сделал шаг.
Как только мы получили эту информацию, мы должны:
1. Проанализировать, кто из игроков мог услышать шаг нашего героя;
2. Разослать всем игрокам поблизости информацию об этом шаге;
3. Возможно, обработать наступание на мину или растяжку.

Наш сервер отлично справится с подобной задачей, но…
В то время, пока сервер обрабатывает логику одного единственного шага, все остальные клиенты ждут, так как функции обработки шага игрока заблокировали выполнение основного цикла и ещё не вернули результат.
Окей, шаг мы обработали…
А теперь рядом с нашим героем кто-то использовал tts-чат, отправив сообщение.
Как мы его получили и обработали — не важно.
Важно только то, что на сервере теперь есть некая последовательность большой длины, которую необходимо доставить клиенту.
Мы вызываем функцию отправки данных, скармливаем ей id нашего клиента и пакет, который нужно передать.
И… Функция блокирует основной цикл до полной передачи пакета.
Если длина пакета около 1024 байт, проблем возникнуть не должно.А вот если мы передаём нечто длиной в 32768 байт, это уже ощутимая задержка в пару миллисекунд точно.
Я лично проверял подобное поведение на примере небезызвестной игры Build And Survive. Попытавшись отправить tts-сообщение длиной в 250 слов я повесил сервер игры на 20 секунд.
Причем сервер завис для всех игроков, а не только для меня.
Дальше разбирать нет смысла, так как основная проблема однопоточного подхода кроется именно здесь, в отправке пакетов большой длины.

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

Магия многопоточности

А теперь представим, что основной сетевой цикл занимается только подключениями и игровой логикой.
«Но как же клиенты?» — спросите вы.
А для каждого клиента выделяется собственный поток, не блокирующий основной цикл.
Сервер запускается и ждёт входящих подключений.
Когда новый клиент подключается, сервер создаёт для клиента отдельный объект сети (тот же network, допустим), и в отдельном потоке вызывает отдельный сетевой цикл, в котором сервер обменивается данными с клиентом.
Когда клиент отключается, основной поток получает уведомление, а поток для клиента уничтожается, ресурс освобождается для новых клиентов.
Таким образом, отправляя или принимая большие данные от одного клиента, мы никак не блокируем работу с другими клиентами.
Да и игровая логика, выполняющаяся в основном потоке, никак не блокирует работу с клиентами.

И это не моё личное мнение. Такие выводы очевидны и легко проверяются.

Ещё один пример

Все вы знаете, что операции над файлами считаются довольно медленными по меркам компьютерного мира.
А ещё вы знаете, что сервер периодически должен делать бекап базы игроков на случай внезапного падения или другой катастрофы.
А теперь представим, что однопоточный сервер начинает бекапить базу из 100 игроков.
На каждого игрока приходится примерно от 20 до 50 килобайт данных (ник, пароль, инвентарь, количество патронов, здоровье, энергия и другие параметры).
Возьмём худший случай — 50 килобайт на игрока.
Получается, что база из 100 игроков весит около 5 мегабайт.
Запись пяти мегабайт данных заблокирует программу аж на несколько миллисекунд, в течение которых вся игровая логика заморозится, пули зависнут, сообщения в чатах не будут доставляться, а роботы встанут и перестанут стрелять.
Но это полбеды.
Когда файлы докопируются, на сервер разом прийдут все сетевые события, которые ждали своей очереди где-то на входе.
Игроки пытались ходить, стрелять, писать в чаты… И это всё нужно обработать.
Получается резкий скачок нагрузки, который заставит сервер ещё больше залагать.
Конечно, в полное зависание сервер не уйдёт, но тормоза точно заметят игроки, а это неприятно.

Заключение

BGT — лёгкий и простой в изучении язык программирования, который хорошо подходит для клиентской части игры.
Но сервер на bgt — это верный способ переплатить за аренду сервера с большей производительностью и в перспективе похоронить игру, когда в неё прийдёт больше 100 человек одновременно.

Серьезное программирование для начинающих: Введение в параллельное программирование: 4 комментария

  1. да, сервер на бгт делать такое себе. можно сделать сервер на тех же шарпах, а клиент на бгт. это упростит дело с потоками, но если написать сервер на шарпе, то стоит ли писать клиент на бгт? вот в чём вопрос. вообще, не хочу делать поспешных выводов :=, но те, кто с бгт ещё не расстался, пожмите ему на прощание руку.

    1. с чем быть? с сервером / клиентом? никто не запрещает тебе писать на бгт и то, и другое. можно научиться писать мультиплеер на бгт, понять, как всё работает в общих чертах, ну а потом можно выйти на более взрослый уровень, перейдя на другой язык. да и 100+ человек в игре ещё заслужить надо. чтобы такую игру сделать, мало код написать. 🙂

Обсуждение закрыто.