Wiki

Clone wiki

Async / Home

Async

Async is an experimental task library for PHP.

It allows developers to use the new yield keyword in PHP 5.5 to asynchronously wait for the result of a task (similar to the await keyword in C#), allowing for asynchronous code that reads very much like the equivalent synchronous code:

<?php

// Wait for $task to complete and receive its result, but without blocking execution
$taskResult = ( yield $task );

// Use $taskResult...

This is of particular benefit when combined with a non-blocking I/O library like React.

Concepts

The main concept in Async is that of a task. A task is an object that represents some work to be done, potentially with a result at the end of it. These tasks are registered with a scheduler that is responsible for running them.

Due to the single-threaded nature of PHP (without extensions anyway), we cannot think of a task as doing a single long-running calculation - this will block the single thread until the task is finished. Instead, tasks must perform work in small chunks ('ticks') where possible, passing control back to the scheduler at appropriate points. This is known as cooperative multi-tasking (so called because the tasks must cooperate by yielding control voluntarily).

The scheduler is responsible for 'ticking' the scheduled tasks, with each scheduled task being repeatedly 'ticked' until it is complete. It is up to the scheduler implementation how to do this in a way that allows all scheduled tasks to run. The scheduler exits when all scheduled tasks are complete (or when it is manually stopped).

It is part of the scheduler interface (see \Async\Scheduler\Scheduler) to accept a delay (time until the task is started) and a minimum tick interval (minimum time between consecutive ticks of a task) when scheduling a task. However, not all scheduler implementations support timers. If timers are not supported by an implementation, an exception will be thrown if an attempt is made to schedule a task with a delay or tick interval. Of the built-in scheduler implementations, the BasicScheduler does not support timers, and the ReactEventLoopScheduler supports timers by utilising the React event loop.

A task can become complete in one of three ways:

  1. The task reaches successful completion, and optionally produces a result
  2. The task encounters an error and fails
  3. The task is cancelled by calling cancel()

In Async, any object implementing the \Async\Task\Task interface can be used as a task:

<?php

namespace Async\Task;

use Async\Scheduler\Scheduler;


interface Task {
    /**
     * Indicates if the task has completed (either by running successfully,
     * failure or cancellation)
     * 
     * @return boolean
     */
    public function isComplete();

    /**
     * Indicates if the task completed successful
     * 
     * @return boolean
     */
    public function isSuccessful();

    /**
     * Indicates if there was an error running the task
     * 
     * @return boolean
     */
    public function isFaulted();

    /**
     * Indicates if the task was cancelled
     * 
     * @return boolean
     */
    public function isCancelled();

    /**
     * Gets the result of running the task, if there is one, or null otherwise
     * 
     * If the task is not complete, null is returned
     * 
     * @return mixed
     */
    public function getResult();

    /**
     * Gets the exception that caused the error, if there is one, or null otherwise
     * 
     * If the task is not complete, null is returned
     * 
     * @return \Exception
     */
    public function getException();

    /**
     * Perform one 'tick' of the task
     * 
     * @param \Async\Scheduler\Scheduler $scheduler
     */
    public function tick(Scheduler $scheduler);

    /**
     * Cancels the running of the task
     */
    public function cancel();
}

There are several built-in tasks - please see the source for more detail:

\Async\Task\CallableTask
Task that takes a callable object. When the task is 'ticked', the callable object is executed and its return value becomes the task result. This task is complete after a single tick.
\Async\Task\RecurringTask
Task that takes a callable object and an optional number of times to execute the callable object. Each time the task is 'ticked', the callable object is executed until it has been called enough times (if the number of times is not given, the callable object is executed on each scheduler tick forever, and the task only completes if an error occurs or it is cancelled).
\Async\Task\PromiseTask
Task that takes a React promise. The task waits for the promise to be resolved, at which point the task is complete and its result is the value the promise was resolved with.
\Async\Task\GeneratorTask
This is the task that enables the yield syntax introduced above. Takes a Generator that yields Tasks. Each time the generator task is 'ticked', it checks to see if the currently yielded task is complete. If it is, then it resumes the generator, passing the result of the task back in. The generator task itself is complete when the generator yields no more tasks.
\Async\Task\SomeTask
Task that takes an array of tasks and a number of tasks required. It is considered to have completed successfully when the required number of tasks have completed successfully, and to have failed when it is not possible for the required number of tasks to complete successfully. If it succeeds, the result is an array of the results of the completed tasks. If it fails, the exception is a compound exception containing the individual exceptions that caused the failures.
\Async\Task\AllTask
Special case of a SomeTask that is considered to have completed successfully only if all the tasks complete successfully.
\Async\Task\AnyTask
Special case of a SomeTask that is considered to have completed successfully once one of the tasks completes successfully. Rather than an array, the result is the result of the first successful task.
\Async\Task\DelayedTask
Takes a task to wrap and a delay in seconds and schedules the wrapped task with the given delay when first ticked. The delayed task completes when the wrapped task completes, and takes its result/failure/cancellation state from the wrapped task.
\Async\Task\ThrottledTask
Takes a task to wrap and an interval in seconds and schedules the wrapped task with the given tick interval when first ticked. The throttled task completes when the wrapped task completes, and takes its result/failure/cancellation state from the wrapped task.

Utility functions

The class \Async\Util provides several static utility functions for easily creating certain kinds of tasks.

\Async\Util::async(mixed $object)

Takes any object and returns a suitable task for that object:

  1. If given a task, it returns that task.
  2. If given a promise, it returns a PromiseTask for that promise.
  3. If given a generator, it returns a GeneratorTask for that generator.
  4. If given a callable object, it returns a CallableTask for the callable.
  5. If given any other object, it returns a task whose result will be the given object.
\Async\Util::some(array $tasks, integer $howMany)

Returns a SomeTask for the given tasks that requires $howMany of those tasks to complete.

\Async\Util::all(array $tasks)

Returns an AllTask for the given tasks that requires all of those tasks to complete.

\Async\Util::any(array $tasks)

Returns an AnyTask for the given tasks that requires one of those tasks to complete.

\Async\Util::delay(mixed $object, float $delay)

Creates a task for the given object using \Async\Util::async and returns a DelayedTask that delays the execution of that task by $delay seconds. $delay can have a fractional part, e.g. for a half second delay, specify $delay = 0.5.

\Async\Util::throttle(mixed $object, float $tickInterval)

Creates a task for the given object using \Async\Util::async and returns a ThrottledTask that throttles the execution of the tick method of that task by $tickInterval seconds. $tickInterval can have a fractional part, e.g. for a half second interval, specify $tickInterval = 0.5.

Installation

Async can be installed via composer:

{
    "require" : {
        "mkjpryor/async" : "0.4"
    }
}

Example

This example prints incrementing integers until the PHP process is halted. It is slightly contrived, and intended to show the mechanisms used in Async.

<?php

use Async\Util;


/*
 * Class for a buffer of values where potential future reads are represented with promises
 */
class Buffer {
    protected $reads, $data;

    public function __construct() {
        $this->reads = new SplQueue();
        $this->data = new SplQueue();
    }

    /**
     * Return a promise that will be fulfilled with a value at some point in the future
     *
     * @return \React\Promise\PromiseInterface
     */
    public function read() {
        if( $this->data->isEmpty() ) {
            $deferred = new \React\Promise\Deferred();
            $this->reads->enqueue($deferred->resolver());
            return $deferred->promise();
        } else {
            return \React\Promise\When::resolve($this->data->dequeue());
        }
    }

    /**
     * Write a string to the buffer that can be used to fulfil a promise
     *
     * @param string $str
     */
    public function write($str) {
        if( $this->reads->isEmpty() ) {
            $this->data->enqueue($str);
        } else {
            $this->reads->dequeue()->resolve($str);
        }
    }
}


/*
 * Generator that prints a value from a buffer and then defers to nested_printer
 */
function printer(Buffer $buffer) {
    while( true ) {
        // Yield a promise task and wait for the result - this is non-blocking
        $value = ( yield Util::async($buffer->read()) );

        echo "Printer: ", $value, PHP_EOL;

        // Yield a generator task for nested_printer and wait for it to complete
        // This is also non-blocking
        // Since a generator task has no result, we don't need to put it into a variable
        yield Util::async(nested_printer($buffer));
    }
}

/*
 * Generator that prints 5 values from a buffer
 */
function nested_printer(Buffer $buffer) {
    for( $i = 0; $i < 5; $i++ ) {
        // Yield a promise task and wait for the result - this is non-blocking
        $value = ( yield Util::async($buffer->read()) );

        echo "Nested printer: ", $value, PHP_EOL;
    }
}


// Create a new buffer
$buffer = new Buffer();

// Create a new scheduler
$scheduler = new \Async\Scheduler\BasicScheduler();

// Schedule a generator task for the printer
$scheduler->schedule(new \Async\Task\GeneratorTask(printer($buffer)));

// Schedule a recurring task that writes incrementing integers to the buffer
$i = 0;
$scheduler->schedule(new \Async\Task\RecurringTask(
    function() use($buffer, &$i) { $buffer->write(++$i); }
));

// Run the scheduler
$scheduler->run();

This will produce the following output until the PHP process is terminated:

Printer: 1
Nested printer: 2
Nested printer: 3
Nested printer: 4
Nested printer: 5
Nested printer: 6
Printer: 7
Nested printer: 8
Nested printer: 9
Nested printer: 10
Nested printer: 11
Nested printer: 12
Printer: 13
Nested printer: 14
Nested printer: 15
Nested printer: 16
Nested printer: 17
Nested printer: 18
...

Integration with the React Event Loop

To allow your application to use non-blocking I/O and timers based on the React event loop, just use the ReactEventLoopScheduler instead of the BasicScheduler:

<?php

$loop = \React\EventLoop\Factory::create();

$scheduler = new \Async\Scheduler\ReactEventLoopScheduler($loop);

// ... Schedule tasks on the scheduler, passing the loop to tasks if required for async I/O etc. ...

$scheduler->run();

Updated