Согласованная мультизадачность с помощью генероторов

by Christopher Pitt

#dpc #Amsterdam

Писатель и кодер, работающий в SilverStripe.

Кристофер - писатель и кодер, работающий в SilverStripe. Обычно он работает над архитектурой приложений, хотя иногда вы найдете его компиляторами или роботами.

  

Поговорим о генераторах в PHP и как они могут сделать код асинхронным

$array = ["foo", "bar", "baz"];
 
foreach ($array as $key => $value) {
    print "item: " . $key . "|" . $value . "\n";
}
 
for ($i = 0; $i < count($array); $i++) {
    print "item: " . $i . "|" . $array[$i] . "\n";
}

Все начинается с массивов

print is_array($array) ? "yes" : "no"; // yes
$document = new DOMDocument();
$document->loadXML("<div></div>");
 
$elements = $document->getElementsByTagName("div");
 
print_r($elements); // DOMNodeList Object ( [length] => 1 )
print ($elements instanceof Traversable) ? "yes" : "no"; // yes

Штуки на-подобии массивов

Классно! 

А можно мне так же?

class MyTraversable implements Traversable
{
    // тут пишем код...
}

PHP Fatal error: Class MyTraversable must implement interface Traversable as part of either Iterator or IteratorAggregate…

Попробуем...

... а так?

class MyIterator implements Iterator
{
    // пишем код тут
class MyIterator implements Iterator
{
    protected $data;
 
    protected $index = 0;
 
    public function __construct(array $data)
    {
        $this->data = $data;
    }
 
    public function current()
    {
        return $this->data[$this->index];
    }
 
    public function next()
    {
        return $this->data[$this->index++];
    }
 
    public function key()
    {
        return $this->index;
    }
 
    public function rewind()
    {
        $this->index = 0;
    }
 
    public function valid()
    {
        return $this->index < count($this->data);
    }
}

Последовательность методов в foreach цикле:

  1. Перед первой итерацией вызывается Iterator::rewind()
  2. Перед каждой итерацией вызывается Iterator::valid(
  3. Если Iterator::valid() вернул false цикл заканчивается
  4. Если Iterator::valid() вернул true, вызывается Iterator::current()
  5. Сразу после Iterator::current() вызывается Iterator::key()
  6. После каждой итерации вызывается Iterator::next() и процесс продолжается с шага 2
$iterator = new MyIterator(["foo", "bar", "baz"]);
 
foreach ($iterator as $key => $value) {
    print "item: " . $key . "|" . $value . "\n";
}

А вот это сработает !

Можно проще

class MyIteratorAggregate implements IteratorAggregate
{
    protected $data;
 
    public function __construct(array $data)
    {
        $this->data = $data;
    }
 
    public function getIterator()
    {
        return new ArrayIterator($this->data);
    }
}

Так что там насчет генераторов ?

$content = file_get_contents(__FILE__);
 
$lines = explode("\n", $content);
 
foreach ($lines as $i => $line) {
    print $i . ". " . $line . "\n";
}

Вернемся к массивам

function lines($file) {
    $handle = fopen($file, "r");
 
    while (!feof($handle)) {
        yield trim(fgets($handle));
    }
 
    fclose($handle);
}
 
foreach (lines(__FILE__) as $i => $line) {
    print $i . ". " . $line . "\n";
}

А теперь сделаем это с помощью генератора

print_r(lines(__FILE__)); // Generator Object ( )

Выглядит как итератор

Что там внутри?

print_r(get_class_methods(lines(__FILE__)));
 
// Array
// (
//     [0] => rewind
//     [1] => valid
//     [2] => current
//     [3] => key
//     [4] => next
//     [5] => send
//     [6] => throw
//     [7] => __wakeup
// )

Метод send

$generator = call_user_func(function() {
    yield "foo";
});
 
print $generator->current() . "\n"; // foo
$generator = call_user_func(function() {
    $input = (yield "foo");
    print "inside: " . $input . "\n";
});
 
print $generator->current() . "\n";
 
$generator->send("bar");

Метод throw

$multiply = function ($x, $y) {
    yield $x * $y;
};
 
print $multiply(5, 6)->current(); // 30
$calculate = function ($op, $x, $y) use ($multiply) {
    if ($op === "multiply") {
        $generator = $multiply($x, $y);
 
        return $generator->current();
    }
};
 
print $calculate("multiply", 5, 6); // 30
$calculate = function ($op, $x, $y) use ($multiply) {
    if ($op === "multiply") {
        $generator = $multiply($x, $y);
 
        if (!is_numeric($x) || !is_numeric($y)) {
            throw new InvalidArgumentException();
        }
 
        return $generator->current();
    }
};
 
print $calculate("multiply", 5, "foo"); // PHP Fatal error...
$multiply = function ($x, $y) {
    try {
        yield $x * $y;
    } catch (InvalidArgumentException $exception) {
        print "ERRORS!";
    }
};
 
$calculate = function ($op, $x, $y) use ($multiply) {
    if ($op === "multiply") {
        $generator = $multiply($x, $y);
 
        if (!is_numeric($x) || !is_numeric($y)) {
            $generator->throw(new InvalidArgumentException());
        }
 
        return $generator->current();
    }
};

Coroutines - это программные компоненты, которые обобщают подпрограммы для неперехваченной многозадачности, позволяя нескольким точкам входа приостанавливать и возобновлять выполнение в определенных местах

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

Создадим планировщик задач

class Task
{
    protected $generator;
 
    public function __construct(Generator $generator)
    {
        $this->generator = $generator;
    }
 
    public function run()
    {
        $this->generator->next();
    }
 
    public function finished()
    {
        return !$this->generator->valid();
    }
}

Нам нужна задача

Ну и сам планировщик

class Scheduler
{
    protected $queue;
 
    public function __construct()
    {
        $this->queue = new SplQueue();
    }
 
    public function enqueue(Task $task)
    {
        $this->queue->enqueue($task);
    }
 
    public function run()
    {
        while (!$this->queue->isEmpty()) {
            $task = $this->queue->dequeue();
            $task->run();
 
            if (!$task->finished()) {
                $this->enqueue($task);
            }
        }
    }
}
$scheduler = new Scheduler();
 
$task1 = new Task(call_user_func(function() {
    for ($i = 0; $i < 3; $i++) {
        print "task 1: " . $i . "\n";
        yield;
    }
}));
 
$task2 = new Task(call_user_func(function() {
    for ($i = 0; $i < 6; $i++) {
        print "task 2: " . $i . "\n";
        yield;
    }
}));
 
$scheduler->enqueue($task1);
$scheduler->enqueue($task2);
 
$scheduler->run();
task 1: 0
task 1: 1
task 2: 0
task 2: 1
task 1: 2
task 2: 2
task 2: 3
task 2: 4
task 2: 5
class Task
{
    protected $generator;
 
    protected $run = false;
 
    public function __construct(Generator $generator)
    {
        $this->generator = $generator;
    }
 
    public function run()
    {
        if ($this->run) {
            $this->generator->next();
        } else {
            $this->generator->current();
        }
 
        $this->run = true;
    }
 
    public function finished()
    {
        return !$this->generator->valid();
    }
}

RecoilPHP

IcicleIO

Привет!

Согласованная мультизадачность с помощью генероторов

By faecie

Согласованная мультизадачность с помощью генероторов

  • 203