1
0
mirror of https://github.com/danog/amp.git synced 2024-11-30 04:29:08 +01:00

Update managing-concurrency to v2, updating the docs for helpers still outstanding, fixes #78

This commit is contained in:
Niklas Keller 2017-03-13 15:49:25 +01:00
parent 85be4e03f4
commit 76c412a802
2 changed files with 48 additions and 99 deletions

View File

@ -7,7 +7,7 @@
`amphp/amp` is a non-blocking concurrency framework for PHP. It provides an event loop, promises and streams as a base for asynchronous programming. `amphp/amp` is a non-blocking concurrency framework for PHP. It provides an event loop, promises and streams as a base for asynchronous programming.
Promises in combination with generators are used to build coroutines, which allow writing asynchronous code just like synchronous code, without any callbacks. Promises in combination with generators are used to build coroutines, which allow writing asynchronous code just like synchronous code, without any callbacks.
## Installation ## Installation
@ -47,4 +47,4 @@ If you discover any security related issues, please email [`bobwei9@hotmail.com`
## License ## License
The MIT License (MIT). Please see [LICENSE](./LICENSE) for more information. The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.

View File

@ -7,42 +7,40 @@ The weak link when managing concurrency is humans; we simply don't think asynchr
## Promises ## Promises
The basic unit of concurrency in an Amp application is the `Amp\Promise`. These objects should be thought of as "placeholders" for values or tasks that aren't yet complete. By using placeholders we're able to reason about the results of concurrent operations as if they were already complete variables. The basic unit of concurrency in Amp applications is the `Amp\Promise`. These objects should be thought of as placeholders for values or tasks that aren't yet complete. By using placeholders we're able to reason about the results of concurrent operations as if they were already complete variables.
> **NOTE** > **NOTE**
> >
> Amp promises do *not* conform to the "Thenables" abstraction common in javascript promise implementations. It is this author's opinion that chaining .then() calls is a suboptimal method for avoiding callback hell in a world with generator coroutines. Instead, Amp utilizes PHP generators to "synchronize" concurrent task execution. > Amp's `Promise` interface **does not** conform to the "Thenables" abstraction common in JavaScript promise implementations. Chaining `.then()` calls is a suboptimal method for avoiding callback hell in a world with generator coroutines. Instead, Amp utilizes PHP generators to "synchronize" concurrent task execution.
>
> However, as ReactPHP is another wide-spread implementation, we also accept any `React\Promise\PromiseInterface` where we accept instances of `Amp\Promise`. In case of custom implementations not implementing `React\Promise\PromiseInterface`, `Amp\adapt` can be used to adapt any object having a `then` or `done` method.
### The Promise API ### The Promise API
```php ```php
interface Promise { interface Promise {
public function when(callable $func, $cbData = null); public function when(callable $onResolve);
public function watch(callable $func, $cbData = null);
} }
``` ```
In its simplest form the `Amp\Promise` aggregates callbacks for dealing with computational results once they eventually resolve. While most code will not interact with this API directly thanks to the magic of [Generators](#generators), let's take a quick look at the two simple API methods exposed on `Amp\Promise` implementations: In its simplest form the `Amp\Promise` aggregates callbacks for dealing with computational results once they eventually resolve. While most code will not interact with this API directly thanks to the magic of [Generators](#generators), let's take a quick look at the one simple API method exposed on `Amp\Promise` implementations:
| Method | Callback Signature | | Method | Callback Signature |
| --------- | ----------------------------------------------------------| | --------- | ----------------------------------------------------------|
| when | `function($error = null, $result = null, $cbData = null)` | | when | `function ($error = null, $result = null)` |
| watch | `function($updateData, $cbData = null)` |
### `when()`
`Amp\Promise::when()` accepts an error-first callback. This callback is responsible for reacting to the eventual result of the computation represented by the promise placeholder. For example: `Amp\Promise::when()` accepts an error-first callback. This callback is responsible for reacting to the eventual result of the computation represented by the promise placeholder. For example:
```php ```php
<?php <?php
$promise = someFunctionThatReturnsAPromise(); $promise = someFunctionThatReturnsAPromise();
$promise->when(function($error = null, $result = null) { $promise->when(function (Throwable $error = null, $result = null) {
if ($error) { if ($error) {
printf( printf(
"Something went wrong:\n%s\n", "Something went wrong:\n%s\n",
$e->getMessage() $error->getMessage()
); );
} else { } else {
printf( printf(
@ -53,87 +51,49 @@ $promise->when(function($error = null, $result = null) {
}); });
``` ```
> **NOTE** Those familiar with JavaScript code generally reflect that the above interface quickly devolves into ["callback hell"](http://callbackhell.com/), and they're correct. We will shortly see how to avoid this problem in the [Generators](#generators) section.
>
> We do not use type declarations here, as PHP 7 introduced the new `Throwable` interface and Amp is PHP 5 compatible.
Those familiar with javascript code generally reflect that the above interface quickly devolves into ["callback hell"](http://callbackhell.com/), and they're correct. We will shortly see how to avoid this problem in the [Generators](#generators) section. ## Deferred
#### Optional Callback Data `Amp\Deferred` is the abstraction responsible for resolving future values once they become available. A library that resolves values asynchronously creates an `Amp\Deferred` and uses it to return an `Amp\Promise` to API consumers. Once the async library determines that the value is ready it resolves the promise held by the API consumer using methods on the linked promisor.
The optional `$cbData` can be used to avoid creating a new closure binding the value and thus avoiding the overhead. It is passed as a parameter to the callback. ### The Deferred API
### `watch()`
`Amp\Promise::watch()` affords promise-producers ([Promisors](#promisors)) the ability to broadcast progress updates while a placeholder value resolves. Whether or not to actually send progress updates is left to individual libraries, but the functionality is available should applications require it. A simple example:
```php ```php
<?php final class Deferred {
$promise = someAsyncFunctionWithProgressUpdates(); public function promise(): Promise;
$promise->watch(function($update) { public function resolve($result = null);
printf(
"Woot, we got an update of some kind:\n%s\n",
print_r($update, true)
);
});
```
#### Optional Callback Data
The optional `$cbData` can be used to avoid creating a new closure binding the value and thus avoiding the overhead. It is passed as a parameter to the callback.
## Promisors
`Amp\Promisor` is the abstraction responsible for resolving future values once they become available. A library that resolves values asynchronously creates an `Amp\Promisor` and uses it to return an `Amp\Promise` to API consumers. Once the async library determines that the value is ready it resolves the promise held by the API consumer using methods on the linked promisor.
### The Promisor API
```php
interface Promisor {
public function promise();
public function update($progress);
public function succeed($result = null);
public function fail($error); public function fail($error);
} }
``` ```
#### `promise()` #### `promise()`
Returns the corresponding `Promise` instance. `Promisor` and `Promise` are separated, so the consumer of the promise can't fulfill it. Returns the corresponding `Promise` instance. `Deferred` and `Promise` are separated, so the consumer of the promise can't fulfill it.
#### `update()` #### `resolve()`
Updates the promise. Invokes all registered `Promise::watch()` callbacks.
#### `succeed()`
Resolves the promise with the first parameter as value, otherwise `null`. If a `Amp\Promise` is passed, the resolution will wait until the passed promise has been resolved. Invokes all registered `Promise::when()` callbacks. Resolves the promise with the first parameter as value, otherwise `null`. If a `Amp\Promise` is passed, the resolution will wait until the passed promise has been resolved. Invokes all registered `Promise::when()` callbacks.
#### `fail()` #### `fail()`
Makes the promise fail. Invokes all registered `Promise::when()` callbacks with the passed `Exception` / `Throwable` as `$error` argument. Makes the promise fail. Invokes all registered `Promise::when()` callbacks with the passed `Throwable` as `$error` argument.
> **NOTE** Here's a simple example of an async value producer `asyncMultiply()` creating a deferred and returning the associated promise to its API consumer.
>
> We do not use type declarations here, as PHP 7 introduced the new `Throwable` interface and Amp is PHP 5 compatible.
### Deferred
`Amp\Deferred` is the standard `Amp\Promisor` implementation.
Here's a simple example of an async value producer `asyncMultiply()` creating a promisor and returning the associated promise to its API consumer. Note that the code below would work exactly the same had we used a `PrivateFuture` as our promisor instead of the `Future` employed below.
```php ```php
<?php // Example async producer using promisor <?php // Example async producer using promisor
use Amp\Loop;
function asyncMultiply($x, $y) { function asyncMultiply($x, $y) {
// Create a new promisor // Create a new promisor
$deferred = new Amp\Deferred; $deferred = new Amp\Deferred;
// Resolve the async result one second from now // Resolve the async result one second from now
Amp\once(function() use ($deferred, $x, $y) { Loop::delay($msDelay = 1000, function () use ($deferred, $x, $y) {
$deferred->succeed($x * $y); $deferred->resolve($x * $y);
}, $msDelay = 1000); });
return $deferred->promise(); return $deferred->promise();
} }
@ -154,16 +114,15 @@ keys from the input array to their resolved values.
The `all()` combinator is extremely powerful because it allows us to concurrently execute many The `all()` combinator is extremely powerful because it allows us to concurrently execute many
asynchronous operations at the same time. Let's look at a simple example using the Amp HTTP client asynchronous operations at the same time. Let's look at a simple example using the Amp HTTP client
([Artax](https://github.com/amphp/artax)) to retrieve multiple HTTP resources concurrently ... ([Artax](https://github.com/amphp/artax)) to retrieve multiple HTTP resources concurrently:
```php ```php
<?php <?php
use function Amp\run; use Amp\Loop;
use function Amp\all; use function Amp\all;
use function Amp\stop;
run(function() { Loop::run(function () {
$httpClient = new Amp\Artax\Client; $httpClient = new Amp\Artax\Client;
$promiseArray = $httpClient->requestMulti([ $promiseArray = $httpClient->requestMulti([
"google" => "http://www.google.com", "google" => "http://www.google.com",
@ -175,7 +134,7 @@ run(function() {
try { try {
// magic combinator sauce to flatten the promise // magic combinator sauce to flatten the promise
// array into a single promise // array into a single promise
$responses = (yield all($promiseArray)); $responses = yield all($promiseArray);
foreach ($responses as $key => $response) { foreach ($responses as $key => $response) {
printf( printf(
@ -186,14 +145,14 @@ run(function() {
$response->getReason() $response->getReason()
); );
} }
} catch (Amp\CombinatorException $e) { } catch (Amp\MultiReasonException $e) {
// If any one of the requests fails the combo // If any one of the requests fails the combo
// promise returned by Amp\all() will fail and // promise returned by Amp\all() will fail and
// be thrown back into our generator here. // be thrown back into our generator here.
echo $e->getMessage(), "\n"; echo $e->getMessage(), "\n";
} }
stop(); Loop::stop();
}); });
``` ```
@ -227,17 +186,19 @@ discarded. Array keys are retained for any results not filtered out by the funct
## Generators ## Generators
The addition of generators in PHP 5.5 trivializes synchronization and error handling in async contexts. The Amp event reactor builds in coroutine support for all reactor callbacks so we can use the `yield` keyword to make async code feel synchronous. Let's look at a simple example executing inside the event reactor run loop: The addition of generators in PHP 5.5 trivializes synchronization and error handling in async contexts. The Amp event loop builds in coroutine support for all event loop callbacks so we can use the `yield` keyword to make async code feel synchronous. Let's look at a simple example executing inside the event loop run loop:
```php ```php
<?php <?php
use Amp\Loop;
function asyncMultiply($x, $y) { function asyncMultiply($x, $y) {
yield new Amp\Pause($millisecondsToPause = 100); yield new Amp\Pause($millisecondsToPause = 100);
return ($x * $y); return $x * $y;
} }
Amp\run(function() { Loop::run(function () {
try { try {
// Yield control until the generator resolves // Yield control until the generator resolves
// and return its eventual result. // and return its eventual result.
@ -259,21 +220,17 @@ are still pending.
### Subgenerators ### Subgenerators
Using PHP 7, you can use `yield from` to delegate a sub task to another generator. That generator will be embedded into the currently running generator. If you're using PHP 5, you can achieve the same using `yield Amp\resolve($generator);`. As of PHP 7 you can use `yield from` to delegate a sub task to another generator. That generator will be embedded into the currently running generator.
### Implicit Yield Behavior ### Yield Behavior
Any value yielded without an associated string yield key is referred to as an "implicit" yield. All implicit yields must be one of the following two types ... All yields must be one of the following three types:
| Yieldable | Description | | Yieldable | Description |
| -----------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | --------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Amp\Promise` | Any promise instance may be yielded and control will be returned to the generator once the promise resolves. If resolution fails the relevant exception is thrown into the generator and must be handled by the application or it will bubble up. If resolution succeeds the promise's resolved value is sent back into the generator. | | `Amp\Promise` | Any promise instance may be yielded and control will be returned to the generator once the promise resolves. If resolution fails the relevant exception is thrown into the generator and must be handled by the application or it will bubble up. If resolution succeeds the promise's resolved value is sent back into the generator. |
| `null` | Gives the event loop time to run other tasks. Continues the generator in the next tick of the loop, just like `Amp\immediately`. | | `React\Promise\PromiseInterface` | Same as `Amp\Promise`. Any React promise will automatically be adapted to an Amp promise. |
| `array` | Yielding an array of promises combines them implicitly using `Amp\all`. An array with elements not being promises will result in an `Amp\InvalidYieldError`. |
> **IMPORTANT**
>
> Any yielded value that is not an `Amp\Promise` or `null` will be treated as an **error** and an appropriate exception will be thrown back into the original yielding generator. This strict behavior differs from older versions of the library in which implicit yield values were simply sent back to the yielding generator function.
## Helpers ## Helpers
@ -293,18 +250,10 @@ Takes a `Promise` as first and timeout in milliseconds as second parameter. Retu
Transforms a `callable` given as first argument into a coroutine function. Transforms a `callable` given as first argument into a coroutine function.
### `resolve()`
Resolves a `Generator` coroutine into a promise. It accepts the `Generator` or a `callable` returning a `Generator` as first and only argument.
Upon resolution the `Generator` return value is used to succeed the promised result. If an error occurs during coroutine resolution the returned promise fails.
A `Generator` coroutine executes the `Generator` until a `Promise` is yielded. It waits for the promise to complete and resumes the `Generator` execution with the resolution value of the yielded promise or throws an exception into the `Generator` in case the yielded promise failed.
### `wait()` ### `wait()`
Block script execution indefinitely until the specified `Promise` resolves. The `Promise` is passed as the first and only argument. Block script execution indefinitely until the specified `Promise` resolves. The `Promise` is passed as the first and only argument.
In the event of promise failure this method will throw the exception responsible for the failure. Otherwise the promise's resolved value is returned. In the event of promise failure this method will throw the exception responsible for the failure. Otherwise the promise's resolved value is returned.
This function should only be used outside of `Amp\run` when mixing synchronous and asynchronous code. This function should only be used outside of `Loop::run` when mixing synchronous and asynchronous code.