Wiki
Clone wikiAsync / 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 <?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:
- The task reaches successful completion, and optionally produces a result
- The task encounters an error and fails
- The task is cancelled by calling
cancel()
In Async, any object implementing the \Async\Task\Task
interface can be used
as a task:
#!php <?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 yieldsTask
s. 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:
- If given a task, it returns that task.
- If given a promise, it returns a
PromiseTask
for that promise. - If given a generator, it returns a
GeneratorTask
for that generator. - If given a callable object, it returns a
CallableTask
for the callable. - 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 aDelayedTask
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 aThrottledTask
that throttles the execution of thetick
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:
#!json { "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 <?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 <?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