Лично мне очень нравится концепция акторов. Что интересно, познакомился я с ней куда раньше повальной моды на функциональщину, в году так 2003, когда начал плотно работать с библиотекой ACE (это та, которая The ADAPTIVE Communication Environment). Ну а сейчас акторами никого не удивишь, все про них только и говорят. И это хорошо, так как данная модель сильно упрощает отладку и разработку, при относительно не большой просадке по производительности и памяти.
В последнее время я присматриваюсь у относительно экзотическим языкам программирования, таким как Rust и Scala, а для обоих языков модель акторов является родной. При этом, на данный момент, Rust ничего не может предложить сравнимого с библиотекой AKKA, хотя даже в текущем своем состоянии его представление модели акторов не безынтересно.
В качестве примера я решил взять фрагмент из задачи, которую решал на Scala и написать эту задачу на двух языках, Scala и Rust, настолько схожей, насколько это возможно.
Scala
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Await
import akka.pattern
import scala.concurrent.duration._
import akka.util.Timeout
object App {
case class Request(id: Int) // 1
case class Response(response: String)
case class Start()
class Requester extends Actor { // 2
def receive = {
case Request(id) =>
sender ! Response("Client #" + id.toString) // 3
}
}
class Supervisor extends Actor { // 4
private val num = 100
private val responses = new ArrayBuffer[String](num)
private var requester:ActorRef = null // 5
def receive = {
case Start => {
val randomRouter = context.actorOf( // 6
Props[Requester].withRouter(RandomRouter(12)), "router")
for (idx <- 1 to num) {
randomRouter ! Request(idx) // 7
}
requester = sender // 8
}
case Response(resp) => { // 9
responses += resp
if (responses.size == num) {
requester ! responses // 10
context.stop(self) // 11
context.system.shutdown() // 12
}
}
}
}
def main(args: Array[String]) {
val supervisor = ActorSystem().actorOf(Props(new Supervisor)) // 13
implicit val timeout = Timeout(60 seconds)
val result = pattern.ask(supervisor, Start) // 14
val responses = Await.result(result, 1.minute).asInstanceOf[ArrayBuffer[String]]
println("Aggregated value: " + responses)
}
}
Конечно, это сугубо индивидуально, но мне работа с библиотекой AKKA приносит удовольствие. Я не так давно закончил читать документацию относящуюся к этой библиотеке, и могу точно сказать, ее возможности и архитектурная продуманность удивляют. Наверное, это одна из лучших библиотек реализующих концепцию акторов на данный момент. Так, хватит хвалить, пора переходить к сути
Система акторов в AKKA представляет собой древовидную структуру, поэтому, довольно удобно иметь некий управляющий актор – супервизор
После того, как запрос был отправлен, начинается самое интересное. Дело в том, что я хотел что бы одновременное количество запросов к серверу находилось в неких разумных пределах, например, при необходимостм получить 20К ответов не должно было возникать 20К одновременных запросов. И тут на помощь приходят планировщики AKKA, которые умеют раздавать запросы строго определенному количеству акторов. В моем примере я решил создать планировщик
При работе с акторами довольно важным моментом является грамотное использование переменной
Осталось не так уж и много: получить результаты, собрать их воедино и отдать все разом. Результаты работы акторов
Rust
use std::{task, io, uint};
use std::comm::{SharedChan};
struct TestInfo {
mode: task::SchedMode,
tasks_count: uint,
}
impl TestInfo {
fn new(m: task::SchedMode) -> TestInfo {
TestInfo {
mode: m,
tasks_count: 100u,
}
}
fn process(&self) -> ~[~str] {
let mut result:~[~str] = ~[];
let (server_chan, server_port) = stream(); // 1
let server_port = SharedChan::new(server_port); // 2
for uint::range(0, self.tasks_count) |i| {
let server_port: SharedChan<~str> = server_port.clone();
do task::spawn_sched(self.mode) { // 3
server_port.send(fmt!("Client #%u", i)); // 4
}
}
for self.tasks_count.times { // 5
result.push(server_chan.recv()); // 6
}
result
}
}
fn main() {
let supervisor = TestInfo::new(task::DefaultScheduler); // 7
let res = supervisor.process();
io::println(fmt!("Aggregated value: %?", res));
}
Решение на Rust я старался сделать максимально близким к “эталонной” версии на Scala, но в силу отсутствия развитой библиотеки акторов, поведение получилось похожим, но не идентичным, а более простой и короткий код не более чем следствие отсутствия ряда возможностей имеющихся в AKKA. Только не подумайте, что я критикую Rust, на самом деле, я его очень люблю, просто он еще совсем молодой и зеленый
Хотя в составе библиотек Rust нет ничего, что бы носило имя Актор, де-факто задачи Rust и являются акторами. Задача так же как и акторы в AKKA имеют предков и потомков, задачам можно отправить сообщение и получить от них ответ. Задачи так же выполняются планировщиком, за исключением того, что прямо сейчас с планировщиками все очень печально.
Принципы работы с задачами в Rust отличаются от работы с акторами в AKKA приблизительно на столько, на сколько отличается написание сетевого приложения на C++ с использованием ACE от написания того же приложения с Berkeley sockets на прямую. Для обеспечения взаимодействия с задачами в Rust нам будет необходимо создать поток, состоящий из порта и канала
Запуск задач на выполнение в Rust будет кардинально отличаться от запуска акторов в Scala. Дело в том, что в Scala мы создавали строго заданное количество задач и при помощи планировщика раздавали им задания. Реализовать схожее поведение в Rust не возможно по причине отсутствия такого функционала в библиотеках. Поэтому, для каждого из запросов будет создана
С отправкой и сбором результатов все значительно проще чем в случае с примером на Scala. Каждая из дочерних задач формирует и отправляет
Вот и все, на данный момент. После того, как планировщики Rust будут приведены в порядок, можно будет доработать Rust версию и, возможно, подумать о написании чего-то похожего на AKKA. Время покажет.
Дополнения, исправления, комментарии приветствуются
Что такое context, там где //6 в Scala?
Насколько я понимаю, это переменная типа (трэйт) ActorContext содержащаяся в каждом акторе.
Почему бы не сделать Request вложенным в Requester, а также, возможно, Response?
А Start в Supervisor.
Сделать можно что угодно. Только зачем?
чтобы не засорять глобальное пространство имен.