diff --git a/.gitignore b/.gitignore
index 53cec81..cd61175 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
/bin/
/vendor/
-composer.lock
/.idea/
/.phpstorm/
-/.vscode/
\ No newline at end of file
+/.vscode/
diff --git a/README.md b/README.md
index f8490a0..ee59b06 100644
--- a/README.md
+++ b/README.md
@@ -1,48 +1,66 @@
-# joopschilder/php-async
-## Introduction
-This package provides functions to run callables asynchronously in PHP. Return values are shared via System-V shared memory.
-To use this package, you'll need PHP >= 7.0.0 with `ext-sysvshm` and `ext-pcntl`.
-You should consider the state of this package to be experimental.
-Note: This package should not be used in a CGI environment.
-The key that is used to access the block of shared memory is created based on the inode information of one of the source files.
-This means that, whenever multiple instances (processes) from the same project source are created, they will try to use the same block of memory and collisions will occur.
-I might swap the `ftok()` call for a random string generator somewhere down the road.
-Note: It is possible (but discouraged) to change the amount of available shared memory.
-If you wish to do so, it's as simple as calling either `Runtime::_setSharedMemorySizeMB();` or `Runtime::_setSharedMemorySizeB();`.
-If you want to use 32MB for example, call `Runtime::_setSharedMemorySizeMB(32);`.
-Be sure to make this call before using any of the asynchronous functionalities.
+# `php-async`
+
## Installation
-This package is available on Packagist
-and can be installed using Composer:
+
+This package is available on [Packagist](https://packagist.org/packages/joopschilder/php-async) and can be installed using [Composer](https://getcomposer.org/):
+
```bash
$ composer require joopschilder/async-php
```
+
It's also possible to manually add it to your `composer.json`:
+
```json
{
"require": {
- "joopschilder/php-async": "dev-master"
+ "joopschilder/php-async": "~1.0"
}
}
```
+## What is this?
+
+This package provides functions to run callables asynchronously in PHP. Return values are shared via System-V shared memory.
+To use this package, you need PHP >= 7 with `ext-sysvshm` and `ext-pcntl`.
+
+You should consider the state of this package to be __highly experimental__.
+
+___Note:__ This package should not be used in a CGI environment._
+The key that is used to access the block of shared memory is created based on the inode information of one of the source files.
+This means that, whenever multiple instances (processes) from the same project source are created, they will try to use the same block of memory and collisions will occur.
+I might swap the `ftok()` call for a multi-instance supporting mechanism later on (feel free to do so yourself).
+
+___Note:__ It is possible (but discouraged) to change the amount of available shared memory._
+If you wish to do so, it's as simple as calling either `Runtime::setSharedMemorySizeMB()` or `Runtime::setSharedMemorySizeB()`.
+If you want to use 32MB for example, call `Runtime::setSharedMemorySizeMB(32)`.
+Be sure to make this call before using any of the asynchronous functionalities.
+
+## What is this not?
+
+This is, as you probably guessed by now, not intended for use in a production environment.
+I'm not saying you _can't_, I'm just saying you _shouldn't_.
+
+The code is _not_ unit tested. It has been documented throughout though, so feel free to take a look.
+
## Usage
-#### Functions
-The library exposes three functions in the global namespace that provide indirect access to the class `Asynchronous`:
-* `async(callable $function, ...$parameters)` to run something asynchronously, giving back a `Promise`;
+
+### Functions
+
+The library exposes three functions in the global namespace that provide indirect access to the `Asynchronous` class:
+
+* `async(callable $function, ...$parameters)` to run something asynchronously, returning a `Promise`;
* `async_wait_all()` to wait for all currently running jobs to finish;
-* `async_reap_zombies()` to clean up any zombie processes during runtime if any exist;
+* `async_cleanup()` to clean up any zombie processes during runtime if any exist;
-#### Promises
-Whenever you call `async(...)`, a `Promise` instance is returned.
-A `Promise` is considered to be resolved when the function it belongs to returned a value or finished execution.
-To block execution until a promise is resolved, simply call the `resolve()` method on the promise.
-It's possible to check whether the promise has been resolved in a non-blocking way by calling the `isResolved()` method.
+### A `Promise`, you say?
+
+`async(...)` returns an instance of `JoopSchilder\Asynchronous\Promise`.
+A `Promise` is considered to be resolved when the function it belongs to returned a value or finished execution.
+To block execution until a promise is resolved, simply call the `resolve()` method on the promise.
+It's possible to check whether the promise has been resolved in a non-blocking way by calling the `isResolved()` method.
You can actually return anything that is serializable in PHP: objects, arrays, strings, you name it.
-```php
-resolve();
$pid = $promise->getValue();
```
-The shutdown handler and destructors should take care of the cleanup.
-#### Asynchronous curl requests
-... though you should probably look into curl multi handles for this: curl_multi_init().
+The shutdown handler and destructors should take care of the cleanup.
+
+### Asynchronous curl requests
+
+The only reason `curl` is used here is to provide an intuitive example.
+If you really wanted to perform concurrent http requests you should look into either [`curl_multi_init`](http://php.net/manual/en/function.curl-multi-init.php) or just use [Guzzle](http://docs.guzzlephp.org/en/stable/).
```php
-
-To track what's happening in real time, I like to use:
+
+If you're using a UNIX system, you can make use of the tools `ipcs` and `ipcrm` to monitor and manage System V shared memory blocks.
+To see what's happening, you can use:
+
```bash
$ watch -n 1 "ipcs -m --human && ipcs -m -p && ipcs -m -t && ipcs -m -u"
```
-
To clean all 'unused' shared memory blocks (they might remain resident in RAM if your program terminated unexpectedly):
+
+To clean all 'unused' shared memory blocks (they might stay in RAM if your program terminated unexpectedly), run
+
```bash
$ ipcrm -a
```
## What's next?
-- Improving stability
-- Add an explaining diagram
\ No newline at end of file
+Who really knows?
diff --git a/composer.json b/composer.json
index 7e23657..417db17 100644
--- a/composer.json
+++ b/composer.json
@@ -1,19 +1,26 @@
{
- "name": "joopschilder/php-async",
- "authors": [
- {
- "name": "Joop Schilder",
- "email": "j.n.m.schilder@st.hanze.nl"
- }
- ],
- "autoload": {
- "files": ["src/functions.php"],
- "psr-4": {
- "JoopSchilder\\Asynchronous\\": ["src"]
- }
- },
- "require": {
- "ext-pcntl": "*",
- "ext-sysvshm": "*"
+ "name": "joopschilder/php-async",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Joop Schilder",
+ "email": "jnmschilder@protonmail.com",
+ "homepage": "https://joopschilder.nl/",
+ "role": "Developer"
}
+ ],
+ "autoload": {
+ "files": [
+ "lib/functions.php"
+ ],
+ "psr-4": {
+ "JoopSchilder\\Asynchronous\\": [
+ "src"
+ ]
+ }
+ },
+ "require": {
+ "ext-pcntl": "*",
+ "ext-sysvshm": "*"
+ }
}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..09f590b
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,20 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "b0b3e06a09fa46405754b75a4beba98c",
+ "packages": [],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "ext-pcntl": "*",
+ "ext-sysvshm": "*"
+ },
+ "platform-dev": []
+}
diff --git a/src/functions.php b/lib/functions.php
similarity index 82%
rename from src/functions.php
rename to lib/functions.php
index e4ed5ba..915d766 100644
--- a/src/functions.php
+++ b/lib/functions.php
@@ -16,14 +16,14 @@ if (!function_exists('async')) {
}
-if (!function_exists('async_reap_zombies')) {
+if (!function_exists('async_cleanup')) {
/**
*
*/
- function async_reap_zombies()
+ function async_cleanup()
{
- Asynchronous::reap();
+ Asynchronous::cleanup();
}
}
@@ -37,4 +37,4 @@ if (!function_exists('async_wait_all')) {
{
Asynchronous::waitForChildren();
}
-}
\ No newline at end of file
+}
diff --git a/src/Asynchronous.php b/src/Asynchronous.php
index 47400ee..16d5f18 100644
--- a/src/Asynchronous.php
+++ b/src/Asynchronous.php
@@ -2,8 +2,11 @@
namespace JoopSchilder\Asynchronous;
+use Throwable;
+
/**
* Class Asynchronous
+ * @package JoopSchilder\Asynchronous
* Responsible for management of child processes and shared memory.
*/
class Asynchronous
@@ -19,11 +22,38 @@ class Asynchronous
/**
- * @param callable $function
- * @param mixed ...$parameters
- * @return Promise|null;
+ * Asynchronous constructor.
*/
- public static function run(callable $function, ...$parameters)
+ private function __construct()
+ {
+ /*
+ * The reason we do this is for when the shm block
+ * already exists. We attach, remove, detach and reattach
+ * to ensure a clean state.
+ */
+ $this->attachToSharedMemory();
+ }
+
+
+ /**
+ *
+ */
+ public function __destruct()
+ {
+ if (Runtime::isChild()) {
+ return;
+ }
+
+ self::getInstance()->freeSharedMemoryBlock();
+ }
+
+
+ /**
+ * @param callable $function
+ * @param mixed ...$parameters
+ * @return Promise|null
+ */
+ public static function run(callable $function, ...$parameters): ?Promise
{
/*
* Prepare for fork
@@ -31,16 +61,10 @@ class Asynchronous
$instance = self::getInstance();
$promiseKey = Promise::generatePromiseKey();
- /*
- * Fork the parent
- */
$pid = pcntl_fork();
-
- /*
- * The fork failed. Instead of returning a promise, we return null.
- */
- if ($pid == -1)
+ if (-1 === $pid) {
return null;
+ }
/*
* Parent process. We keep track of the PID of the child process
@@ -66,25 +90,25 @@ class Asynchronous
* the Promise to be able to resolve.
*/
Runtime::markAsChild();
- $instance->_attachToShm();
+ $instance->attachToSharedMemory();
try {
$response = call_user_func($function, ...$parameters);
shm_put_var($instance->shm, $promiseKey, $response ?? Promise::RESPONSE_NONE);
exit(0);
-
- } catch (\Throwable $throwable) {
+ } catch (Throwable $throwable) {
shm_put_var($instance->shm, $promiseKey, Promise::RESPONSE_ERROR);
exit(1);
}
}
+
/**
*
*/
- public static function reap()
+ public static function cleanup(): void
{
/*
* Iterate over all child process PIDs and check
@@ -93,58 +117,35 @@ class Asynchronous
$instance = self::getInstance();
foreach ($instance->children as $index => $pid) {
$response = pcntl_waitpid($pid, $status, WNOHANG);
- if ($response === $pid)
+ if ($response === $pid) {
unset($instance->children[$index]);
+ }
}
}
- /**
- *
- */
- public static function waitForChildren()
- {
- self::getInstance()->_awaitChildren();
- }
/**
*
*/
- public static function removeShmBlock()
+ public static function waitForChildren(): void
{
- self::getInstance()->_removeShmBlock();
+ self::getInstance()->awaitChildProcesses();
}
+
/**
- * @return int
+ *
*/
- public static function childCount()
+ public static function removeShmBlock(): void
{
- return count(self::getInstance()->children);
+ self::getInstance()->freeSharedMemoryBlock();
}
-
- /*
- * Private methods below
- */
-
- /**
- * Asynchronous constructor.
- */
- private function __construct()
- {
- /*
- * The reason we do this is for when the shm block
- * already exists. We attach, remove, detach and reattach
- * to ensure a clean state.
- */
- $this->_attachToShm(); // Attach
- }
-
/**
* @return $this
*/
- private function _awaitChildren()
+ private function awaitChildProcesses(): self
{
/*
* Wait for the children to terminate
@@ -157,14 +158,12 @@ class Asynchronous
return $this;
}
+
/**
* @return $this
*/
- private function _removeShmBlock()
+ private function freeSharedMemoryBlock(): self
{
- /*
- * Detach from the shared memory block
- */
if (is_resource($this->shm)) {
shm_remove($this->shm);
shm_detach($this->shm);
@@ -177,7 +176,7 @@ class Asynchronous
/**
* @return $this
*/
- private function _attachToShm()
+ private function attachToSharedMemory(): self
{
$this->shm = shm_attach(Runtime::getSharedMemoryKey(), Runtime::getSharedMemorySize());
@@ -188,14 +187,9 @@ class Asynchronous
/**
* @return Asynchronous
*/
- private static function getInstance()
+ private static function getInstance(): self
{
if (is_null(self::$instance)) {
- /*
- * This is executed once during runtime;
- * when a functionality from this class
- * is used for the first time.
- */
self::$instance = new static();
self::registerHandlers();
}
@@ -207,32 +201,17 @@ class Asynchronous
/**
*
*/
- private static function registerHandlers()
+ private static function registerHandlers(): void
{
$instance = self::getInstance();
-
- /*
- * The shutdown handler
- */
register_shutdown_function(function () use (&$instance) {
- if (Runtime::isChild())
+ if (Runtime::isChild()) {
return;
+ }
- $instance->_awaitChildren();
- $instance->_removeShmBlock();
+ $instance->awaitChildProcesses();
+ $instance->freeSharedMemoryBlock();
});
}
- /**
- *
- */
- public function __destruct()
- {
- if (Runtime::isChild())
- return;
-
- $instance = self::getInstance();
- $instance->_removeShmBlock();
- }
-
-}
\ No newline at end of file
+}
diff --git a/src/Promise.php b/src/Promise.php
index 40378b9..0cd5189 100644
--- a/src/Promise.php
+++ b/src/Promise.php
@@ -2,6 +2,10 @@
namespace JoopSchilder\Asynchronous;
+/**
+ * Class Promise
+ * @package JoopSchilder\Asynchronous
+ */
class Promise
{
/*
@@ -43,8 +47,9 @@ class Promise
* shm keys).
*/
self::$generatedKey++;
- if (self::$generatedKey > 99999999)
+ if (self::$generatedKey > 99999999) {
self::$generatedKey = 0;
+ }
return $promiseKey;
}
@@ -65,7 +70,7 @@ class Promise
/**
* @return bool
*/
- public function isResolved()
+ public function isResolved(): bool
{
$this->attempt();
@@ -76,7 +81,7 @@ class Promise
/**
* @return bool
*/
- public function isVoid()
+ public function isVoid(): bool
{
return $this->getValue() === self::RESPONSE_NONE;
}
@@ -85,7 +90,7 @@ class Promise
/**
* @return bool
*/
- public function isError()
+ public function isError(): bool
{
return $this->getValue() === self::RESPONSE_ERROR;
}
@@ -103,14 +108,15 @@ class Promise
/**
* @return $this
*/
- public function resolve()
+ public function resolve(): self
{
/*
* Actually block execution until a value is written to
* the expected memory location of this Promise.
*/
- while (!$this->isResolved())
- usleep(1000);
+ while (!$this->isResolved()) {
+ usleep(50);
+ }
return $this;
}
@@ -128,24 +134,28 @@ class Promise
* garbage collector has noticed that there are no more
* references to this Promise instance.
*/
- if (Runtime::isChild())
+ if (Runtime::isChild()) {
return;
+ }
- if (shm_has_var($this->shm, $this->key))
+ if (shm_has_var($this->shm, $this->key)) {
shm_remove_var($this->shm, $this->key);
+ }
shm_detach($this->shm);
-
}
+
/**
* @return $this
*/
- private function attempt()
+ private function attempt(): self
{
- if (shm_has_var($this->shm, $this->key))
+ if (shm_has_var($this->shm, $this->key)) {
$this->value = shm_get_var($this->shm, $this->key);
+ }
return $this;
}
-}
\ No newline at end of file
+
+}
diff --git a/src/Runtime.php b/src/Runtime.php
index a43cb84..2b195d7 100644
--- a/src/Runtime.php
+++ b/src/Runtime.php
@@ -1,6 +1,5 @@