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 @@