An asynchronous coroutine kernel for PHP 7.
composer require recoil/recoil
The Recoil project comprises the following packages:
- recoil/api- The public Recoil API for application and library developers.
- recoil/dev- Development and debugging tools.
- recoil/recoil(this package) - A reference implementation of the kernel described in the API.
- recoil/react- A kernel implementation based on theReactPHPevent loop.
- recoil/kernel- Common components used to implement the kernels.
Recoil aims to ease development of asynchronous applications by presenting asynchronous control flow in a familiar "imperative" syntax.
What does that mean?Let's jump right in with an example that resolves multiple domain namesconcurrently.
useRecoil\React\ReactKernel;
useRecoil\Recoil;
functionresolveDomainName(string$name,React\Dns\Resolver\Resolver$resolver)
{
try{
$ip= yield$resolver->resolve($name);
echo'Resolved "'.$name.'"to'.$ip.PHP_EOL;
}catch(Exception$e) {
echo'Failed to resolve "'.$name.'"-'.$e->getMessage().PHP_EOL;
}
}
ReactKernel::start(function() {
// Create a React DNS resolver...
$resolver= (newReact\Dns\Resolver\Factory)->create(
'8.8.8.8',
yieldRecoil::eventLoop()
);
// Concurrently resolve three domain names...
yield [
resolveDomainName('recoil.io',$resolver),
resolveDomainName('php.net',$resolver),
resolveDomainName('probably-wont-resolve',$resolver),
];
});
This code resolves three domain names to their IP address and prints the results to the terminal. You can try the example yourself by running the following command in the root of the repository:
./examples/dns
Run it a few times. You'll notice that the output is not always in the same order. This is because the requests are made concurrently and the results are shown as soon as they are received from the DNS server.
Note that there isno callback-passing,and that regular PHPexceptions are used for reporting errors.This is what we mean by "familiar imperative syntax".
Clear as mud?Read on:)
Coroutinesare essentially functions that can be suspended and resumed while maintaining their state. This is useful in asynchronous applications, as the coroutine can suspend while waiting for some task to complete or information to arrive, and the CPU is free to perform other tasks.
PHP generators provide the language-level support for functions that can suspend and resume, and Recoil provides the glue that lets us use these features to perform asynchronous operations.
A Recoil application is started by executing an "entry-point" generator, a little like themain()
function in the C
programming language. The Recoil kernel inspects the values yielded by the generator and identifies an operation to
perform. For example, yielding afloat
with value30
causes the coroutine to suspend execution for 30 seconds.
The DNS example above shows a rather more advanced usage, including concurrent execution and integration with
asynchronous code that is not part of Recoil. The resulting code, however, is quite normal looking, except for the
yield
statements!
Within Recoil, the termcoroutinespecifically refers to a PHP generator that is being executed by the Recoil kernel. It's no mistake that generators can be used in this way.Nikita Popov(who is responsible for the original generator implementation in PHP) published anexcellent article explaining generator-based coroutines. The article even includes an example implementation of a coroutine scheduler, though it takes a somewhat different approach.
AStrandis Recoil's equivalent to your operating system's threads. Each strand has its own call-stack and may be suspended, resumed, joined and terminated without affecting other strands. The elements on the call-stack are not regular functions, but are instead coroutines.
Unlike threads, execution of a strand can only suspend or resume when a coroutine specifically requests to do so, hence the termcooperative multitasking.
Strands are very light-weight and are sometimes known asgreen threads,or (perhaps less correctly) asfibers.
Recoil's concept of the strand is defined by theStrandinterface.
AnDispatchable Valueis any value that Recoil recognises when yielded by a coroutine. For example, yielding another generator pushes that generator onto the current strand's call-stack and invokes it, thus making it a coroutine.
TheRecoil facadeclass describes the complete list of supported values.
Thekernelis responsible for creating and scheduling strands, much like the operating system kernel does for threads.
The kernel and strands are manipulated using thekernel API,which is a set of standard operations defined in the Recoil APIand accessible using theRecoil facade.
There are multiple kernel implementations available. This repository contains a stand-alone implementation based on
stream_select()
.Therecoil/react
packageprovides a kernel based on theReactPHPevent-loop.
The following examples illustrate the basic usage of coroutines and the kernel API. Additional examples are available in theexamples folder.
References toRecoil
andReactKernel
refer to theRecoil facade,
and theReact kernel implementation,
respectively.
The following example shows the simplest way to execute a generator as a coroutine.
ReactKernel::start(
function() {
echo'Hello, world!'.PHP_EOL;
yield;
}
);
ReactKernel::start()
is a convenience method that instantiates the React-based kernel and executes the given coroutine
in a new strand. Yieldingnull
(viayield
with no explicit value) allows PHP to parse the function as a generator,
and allows the kernel to process other strands, though there are none in this example.
A coroutine can be invoked by simply yielding it, as described in the section on coroutines above. You can also use the
yield from
syntax, which may perform better but only works with generators, whereasyield
works with any dispatchable
value.
functionhello()
{
echo'Hello,';
yield;
}
functionworld()
{
echo'world!'.PHP_EOL;
yield;
}
ReactKernel::start(function() {
yieldhello();
yieldworld();
});
To return a value from a coroutine, simply use thereturn
keyword as you would in a normal function.
functionmultiply($a,$b)
{
yield;// force PHP to parse this function as a generator
return$a*$b;
echo'This code is never reached.';
}
ReactKernel::start(function() {
$result= yieldmultiply(2,3);
echo'2 * 3 is'.$result.PHP_EOL;
});
One of the major syntactic advantages of coroutines over callbacks is that errors can be reported using familiar
exception handling techniques. Thethrow
keyword can be used in in a coroutine just as it can in a regular function.
functionmultiply($a,$b)
{
if(!is_numeric($a) ||!is_numeric($b)) {
thrownewInvalidArgumentException();
}
yield;// force PHP to parse this function as a generator
return$a*$b;
}
ReactKernel::start(function() {
try{
yieldmultiply(1,'foo');
}catch(InvalidArgumentException$e) {
echo'Invalid argument!';
}
});