Многопоточность в Photon Plugin

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Плагины в фотоне предназначены для того, чтобы запускать какую-то свою логику на своём Photon облаке. Так же даже если используется Photon SDK плагин является более быстрым способом, получить что-то работающее без погружения в дебри SDK.

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

И так мы начали говорить, о том, что сервер может выполнять часть логики. Какая это может быть логика? Если коротко, то любая. Но я бы всё равно выделил основные направления:

  • симуляция, когда сервер обсчитывает мир и клиенты с ним синхронизируются

  • всевозможного вида проверки, чтобы обеспечить защиту игроков как от читеров так и от хакеров.

  • взаимодействие с бэкендами разработчиков, чтобы поддержать социализацию игроков, собирать статистику и т.д. и т.п.

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

Прежде чем объяснить природу упущения, коснёмся, сначала, вопроса - как устроена многопоточность в фотоне и в плагине в частности.

Я предполагаю, что статью про файберы уже читали и знаете, что это такое. Если коротко, то файбер это очередь задач построенная либо надо пулом потоков, либо над одним потоком. Мы будем говорить про версию, работающую с пулом потоков. Остальные варианты не используются, поэтому не интересны. Вариант с пулом потоков исполняет накопившиеся задачи в системном пуле потоков, так что все они исполняются строго по порядку. Но приэтом каждый раз это может быть новый поток. [[Photon Fibers|Подробности тут]].

И так, в photon каждый пир и каждая комната имеет свой файбер. Сообщение от клиента попадает в файбер пира и там десеарилизуется и проверяется. Если всё хорошо, пир передаёт это сообщение в файбер комнаты, где оно ещё раз может быть подвергнуто проверкам и только после этого оно попадает в плагин. Такое разделение ответственности позволяет нам эффективно использовать возможности многопроцессорных/многоядерных систем.

Так же, из вышесказанного, следует, что плагин работает в файбере комнаты. Это в свою очередь означает, что из всех методов IPhotonPlugin можно безопасно работать с данными комнаты. Коллбэки для таймеров и http запросов созданных с помощью методов IPluginHost так же вызываются в файбере комнаты и доступ к данным комнаты из них безопасен.

Теперь про упущение - упущение всегда одно и тоже: разработчики начинают обращаться к данным комнаты не из файбера комнаты, что, обычно, завершается тремя вариантами:

  • повезло и ничего не упало

  • не обработанное исключение в управляемом коде -> рестарт всего процесса

  • не обработанное исключение в нэйтив коде -> рестарт всего процесса

Теперь как же всё это победить? Победить очень просто. Метод IPluginHost.Enqueue предназначен специально для таких случаев. Он наша палочка-выручалочка во всех возможных сценариях.

Для целей статьи выделю два сценария:

  1. мы что-то делаем независимо от файбера комнаты и по результатам работы нам надо, например, разослать сообщения клиентам без использования async/await

  2. то же что и 1, но с использованием async/await

Первый сценарий решается легко: мы просто вызываем IPluginHost.Enqueue и передаём ему экшен. Ну что-то вроде этого:

// что-то считали, вычисляли запрашивали и результируем :)
this.PluginHost.Enqueue(()=>
{
    this.BroadcastEvent(....)
}
);

Второй сценарий более интересен, потому что до сих пор использование async/await было нерекомендовано и предлагаемые решения были угловаты. Но как мне кажется есть одно решение получше - это synchronization context.

Сразу оговорюсь, что в версии ExitGamesLibs.dll 1.3.34 PoolFiber and ThreadFiber имеют свой synchronization context и async/await будет работать правильно из коробки, но пока она недоступна широкому кругу разработчиков, поэтому обсудим решение проблемы, если, как это часто бывает, нужно вчера.

Решение простое, но требует от разработчика внимательности.

  1. создаётся класс-наследник класса SynchronizationContext

  2. каждый плагин создаёт его и держит на готове

  3. перед использованием await текущим контекстом выставляется контекст плагина.

  4. сбрасываем в правильный момент на предыдущий

Класс-наследник

public class PluginSychContext : SynchronizationContext
{
    private readonly IPluginHost host;
    public PluginSychContext(IPluginHost host)
    {
        this.host = host;
    }
    
    public override void Post(SendOrPostCallback d, object state)
    {
        this.host.Enqueue(() => d(state));
    }
}

Всё. Вот так просто.

Пункт два пропустим и перейдём к 3 и 4.

public void CallerForAsyncMethod()
{
    var old = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(this.pluginContext);
    try
    {
        this.AsyncMethod();
    }
    finally
    {
        SynchronizationContext.SetSynchronizationContext(old);
    }
}

public async void AsyncMethod()
{
    await Task.Delay(500);
}

Для того, чтобы установить и сбросить контекст пришлось добавить специальный метод. Строго говоря, он не нужен, чтобы выставить новый контекст, но он нужен, чтобы корректно его сбросить. Контекст устанавливается для потока, код после await в общем случае будет выполнятся в другом потоке, не в том в котором контекст выставлялся.

Очень надеюсь, что этот приём поможет в разработке плагина. Частенько разработчикам приходится использовать сторонние SDK для работы с бэкэндами. Например, AWS SDK. вот в таких случаях этот приём будет работать. Он так же будет работать, если после обновления PoolFiber будет использовать собственный SynchronizationContext.

В заключении, хочу привести ссылку на шикарную статью Стефана Тоуба про контексты. Именно она и натолкнула меня на решение.

Источник: https://habr.com/ru/articles/766400/


Интересные статьи

Интересные статьи

 Что надо знать, чтобы успешно применять-реализовать многопоточность (Multithreading) в своей программе? Мне кажется есть некоторые неудобные для изложения куски в разных описаниях потоков и того...
В первой части статьи мы остановились на моменте, когда с помощью распределения задач между потоками по алгоритму Round-robin мы добились-таки ускорения работы приложения за счет многопоточности.Но во...
Вот уже в который раз хочется поднять тему многопоточного программирования. Сейчас я попытаюсь донести мысль, что если посмотреть на эту тему под другим - более простым, как мне кажется, углом, то она...
... но и любой другой логгер.Традиционно Photon Server SDK поставляется с log4net. Но это не значит что все им должны пользоваться. Пользоваться можно практически любым логгером. Всё что ...
Публикую на Хабр оригинал статьи, перевод которой размещен в корпоративном блоге. Необходимость делать что-то асинхронно, не дожидаясь результат здесь и сейчас, или разделять большую работу ме...