Несмотря на большой опыт, довелось использовать только mutex и event; да еще разок скопипастил код с conditional variable. А хочется войти в тему поглубже — как в принципе грамотно организовать многопоточное приложение. Но если гуглить, везде в основном описывают разные примитивы синхронизации и что надо захватывать ресурсы в одном порядке, а это я и так знаю. Более интересны паттерны дизайна на эту тему, а также тонкие моменты.
Например, я как-то сделал класс для многопоточного счетчика через операцию ++. А тимлид сказал что так нельзя, ибо стандартная сигнатура операции ++ не позволяет корректно вернуть результат в многопоточной среде, и надо использовать функцию-член типа inc(). Вот таких нюансов хотелось бы (не в смысле С++, а в смысле принципов работы с переменными).
Конкретно, стоит задача написать сервер для небольшой сессионной MMO-игры. Я интуитивно понимаю, что клиентов надо обрабатывать в разных потоках, чтобы использовать все ядра проца. И притом желательно через тред-пул, поскольку клиент=поток будет слишком накладно если много клиентов. Но выразить это в грамотную архитектуру не могу (могу наколбасить связку из мутексов и возможно будет работать), но хочется ясного понимания как это спроектировать.
Что посоветуете прочитать?
P.S. Языком, скорее всего, будет не(!) С++, поэтому желательно без привязки к языку.
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
Здравствуйте, Basil2, Вы писали:
B>Несмотря на большой опыт, довелось использовать только mutex и event; да еще разок скопипастил код с conditional variable. А хочется войти в тему поглубже — как в принципе грамотно организовать многопоточное приложение. Но если гуглить, везде в основном описывают разные примитивы синхронизации и что надо захватывать ресурсы в одном порядке, а это я и так знаю. Более интересны паттерны дизайна на эту тему, а также тонкие моменты.
Я бы сказал что паттерна, основных, на данный момент два три: CSP, акторы и прямой путь в ад — шареная память. Самопальная реализация скорее зло, на которое можно идти если ты очень хорошо понимаешь что делаешь, тут, судя по вопросу, не тот случай. Хорошей реализации CSP для C++ не знаю, а про акторов может so5team подсказать, они тут часто свой SObjectizer рекламируют
.
B>Конкретно, стоит задача написать сервер для небольшой сессионной MMO-игры. Я интуитивно понимаю, что клиентов надо обрабатывать в разных потоках, чтобы использовать все ядра проца. И притом желательно через тред-пул, поскольку клиент=поток будет слишком накладно если много клиентов. Но выразить это в грамотную архитектуру не могу (могу наколбасить связку из мутексов и возможно будет работать), но хочется ясного понимания как это спроектировать.
Я правильно понимаю, что планируется писать на C++? Если да, то можно начать с азов, C++ Concurrency in Action
, как раз недавно свежачок подвезли. Эту книгу стоит прочитать вне зависимости от того, будете пользоваться фрэймворком или будете велосипедить.
B>Что посоветуете прочитать?
Ну и вообще, я бы порекомендовал подумать про инструмент в первую очередь и, если здравый смысл возобладает, не брать C++ для этой задачи, он тут не нужен ну вот вообще совсем. Я, как обычно, советую Go — быстро и просто с приличной скоростью. Если здравый смысл не победит и вы будете писать на C++, то вот прям ну крайне важно начать с фреймфорка, который будет решать вам задачи связанные с многопоточтностью, велосипед будет дорогой и мучительный.
Здравствуйте, Basil2, Вы писали:
B>Несмотря на большой опыт, довелось использовать только mutex и event; да еще разок скопипастил код с conditional variable. А хочется войти в тему поглубже — как в принципе грамотно организовать многопоточное приложение. Но если гуглить, везде в основном описывают разные примитивы синхронизации и что надо захватывать ресурсы в одном порядке, а это я и так знаю. Более интересны паттерны дизайна на эту тему, а также тонкие моменты.
Есть хорошая книга "Параллельное и распределённое программирование на С++", там рассказывают немного о том как вообще можно организовать многопоточность с отсылкой к сетям Петри для моделирования. Советую ознакомится.
B>Например, я как-то сделал класс для многопоточного счетчика через операцию ++. А тимлид сказал что так нельзя, ибо стандартная сигнатура операции ++ не позволяет корректно вернуть результат в многопоточной среде, и надо использовать функцию-член типа inc(). Вот таких нюансов хотелось бы (не в смысле С++, а в смысле принципов работы с переменными).
B>Конкретно, стоит задача написать сервер для небольшой сессионной MMO-игры. Я интуитивно понимаю, что клиентов надо обрабатывать в разных потоках, чтобы использовать все ядра проца. И притом желательно через тред-пул, поскольку клиент=поток будет слишком накладно если много клиентов. Но выразить это в грамотную архитектуру не могу (могу наколбасить связку из мутексов и возможно будет работать), но хочется ясного понимания как это спроектировать.
Я так понимаю что при воздействии игрока на мир он должен пересчитываться. Мне кажется надо обрабатывать не клиента, а запросы, а клиент это тупо некоторая модель данных.
Здравствуйте, Basil2, Вы писали:
B>Несмотря на большой опыт, довелось использовать только mutex и event; да еще разок скопипастил код с conditional variable. А хочется войти в тему поглубже — как в принципе грамотно организовать многопоточное приложение. Но если гуглить, везде в основном описывают разные примитивы синхронизации и что надо захватывать ресурсы в одном порядке, а это я и так знаю. Более интересны паттерны дизайна на эту тему, а также тонкие моменты.
B>Например, я как-то сделал класс для многопоточного счетчика через операцию ++. А тимлид сказал что так нельзя, ибо стандартная сигнатура операции ++ не позволяет корректно вернуть результат в многопоточной среде, и надо использовать функцию-член типа inc(). Вот таких нюансов хотелось бы (не в смысле С++, а в смысле принципов работы с переменными).
B>Конкретно, стоит задача написать сервер для небольшой сессионной MMO-игры. Я интуитивно понимаю, что клиентов надо обрабатывать в разных потоках, чтобы использовать все ядра проца. И притом желательно через тред-пул, поскольку клиент=поток будет слишком накладно если много клиентов. Но выразить это в грамотную архитектуру не могу (могу наколбасить связку из мутексов и возможно будет работать), но хочется ясного понимания как это спроектировать.
B>Что посоветуете прочитать?
Здравствуйте, Kernan, Вы писали:
K>Есть хорошая книга "Параллельное и распределённое программирование на С++", там рассказывают немного о том как вообще можно организовать многопоточность с отсылкой к сетям Петри для моделирования. Советую ознакомится.
Спасибо, присмотрюсь. Только язык видимо будет уже не С++.
K>Я так понимаю что при воздействии игрока на мир он должен пересчитываться. Мне кажется надо обрабатывать не клиента, а запросы, а клиент это тупо некоторая модель данных.
Да; я просто считал что "клиент = запрос(ы)". Тут правда в предыдущем посте подкинули идею с акторами и это действительно идея. Не лочить игровой мир при модификации, а просто отправлять все изменения "хозяину мира", а он сам их проставит безо всяких лочек.
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
Здравствуйте, kaa.python, Вы писали:
KP>Я правильно понимаю, что планируется писать на C++? Если да, то можно начать с азов,
Нет, будет скорее всего Rust.
KP>Я бы сказал что паттерна, основных, на данный момент два три: CSP, акторы и прямой путь в ад — шареная память. Самопальная реализация скорее зло, на которое можно идти если ты очень хорошо понимаешь что делаешь, тут, судя по вопросу, не тот случай.
Изначально я думал рискнуть и попробовать шареную память — заодно и узнать, так ли хороша защита для многопоточности в Rust. CSP я загуглил но не понял, а вот идея с акторами мне понравилась. Типа есть "хозяин мира", то есть карты на которой идет игра, и есть обработчики событий от игроков. И обработчики событий не пытаются сами лезть в карту под лочкой, а шлют хозяину запросы. А хозяин все делает вообще без лочек, т.к. он единственный владелец карты. По идее, в Rust тоже есть каналы (как и в Go), так что может получиться относительно нативно, без особых фреймворков.
Но это не отменяет общей архитектуры. Ведь так получаются целых три слоя потоков: сетевые с соединением (плюс главный на котором сделан listen), обработчики запросов от клиентов, владельцы карт (игровых сессий). И все это как-то надо разруливать...
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
Здравствуйте, Basil2, Вы писали:
B>Например, я как-то сделал класс для многопоточного счетчика через операцию ++. А тимлид сказал что так нельзя, ибо стандартная сигнатура операции ++ не позволяет корректно вернуть результат в многопоточной среде, и надо использовать функцию-член типа inc(). Вот таких нюансов хотелось бы (не в смысле С++, а в смысле принципов работы с переменными).
Интересно, а тим-лид как-то обосновал, почему ++ не подходит, или это его суеверия не велят так делать?
B>Конкретно, стоит задача написать сервер для небольшой сессионной MMO-игры. Я интуитивно понимаю, что клиентов надо обрабатывать в разных потоках, чтобы использовать все ядра проца. И притом желательно через тред-пул, поскольку клиент=поток будет слишком накладно если много клиентов. Но выразить это в грамотную архитектуру не могу (могу наколбасить связку из мутексов и возможно будет работать), но хочется ясного понимания как это спроектировать.
Если хочется выжать из железа 100%, это, как раз, не слишком светлая идея, выделять по потоку на клиента. Ну а если 100% из железа выжимать не обязательно, зато хочется простого и понятного кода, я бы написал эту программу на Go, как тут по соседству уже посоветовали. Проигрыш в скорости будет не очень существенным, а вот с кодом будет куда как приятнее иметь дело.
И да, в Go-то, как раз, поощряется заводить потоки в большом количестве. Но надо понимать, что потоки Go это не то же самое, что потоки операционной системы.
Обширный теоретический труд, с самого нуля и примеров "из жизни" до lock-free и аппаратных деталей синхронизации x86.
Плюсы — материал подается с нуля, содержит всю необходимую теорию для решения сложных задач синхронизации.
Минусы — примеры кода на Java, изложение не привязано ни к какому языку, так что C++ memory model придется учить отдельно, библиотеки тоже не рассматриваются
Здравствуйте, Basil2, Вы писали:
B>Спасибо, присмотрюсь. Только язык видимо будет уже не С++.
Это не так важно. Концепции везде одинаковые.
K>>Я так понимаю что при воздействии игрока на мир он должен пересчитываться. Мне кажется надо обрабатывать не клиента, а запросы, а клиент это тупо некоторая модель данных. B>Да; я просто считал что "клиент = запрос(ы)". Тут правда в предыдущем посте подкинули идею с акторами и это действительно идея. Не лочить игровой мир при модификации, а просто отправлять все изменения "хозяину мира", а он сам их проставит безо всяких лочек.
Хозяин мира будет узким местом. Надо делить мир на куски и обсчитывать изменения по кускам для всех где есть взаимодействующие объекты. Это очень похоже на то, что происходит в рендере сцены когда надо просчитать взаимодейсвия предметов, а не взаимодействующие просто отсечь. Будут те же самые деревья и т.п. По сути, всё сведётся к большому многопточному конвееру в котром бдует обрабатываться маленький кусочек мира, а артефакт обработки — дельта кусочка или нескольких.
Здравствуйте, Basil2, Вы писали:
B>Изначально я думал рискнуть и попробовать шареную память — заодно и узнать, так ли хороша защита для многопоточности в Rust. CSP я загуглил но не понял, а вот идея с акторами мне понравилась. Типа есть "хозяин мира", то есть карты на которой идет игра, и есть обработчики событий от игроков. И
CSP ооочень приятный. Я использую его реализацию в boost::fiber, но с документацией там не очень.
Про варианты можно послушать толковую лекцию от одного из парней SObjectizer'а :
Здравствуйте, Basil2, Вы писали:
B>Нет, будет скорее всего Rust.
Хороший выбор, надеюсь, у вас есть кто-то с хорошим знанием этого весьма не тривиального языка.
B>Но это не отменяет общей архитектуры. Ведь так получаются целых три слоя потоков: сетевые с соединением (плюс главный на котором сделан listen), обработчики запросов от клиентов, владельцы карт (игровых сессий). И все это как-то надо разруливать...
Думается мне, у игроделов какие-то свои паттерны широко распространенные есть. Оттуда и стоит копать.7
Здравствуйте, Pzz, Вы писали:
B>>Например, я как-то сделал класс для многопоточного счетчика через операцию ++. А тимлид сказал что так нельзя, ибо стандартная сигнатура операции ++ не позволяет корректно вернуть результат в многопоточной среде, и надо использовать функцию-член типа inc(). Вот таких нюансов хотелось бы (не в смысле С++, а в смысле принципов работы с переменными).
Pzz>Интересно, а тим-лид как-то обосновал, почему ++ не подходит, или это его суеверия не велят так делать?
Конечно. ++ возвращает ссылку, а она невалидна в многопоточной среде. А если возвращать значение, то нарушится стандартная сигнатура, что тоже нехорошо.
B>>И притом желательно через тред-пул, поскольку клиент=поток будет слишком накладно если много клиентов. Но выразить это в грамотную архитектуру не могу (могу наколбасить связку из мутексов и возможно будет работать), но хочется ясного понимания как это спроектировать.
Pzz>Если хочется выжать из железа 100%, это, как раз, не слишком светлая идея, выделять по потоку на клиента.
Я вроде бы так и написал.
Pzz>И да, в Go-то, как раз, поощряется заводить потоки в большом количестве. Но надо понимать, что потоки Go это не то же самое, что потоки операционной системы.
Я понимаю. Встроенный тред-пул это удобно — интересно, есть ли в Rust нечто подобное.
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
Здравствуйте, kaa.python, Вы писали:
B>>Нет, будет скорее всего Rust. KP>Хороший выбор, надеюсь, у вас есть кто-то с хорошим знанием этого весьма не тривиального языка.
После С++11 выглядит очень просто: move-семантика, ссылки, лямбда-функции, RW-lock, Option<T> — всё очень знакомо. Итераторы как у LUA, обработка ошибок по Александреску, паттерн матчинг просто офигенен. Я на пробу переделал с С++ на Раст тестовое задание одной из крупных контор (типа есть модель сотрудник-отдел-фирма, надо считать их из файла и поставить каждой сущности задачу через интерфейс с консоли) — получилось без особых проблем. Даже пресловутый borrow checker сработал единственный (!) раз, когда я пытался считать строку из консоли в буфер
KP>Думается мне, у игроделов какие-то свои паттерны широко распространенные есть. Оттуда и стоит копать.
Да, уже посоветовали книжку в тему.
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
B>Да; я просто считал что "клиент = запрос(ы)". Тут правда в предыдущем посте подкинули идею с акторами и это действительно идея. Не лочить игровой мир при модификации, а просто отправлять все изменения "хозяину мира", а он сам их проставит безо всяких лочек.
Что такое "хозяин мира" ? Сервер ?
Как он проставит без лочек не совсем понятно, например два игрока одновременно шагают в одну точку, на сервер прилетает 2 запроса 2 потока обрабатывает, если не делать lock карты при изменении состояния, то могут одновременно в одной "клетке" оказаться, а должно быть что кто-то один ее занял а у второго сработала коллизия.
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов
Здравствуйте, Basil2, Вы писали:
B>Я понимаю. Встроенный тред-пул это удобно — интересно, есть ли в Rust нечто подобное.
Нет. Они обещали на ранних этапах развития языка, но не осилили.
В Go несколько больше, чем тред-пул. Например, если запустить Go 100500 потоков, которые что-то делают с сетью, то операционная система будет видеть количество потоков, сравнимое с количеством процессоров, и ожидание готовности будет осуществляться наиболее подходачим для системы опразом — через completion port в венде, через epoll в линухе и т.п.
B>>Да; я просто считал что "клиент = запрос(ы)". Тут правда в предыдущем посте подкинули идею с акторами и это действительно идея. Не лочить игровой мир при модификации, а просто отправлять все изменения "хозяину мира", а он сам их проставит безо всяких лочек.
O>Что такое "хозяин мира" ? Сервер ?
Объект, который владеет картой. Условно, экземпляр класса с текущей сессией игры.
O>Как он проставит без лочек не совсем понятно, например два игрока одновременно шагают в одну точку, на сервер прилетает 2 запроса 2 потока обрабатывает, если не делать lock карты при изменении состояния, то могут одновременно в одной "клетке" оказаться, а должно быть что кто-то один ее занял а у второго сработала коллизия.
Оба запроса кидаются в очередь хозяину карты. Хозяин их вычитывает из очереди и выполняет. Лочить карту ему не нужно, ведь он ее владелец — никто другой с ней не работает.
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
KP>>Я бы сказал что паттерна, основных, на данный момент два три: CSP, акторы и прямой путь в ад — шареная память. Самопальная реализация скорее зло, на которое можно идти если ты очень хорошо понимаешь что делаешь, тут, судя по вопросу, не тот случай.
B>Изначально я думал рискнуть и попробовать шареную память — заодно и узнать, так ли хороша защита для многопоточности в Rust. CSP я загуглил но не понял, а вот идея с акторами мне понравилась. Типа есть "хозяин мира", то есть карты на которой идет игра, и есть обработчики событий от игроков. И обработчики событий не пытаются сами лезть в карту под лочкой, а шлют хозяину запросы. А хозяин все делает вообще без лочек, т.к. он единственный владелец карты. По идее, в Rust тоже есть каналы (как и в Go), так что может получиться относительно нативно, без особых фреймворков.
Это означает, что каждый владыка мира может работать только на одном потоке.
B>Но это не отменяет общей архитектуры. Ведь так получаются целых три слоя потоков: сетевые с соединением (плюс главный на котором сделан listen), обработчики запросов от клиентов, владельцы карт (игровых сессий). И все это как-то надо разруливать...
Поздравляю, ты изобрел конвейер (возможно, что-то похожее Охотников называл SEDA, но это не точно). У тебя сеть работает на одном потоке, которая передает данные на другой поток (о новых подключениях или результат poll'инга (есть данные)), обработчики сетевых соединений работают на своем пуле потоков, отдают данные обработчикам событий (другой пул потоков), те отдают данные владыкам миров (каждый максимум на одном потоке, но для всех будет пул), те отдают данные обратно обработчикам событий -> сетевых подключений. Вполне рабочая архитектура.
Poller -> Читатели(писатели) <-> сериализаторы и обработчики событий <-> владыки миров (меняют мир и формируют ответ на запрос)
Я бы это реализовал на корутинах C++. Одна корутина читает и десериализует события, ставит запрос к владыке мира в очередь. Владыки работают на своем пуле. Далее корутина, которая записывает ответ от владыки в сеть.
B>Оба запроса кидаются в очередь хозяину карты. Хозяин их вычитывает из очереди и выполняет. Лочить карту ему не нужно, ведь он ее владелец — никто другой с ней не работает.
Но тогда очередь должна быть потокобезопасной, т.е. лочим не карту а очередь.
Например 2 потока пытаются одновременно добавить записи в очередь, а хозяин еще и читать ее пытается, если там не будет критической секции то может получиться непредсказуемое поведение.
”Жить стало лучше... но противнее. Люди которые ставят точку после слова лучше становятся сторонниками Путина, наши же сторонники делают акцент на слове противнее ( ложь, воровство, лицемерие, вражда )." (с) Борис Немцов