Implementation of Actor concurrency model with Swoole under PHP

  • 2021-12-12 03:54:17
  • OfStack

What is Actor?

Actor for PHPer, It may be unfamiliar. Students who have written Java will be familiar with it. Java1 is directly wired (although PHP has Pthread, it is not popular). It is a concurrent model without shared memory. The data in each Actor exists independently, and Actor is interactively scheduled in the form of message passing. Actor is a highly abstract programming model, which is very suitable for games and hardware industries.

Swoole Synergy and Mailbox

Thanks to Swoole4.x, we can quickly realize one mailbox mode scheduling based on Swoole and Channel. The simulation code is as follows:


use Swoole\Coroutine\Channel;
go(function (){
  // Create 10 Mailbox channel 
  $mailBoxes = [];
  for ($i = 1;$i <= 10;$i++){
    $mailBoxes[$i] = new Channel(16);
  }
  // Simulation master  Post office dispatching , Random image 1 Deliver messages in a mailbox 
  go(function ()use($mailBoxes){
    while (1){
      \co::sleep(2);
      $key = rand(1,10);
      ($mailBoxes[$key])->push(time());
    }
  });
  // Simulation actor  Entity consumption 
  for ($i = 1;$i <= 10;$i++){
    go(function ()use($mailBoxes,$i){
      while (1){
        $msg = ($mailBoxes[$i])->pop();
        echo "Actor {$i} recv msg : {$msg} \n";
      }
    });
  }
});

The above code executes the output:

php test.php
Actor 8 recv msg : 1559622691
Actor 10 recv msg : 1559622693
Actor 1 recv msg : 1559622695
Actor 5 recv msg : 1559622697

When there is no data in POP, the co-process channel will automatically give up the execution right (see Swoole co-process scheduling for details)

Actor Library

Based on the above principle, we implement a co-process Actor library with multi-process distribution


composer require easyswoole/actor=2.x-dev

We rely on the dev library for testing, and production can rely on the stable version itself

Process relationship

In Actor model of Easyswoole, there are two groups of processes, one is proxy process, which is used to realize Actor external service, and the other is worker process. proxy process and worker process communicate through unixsock, while Actor instances are evenly distributed among worker.

Sample code

For example, in a chat room, we can define a room model.


namespace EasySwoole\Actor\Test;


use EasySwoole\Actor\AbstractActor;
use EasySwoole\Actor\ActorConfig;

class RoomActor extends AbstractActor
{
  public static function configure(ActorConfig $actorConfig)
  {
    $actorConfig->setActorName('Room');
  }
  public function onStart()
  {
    // Whenever 1 A RoomActor This callback is executed when the entity is created 
    var_dump('room actor '.$this->actorId().' start');
  }
  public function onMessage($msg)
  {
    // Whenever 1 A RoomActor When an entity receives an external message, it executes this callback when 
    var_dump('room actor '.$this->actorId().' onmessage: '.$msg);
    return 'reply at '.time();
  }
  public function onExit($arg)
  { 
    // Whenever 1 A RoomActor This callback is executed when the entity exits 
    var_dump('room actor '.$this->actorId().' exit at arg: '.$arg);
    return 'exit at '.time();
  }
  protected function onException(\Throwable $throwable)
  {
    // Whenever 1 A RoomActor This callback is executed when an exception occurs 
    var_dump($throwable->getMessage());
  }
}

Create an Actor service in cli mode


use EasySwoole\Actor\Actor;
use EasySwoole\Actor\Test\RoomActor;
use EasySwoole\Actor\ProxyProcess;

Actor::getInstance()->register(RoomActor::class);
$list = Actor::getInstance()->generateProcess();

foreach ($list['proxy'] as $proxy){
  /** @var ProxyProcess $proxy */
  $proxy->getProcess()->start();
}
foreach ($list['worker'] as $actors){
  foreach ($actors as $actorProcess){
    /** @var ProxyProcess $actorProcess */
    $actorProcess->getProcess()->start();
  }
}
while($ret = \Swoole\Process::wait()) {
  echo "PID={$ret['pid']}\n";
}

Create an cli test script


use EasySwoole\Actor\Actor;
use EasySwoole\Actor\Test\RoomActor;
Actor::getInstance()->register(RoomActor::class);

go(function (){
  $actorId = RoomActor::client()->create('create arg1');
  var_dump($actorId);
  \co::sleep(3);
  var_dump(RoomActor::client()->send($actorId,'this is msg'));
  \co::sleep(3);
  var_dump(RoomActor::client()->exit($actorId,'this is exit arg'));
  \co::sleep(3);
  RoomActor::client()->create('create arg2');
  \co::sleep(3);
  RoomActor::client()->create('create arg3');
  \co::sleep(3);
  var_dump(RoomActor::client()->sendAll('sendAll msg'));
  \co::sleep(3);
  var_dump(RoomActor::client()->status());
  \co::sleep(3);
  var_dump(RoomActor::client()->exitAll('sendAll exit'));
});

The execution results of the above code are as follows:

Server side


php test.php 
string(40) "room actor 00101000000000000000001 start"
string(57) "room actor 00101000000000000000001 onmessage: this is msg"
string(64) "room actor 00101000000000000000001 exit at arg: this is exit arg"
string(40) "room actor 00101000000000000000002 start"
string(40) "room actor 00103000000000000000001 start"
string(57) "room actor 00101000000000000000002 onmessage: sendAll msg"
string(57) "room actor 00103000000000000000001 onmessage: sendAll msg"
string(60) "room actor 00101000000000000000002 exit at arg: sendAll exit"
string(60) "room actor 00103000000000000000001 exit at arg: sendAll exit"

Client


php test2.php 
string(23) "00101000000000000000001"
string(19) "reply at 1559623925"
string(18) "exit at 1559623928"
bool(true)
array(3) {
 [1]=>
 int(1)
 [2]=>
 int(0)
 [3]=>
 int(1)
}
bool(true)

More details can be obtained from the document support of EasySwoole project official website http://easyswoole.com/

If you like the EasySwoole project, you can give an star https://github.com/easy-swoole/easyswoole


Related articles: