Хабрахабр

Сервер Игры на MS Orleans — часть 1: Что такое Акторы

Привет Хабр! И так, на четырнадцатый день копья решил я значит начать делать простенький игровой сервер для простой онлайн стрелялки. За одно тему распределенных вычислений затронуть. В этой вводной статье цикла хочу рассказать что такое акторы (в Орлеанс их зернами называют) и принцип их работы. Для этого я пока пример простенького приложения с самодельными акторами без Orleans. Как говориться прежде чем строить корабль посмотрим как плавает и почему плавает обычный бумажный кораблик. За подробностями добро пожаловать под кат.

Содержание

  • Сервер Игры на MS Orleans — часть 1: Что такое Акторы

Акторы

Актор является вычислительной сущностью, которая в ответ на полученное сообщение может одновременно:
отправить конечное число сообщений другим акторам;
создать конечное число новых акторов;
выбрать поведение, которое будет использоваться при обработке следующего полученного сообщения.
Не предполагается существования определённой последовательности вышеописанных действий и все они могут выполняться параллельно.
Мы посылаем актору какие-то сообщения (Команды или События) и он сам может посылать другим такие же сообщения. По похожему принципу работаю корутины с каналами. Они принимает какие-то данные из канала и отправляет какие-то данные в канал работая при этом асинхронно.

Пример

Собственно код для двух очень простых акторов:

 class Program { interface IMessage { } class IncrementCommand : IMessage { } class TellCountCommand : IMessage { } class SaidCountEvent : IMessage { public int Count { get; } public int ActorId { get; } public SaidCountEvent(int count, int actorId) { Count = count; ActorId = actorId; } } class WriteMessageCommand : IMessage { public string Message { get; } public WriteMessageCommand(string message) { Message = message; } } static Task CreateCounterActor( BlockingCollection<IMessage> output, BlockingCollection<IMessage> input, int id ) { return Task.Run(() => { var count = 0; while (true) { var m = input.Take(); if (m is IncrementCommand) count++; if (m is TellCountCommand) output.Add(new SaidCountEvent(count, id)); } }); } static Task CreateWriterActor(BlockingCollection<IMessage> input) { return Task.Run(() => { while (true) { var m = input.Take(); if (m is WriteMessageCommand write) Console.WriteLine(write.Message); if (m is SaidCountEvent sc) Console.WriteLine( $"Counter сейчас равен {sc.Count} для актора {sc.ActorId}" ); } }); } static void Main(string[] args) { var writerInput = new BlockingCollection<IMessage>(); var firstInput = new BlockingCollection<IMessage>(); var secondInput = new BlockingCollection<IMessage>(); var writer = CreateWriterActor(writerInput); var firstCounter = CreateCounterActor(writerInput, firstInput, 1); var secondCounter = CreateCounterActor(writerInput, secondInput, 2); for (int i = 0; i < 5; i++) { firstInput.Add(new IncrementCommand()); } for (int i = 0; i < 9; i++) { secondInput.Add(new IncrementCommand()); } firstInput.Add(new TellCountCommand()); secondInput.Add(new TellCountCommand()); writerInput.Add(new WriteMessageCommand("Конец метода Main")); Console.ReadLine(); } }
  1. Интерфейс которым мы помечаем сообщения которые получает или отправляет актор:
    interface IMessage { }

  2. Команда с помощью которой мы говорим актору увеличить его счетчик (внутренне состояние):
    class IncrementCommand : IMessage { }

  3. Команда с помощью которой мы говорим актору сказать его текущее состояние (счетчик) другим:
    class TellCountCommand : IMessage { }

  4. Событие которое говорит другим акторам о том что актор сказал всем свое текущее состояние (счетчик). Генерируется при обработке команды TellCountCommand:
    class SaidCountEvent : IMessage{ public int Count { get; } public int ActorId { get; } public SaidCountEvent(int count, int actorId) { Count = count; ActorId = actorId; }}

    Count это сколько сейчас набралось на счетчике у актора с идентификатором равным ActorId

  5. Эта команда говорит актору вывести данное сообщение на консоль:
    class WriteMessageCommand : IMessage{ public string Message { get; } public WriteMessageCommand(string message) { Message = message; }}

  6. Запускает инстанс актора который управляет текущим состоянием счетчика:
     static Task CreateCounterActor( //Исходящие сообщения из актора BlockingCollection<IMessage> output, //Входящие сообщения BlockingCollection<IMessage> input, // Идентификатор по которому мы будем различать разные инстансы актора одного и того же типа int id ) { return Task.Run(() => { //Внутренне состояние актора. То чем он управляет var count = 0; while (true) { //Достаем следующее сообщение из потокобезопасной коллекции //Выполнение блокируется на этом месте до тех пор пока в коллекции не появится значение var m = input.Take(); if (m is IncrementCommand) count++; if (m is TellCountCommand) output.Add(new SaidCountEvent(count, id)); } }); }

  7. Создает актор который просто пишет сообщения в консоль:
     static Task CreateWriterActor(BlockingCollection<IMessage> input) { return Task.Run(() => { while (true) { var m = input.Take(); if (m is WriteMessageCommand write) Console.WriteLine(write.Message); if (m is SaidCountEvent sc) Console.WriteLine( $"Counter сейчас равен {sc.Count} для актора {sc.ActorId}" ); } }); }

  8.  static void Main(string[] args) { var writerInput = new BlockingCollection<IMessage>(); var firstInput = new BlockingCollection<IMessage>(); var secondInput = new BlockingCollection<IMessage>(); var writer = CreateWriterActor(writerInput); //Создаем два актора одного типа с разными идентификаторами var firstCounter = CreateCounterActor(writerInput, firstInput, 1); var secondCounter = CreateCounterActor(writerInput, secondInput, 2); //Пять раз говорим первому актору увеличить его счетчик for (int i = 0; i < 5; i++) { firstInput.Add(new IncrementCommand()); } //Девять раз говорим второму актору увеличить его счетчик. for (int i = 0; i < 9; i++) { secondInput.Add(new IncrementCommand()); } //Говорим акторам сказать всем сколько у них набралось на счетчике. //Так как они отправляют свои события writer актору он выводит их на экран firstInput.Add(new TellCountCommand()); secondInput.Add(new TellCountCommand()); //говорим writer актору вывести сообщение в консоль. writerInput.Add(new WriteMessageCommand("Конец метода Main")); Console.ReadLine(); }

Тут использовалась реактивная модель. Наши акторы не возвращают значения а только генерируют новые сообщения с нужными значениями. Тут у нас один актор просто в своем отдельном потоке, потокобезопасно от всех остальных, увеличивает свой счетчик. Другой актор просто пишет в консоль. Однако представим что у нас каждый актор например вычисляет текущие координаты, запас здоровья и манны одного из десятка игроков которые сейчас играют на нашем сервере. Каждый со своим собственным состоянием в своем собственном потоке. Отправляем другим сообщения или получает от других сообщения о полученном уроне, использованном навыке или смене своих координат.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»