Привет Хабр! И так, на четырнадцатый день копья решил я значит начать делать простенький игровой сервер для простой онлайн стрелялки. За одно тему распределенных вычислений затронуть. В этой вводной статье цикла хочу рассказать что такое акторы (в Орлеанс их зернами называют) и принцип их работы. Для этого я пока пример простенького приложения с самодельными акторами без 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();
}
}
- Интерфейс которым мы помечаем сообщения которые получает или отправляет актор:
interface IMessage { }
- Команда с помощью которой мы говорим актору увеличить его счетчик (внутренне состояние):
class IncrementCommand : IMessage { }
- Команда с помощью которой мы говорим актору сказать его текущее состояние (счетчик) другим:
class TellCountCommand : IMessage { }
- Событие которое говорит другим акторам о том что актор сказал всем свое текущее состояние (счетчик). Генерируется при обработке команды TellCountCommand:
class SaidCountEvent : IMessage { public int Count { get; } public int ActorId { get; } public SaidCountEvent(int count, int actorId) { Count = count; ActorId = actorId; } }
Count это сколько сейчас набралось на счетчике у актора с идентификатором равным 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()); } //Говорим акторам сказать всем сколько у них набралось на счетчике. //Так как они отправляют свои события writer актору он выводит их на экран firstInput.Add(new TellCountCommand()); secondInput.Add(new TellCountCommand()); //говорим writer актору вывести сообщение в консоль. writerInput.Add(new WriteMessageCommand("Конец метода Main")); Console.ReadLine(); }
Ну и чтобы совсем было совсем красиво и похоже на грейн (актор) из Orleans давай сделаем обертку на нашим каунтером:
public interface ICounterActor
{
void Increment();
void TellCount();
}
public class CounterActor : ICounterActor
{
private readonly Task _taks;
private readonly BlockingCollection<IMessage> _input;
private readonly BlockingCollection<IMessage> _output;
public CounterActor(BlockingCollection<IMessage> output)
{
_input = new BlockingCollection<IMessage>(); ;
_output = output;
_taks = Task.Run(() =>
{
//State
var count = 0;
while (true)
{
var m = _input.Take();
if (m is IncrementCommand)
count++;
if (m is TellCountCommand)
_output.Add(new SaidCountEvent(count, _taks.Id));
}
});
}
public void Increment()
{
_input.Add(new IncrementCommand());
}
public void TellCount()
{
_input.Add(new TellCountCommand());
}
}
Тут использовалась реактивная модель. Наши акторы не возвращают значения а только генерируют новые сообщения с нужными значениями. Тут у нас один актор просто в своем отдельном потоке, потокобезопасно от всех остальных, увеличивает свой счетчик. Другой актор просто пишет в консоль. Однако представим что у нас каждый актор например вычисляет текущие координаты, запас здоровья и манны одного из десятка игроков которые сейчас играют на нашем сервере. Каждый со своим собственным состоянием в своем собственном потоке. Отправляем другим сообщения или получает от других сообщения о полученном уроне, использованном навыке или смене своих координат.