69

GitHub - concurrent-php/ext-async: Concurrent Task Extension

 6 years ago
source link: https://github.com/concurrent-php/ext-async
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

README.md

Async Extension for PHP

Build Status Coverage Status

Provides concurrent Zend VM executions using native C fibers in PHP.

Async API

The async extension exposes a public API that can be used to create, run and interact with fiber-based async executions. You can obtain the API stub files for code completion in your IDE by installing concurrent-php/async-api via Composer.

Awaitable

This interface cannot be implemented directly by userland classes, implementations are provided by Deferred and Task.

namespace Concurrent;

interface Awaitable { }

Deferred

A deferred is a placeholder for an async operation that can be succeeded or failed from userland. It can be used to implement combinator function that operate on multiple Awaitable and expose a single Awaitable as result. The value returned from awaitable() is meant to be consumed by other tasks (or deferreds). The Deferred object itself must be kept private to the async operation because it can eighter succeed or fail the awaitable.

namespace Concurrent;

final class Deferred
{
    public function awaitable(): Awaitable { }

    public function resolve($val = null): void { }

    public function fail(\Throwable $e): void { }

    public static function value($val = null): Awaitable { }

    public static function error(\Throwable $e): Awaitable { }

    public static function combine(array $awaitables, callable $continuation): Awaitable { }

    public static function transform(Awaitable $awaitable, callable $transform): Awaitable { }
}

Task

A task is a fiber-based object that executes a PHP function or method on a separate call stack. Tasks are created using Task::async() or TaskScheduler::run() (and there contextual counterparts). All tasks are associated with a task scheduler as they are created, there is no way to migrate tasks between different schedulers.

Calling Task::await() will suspend the current task and await resolution of the given Awaitable. If the awaited object is another Task it has to be run on the same scheduler, otherwise await() will throw an error.

namespace Concurrent;

final class Task implements Awaitable
{   
    public static function isRunning(): bool { }

    /* Should be replaced with async keyword if merged into PHP core. */
    public static function async(callable $callback, ...$args): Task { }

    /* Should be replaced with extended async keyword expression if merged into PHP core. */
    public static function asyncWithContext(Context $context, callable $callback, ...$args): Task { }

    /* Should be replaced with await keyword if merged into PHP core. */
    public static function await(Awaitable $awaitable): mixed { }
}

TaskScheduler

The task scheduler manages a queue of ready-to-run tasks and a (shared) event loop that provides support for timers and async IO. It will also keep track of suspended tasks to allow for proper cleanup on shutdown. There is an implicit default scheduler that will be used when Task::async() or Task::asyncWithContext() is used in PHP code that is not run using one of the public scheduler methods. It is neighter necessary (nor advisable) to create a task scheduler instance yourself. The only exception to that rule are unit tests, each test should use a dedicated task scheduler to ensure proper test isolation.

You can use run() or runWithContext() to have the given callback be executed as root task within an isolated task scheduler. The run methods will return the value returned from your task callback or throw an error if your task callback throws. The scheduler will allways run all scheduled tasks to completion, even if the callback task you passed is completed before other tasks. The optional inspection callback will be called as soon as the root task (= the callback) is completed and receive an array containing information about all tasks that have not been completed yet.

namespace Concurrent;

final class TaskScheduler
{
    public static function run(callable $callback, ?callable $inspect = null): mixed { }

    public static function runWithContext(Context $context, callable $callback, ?callable $inspect = null): mixed { }
}

Context

Each async operation is associated with a Context object that provides a logical execution context. The context can be used to carry execution-specific variables and (in a later revision) cancellation signals across API boundaries. The context is immutable, a new context must be derived whenever anything needs to be changed for the current execution. You can pass a Context to Task::asyncWithContext() or TaskScheduler::runWithContext() that will become the current context for the new task. It is also possible to enable a context for the duration of a callback execution using the run() method. Every call to Task::await() will backup the current context and restore it when the task is resumed.

namespace Concurrent;

final class Context
{
    public function with(ContextVar $var, $value): Context { }

    public function run(callable $callback, ...$args): mixed { }

    public static function current(): Context { }

    public static function background(): Context { }
}

ContextVar

You can access contextual data using a ContextVar object. Calling get() will lookup the variable's value from the context (passed as argument, current context by default). You have to use Context::with() to derive a new Context that has a value bound to the variable.

namespace Concurrent;

final class ContextVar
{
    public function get(?Context $context = null) { }
}

Timer

The Timer class is used to schedule timers with the integrated event loop. Timers do not make use of callbacks, instead they will suspend the current task during awaitTimeout() and continue when the next timeout is exceeded. The first call to awaitTimeout() will start the timer. If additional tasks await an active the timer they will share the same timeout (which could be less than the value passed to the constructor). A Timer can be closed by calling close() which will fail all pending timeout subscriptions and prevent any further operations.

namespace Concurrent;

final class Timer
{
    public function __construct(int $milliseconds) { }

    public function close(?\Throwable $e = null): void { }

    public function awaitTimeout(): void { }
}

StreamWatcher

A StreamWatcher observes a PHP stream or socket for readability or writability. Only a single stream watcher is allowed for any PHP resource. The watcher should be closed when it is no longer needed to free internal resources. The StreamWatcher will suspend the current task during awaitReadable() and awaitWritable() and continue once the watched stream becomes readable or is closed by the remote peer. A StreamWatcher can be closed by calling close() which will fail all pending read & write subscriptions and prevent any further operations.

namespace Concurrent;

final class Watcher
{
    public function __construct($resource) { }

    public function close(?\Throwable $e = null): void { }

    public function awaitReadable(): void { }

    public function awaitWritable(): void { }
}

SignalWatcher

A SignalWatcher observes UNIX signals (limited support on Windows). The watcher should be closed when it is no longer needed to free internal resources. The current task will be suspended during calls to awaitSignal() and continue once the signal has been received. You can use isSupported() to check if the passed signal can be observed. Windows systems only support SIGHUP (console window closed) and SIGINT (CTRL + C) handling.

namespace Concurrent;

final class SignalWatcher
{
    public const SIGHUP;
    public const SIGINT;
    public const SIGQUIT;
    public const SIGKILL;
    public const SIGTERM;
    public const SIGUSR1;
    public const SIGUSR2;

    public function __construct(int $signum) { }

    public function close(?\Throwable $e = null): void { }

    public function awaitSignal(): void { }

    public static function isSupported(int $signum): bool { }
}

Fiber

A lower-level API for concurrent callback execution is available through the Fiber API. The underlying stack-switching is the same as in the Task implementation but fibers do not come with a scheduler or a higher level abstraction of continuations. A fiber must be started and resumed by the caller in PHP userland. Calling Fiber::yield() will suspend the fiber and return the yielded value to start(), resume() or throw(). The status() method is needed to check if the fiber has been run to completion yet.

namespace Concurrent;

final class Fiber
{
    public const STATUS_INIT;
    public const STATUS_SUSPENDED;
    public const STATUS_RUNNING;
    public const STATUS_FINISHED;
    public const STATUS_FAILED;

    public function __construct(callable $callback, ?int $stack_size = null) { }

    public function status(): int { }

    public function start(...$args): mixed { }

    public function resume($val = null): mixed { }

    public function throw(\Throwable $e): mixed { }

    public static function isRunning(): bool { }

    public static function backend(): string { }

    public static function yield($val = null): mixed { }
}

Functions

An async version of gethostbyname() is provided to allow non-blocking DNS name resolution. This requires PHP to be run via cli or phpdbg SAPI, any other API will fallback to synchronous resolution for now (the function can still be used in these cases, but it will block until the name is resolved).

namespace Concurrent;

function gethostbyname(string $host): string { }

Async / Await Keyword Transformation

The extension provides Task::async() and Task::await() static methods that are implemented in a way that allows for a very simple transformation to the keywords async and await which could be introduced into PHP some time in the future.

$task = async $this->sendRequest($request, $timeout);
$response = await $task;

// The above code would be equivalent to the following:
$task = Task::async(Closure::fromCallable($this, 'sendRequest'), $request, $timeout);
$response = Task::await($task);

The example shows a possible syntax for a keyword-based async execution model. The async keyword can be prepended to any function or method call to create a Task object instead of executing the call directly. The calling scope should be preserved by this operation, hence being consistent with the way method calls work in PHP (no need to create a closure in userland code). The await keyword is equivalent to calling Task::await() but does not require a function call, it can be implemented as an opcode handler in the Zend VM.

$context = Context::inherit(['foo' => 'bar']);

$task = async $context => doSomething($a, $b);
$result = await $task;

// The above code would be equivalent to the following:
$task = Task::asyncWithContext($context, 'doSomething', [$a, $b]);
$result = Task::await($task);

The second example shows how passing a new context to a task would also be possible using the async keyword. This would allow for a very simple and readable way to setup tasks in a specific context using a keyword-based syntax.

PHP with support for async & await keywords

You can install a patched version of PHP that provides native support for async and await as described in the transformation section. To get up and running with it you can execute this in your shell:

mkdir php-src
curl -LSs https://github.com/concurrent-php/php-src/archive/async.tar.gz | sudo tar -xz -C "php-src" --strip-components 1

pushd php-src
./buildconf --force
./configure --prefix=/usr/local/php/cli --with-config-file-path=/usr/local/php/cli --without-pear
make -j4
make install
popd

mkdir ext-async
curl -LSs https://github.com/concurrent-php/ext-async/archive/master.tar.gz | sudo tar -xz -C "ext-async" --strip-components 1

pushd ext-async
phpize
./configure
make install
popd

This will install a modified version of PHP's master branch that has full support for async and await. It will also install the async extension that is required for the actual async execution model.

Source Transformation Examples

The source transformation needs to consider namespaces and preserve scope. Dealing with namespaces is a problem when it comes to function calls because there is a fallback to the global namespace involved and there is no way to determine the called function in all cases during compilation. Here are some examples of code using async / await syntax and the transformed source code:

$task = async max(1, 2, 3);
$result = await $task;

$task = \Concurrent\Task::async('max', 1, 2, 3);
$result = \Concurrent\Task::await($task);

Function calls in global namespace only have to check imported functions, the correct function can be determined at compile time.

namespace Foo;

$task = async bar(1, 2);

$task = \Concurrent\Task::async(\function_exists('Foo\\bar') ? 'Foo\\bar' : 'bar', 1, 2);

Unqualified function calls in namespace require runtime evaluation of the function to be called (unless the function is imported via use statement).

namespace Foo;

$context = \Concurrent\Context::inherit(['num' => 321]);
$work = function (int $a): int { return $a + Context::var('num'); };

$result = await async $context => $work(42);

$result = \Concurrent\Task::await(\Concurrent\Task::asyncWithContext(\Closure::fromCallable($work), 42));

Calling functions stored in variables requires to keep track of the calling scope because $work might contain a method call (or an object with __invoke() method) with a visibility other than public.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK