diff --git a/README.md b/README.md index c91c099..f8490a0 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,67 @@ # joopschilder/php-async -Asynchronous PHP callable processing with return values via SysV shared memory.
-Requires the `php-sysvshm` extension.
-Works with PHP >= 5.3 due to `shm_attach(...)` returning a resource instead of an int.

-Note: This package should not be used in a CGI environment, as each PHP runtime that makes a call on the async functions WILL -create an 8MB shared memory block in RAM. If you run out of memory: you have been warned. -
-Note: This project is an experiment. It is, however, available on packagist. -If you think your project lacks witchcraft and black magic, just add this package to your `composer.json`: +## 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. +## Installation +This package is available on Packagist +and can be installed using Composer:
+```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" } } -``` +``` +## 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`; +* `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; -## Examples - -### Promises +#### 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.
You can actually return anything that is serializable in PHP: objects, arrays, strings, you name it. ```php resolve(); $pid = $promise->getValue(); -printf("Me (%d) and %d have worked very hard!\n", getmypid(), $pid); ``` -The shutdown handler and destructors should take care of the rest. - - - -### Asynchronous curl requests -... though you should probably look into curl multi handles for this: curl_multi_init() on PHP.net. +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(). ```php +To track what's happening in real time, I like to 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):
+```bash +$ ipcrm -a +``` + ## What's next? -- Refactoring -- More functionality (maybe) - Improving stability -- Add a diagram that explains this witchcraft \ No newline at end of file +- Add an explaining diagram \ No newline at end of file diff --git a/composer.json b/composer.json index 030bf42..7e23657 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,6 @@ }, "require": { "ext-pcntl": "*", - "ext-sysvshm": "*", - "ext-posix": "*" + "ext-sysvshm": "*" } } diff --git a/src/Asynchronous.php b/src/Asynchronous.php index ffcad0b..47400ee 100644 --- a/src/Asynchronous.php +++ b/src/Asynchronous.php @@ -8,25 +8,15 @@ namespace JoopSchilder\Asynchronous; */ class Asynchronous { - public const BLOCK_SIZE_MB = 8; - private const BLOCK_SIZE_BYTES = self::BLOCK_SIZE_MB * (1024 ** 2); - /** @var Asynchronous|null */ private static $instance; - /** @var int */ - private static $key = 0; - - /** @var int[] */ private $children = []; /** @var resource */ private $shm; - /** @var int */ - private $shmKey; - /** * @param callable $function @@ -39,7 +29,7 @@ class Asynchronous * Prepare for fork */ $instance = self::getInstance(); - $key = self::generatePromiseKey(); + $promiseKey = Promise::generatePromiseKey(); /* * Fork the parent @@ -61,7 +51,7 @@ class Asynchronous if ($pid > 0) { $instance->children[] = $pid; - return new Promise($key); + return new Promise($promiseKey); } /* @@ -75,17 +65,17 @@ class Asynchronous * On failure, write a default response to the block in order for * the Promise to be able to resolve. */ - Runtime::markChild(); + Runtime::markAsChild(); $instance->_attachToShm(); try { $response = call_user_func($function, ...$parameters); - shm_put_var($instance->shm, $key, $response ?? Promise::RESPONSE_NONE); + shm_put_var($instance->shm, $promiseKey, $response ?? Promise::RESPONSE_NONE); exit(0); } catch (\Throwable $throwable) { - shm_put_var($instance->shm, $key, Promise::RESPONSE_ERROR); + shm_put_var($instance->shm, $promiseKey, Promise::RESPONSE_ERROR); exit(1); } @@ -144,14 +134,11 @@ class Asynchronous private function __construct() { /* - * Use the filename as an identifier to create the - * System V IPC key. + * The reason we do this is for when the shm block + * already exists. We attach, remove, detach and reattach + * to ensure a clean state. */ - if ($this->shmKey == null) - $this->shmKey = ftok(__FILE__, 't'); - - Promise::__setShmKey($this->shmKey); - $this->_attachToShm(); + $this->_attachToShm(); // Attach } /** @@ -192,7 +179,7 @@ class Asynchronous */ private function _attachToShm() { - $this->shm = shm_attach($this->shmKey, self::BLOCK_SIZE_BYTES); + $this->shm = shm_attach(Runtime::getSharedMemoryKey(), Runtime::getSharedMemorySize()); return $this; } @@ -217,29 +204,6 @@ class Asynchronous } - /** - * @return int - */ - private static function generatePromiseKey() - { - /* - * Get the current key. - */ - $promiseKey = self::$key; - - /* - * Reset the key to 0 if the upper bound of - * 9.999.999 is reached (Windows limit for - * shm keys). - */ - self::$key++; - if (self::$key > 99999999) - self::$key = 0; - - return $promiseKey; - } - - /** * */ diff --git a/src/Promise.php b/src/Promise.php index 56125ca..40378b9 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -15,8 +15,7 @@ class Promise public const RESPONSE_ERROR = '__PROMISE_RESPONSE_ERROR__'; /** @var int */ - private static $shmKey; - + private static $generatedKey = 0; /** @var resource */ private $shm; @@ -27,16 +26,27 @@ class Promise /** @var mixed|null */ private $value; + /** - * @param int $shmKey + * @return int */ - public static function __setShmKey(int $shmKey) + public static function generatePromiseKey() { /* - * Should be done only once: when the Asynchronous class - * has created a key that will be used for IPC. + * Get the current key. */ - self::$shmKey = $shmKey; + $promiseKey = self::$generatedKey; + + /* + * Reset the key to 0 if the upper bound of + * 9.999.999 is reached (Windows limit for + * shm keys). + */ + self::$generatedKey++; + if (self::$generatedKey > 99999999) + self::$generatedKey = 0; + + return $promiseKey; } @@ -48,19 +58,9 @@ class Promise { $this->key = $key; $this->value = null; - $this->shm = shm_attach(self::$shmKey); + $this->shm = shm_attach(Runtime::getSharedMemoryKey()); } - /** - * @return $this - */ - private function attempt() - { - if (shm_has_var($this->shm, $this->key)) - $this->value = shm_get_var($this->shm, $this->key); - - return $this; - } /** * @return bool @@ -137,4 +137,15 @@ class Promise shm_detach($this->shm); } + + /** + * @return $this + */ + private function attempt() + { + 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 f1ad26e..a43cb84 100644 --- a/src/Runtime.php +++ b/src/Runtime.php @@ -13,16 +13,54 @@ namespace JoopSchilder\Asynchronous; */ class Runtime { + /** @var int */ + public const INITIAL_SHM_SIZE_MB = 16; + + /** @var int */ + private static $sharedMemKey = null; + + /** @var int */ + private static $sharedMemSize = self::INITIAL_SHM_SIZE_MB * (1024 ** 2); + /** @var bool */ private static $inParentRuntime = true; /** - * + * Runtime constructor. */ - public static function markChild() + private function __construct() { - self::$inParentRuntime = false; + } + + + /* + * Public + */ + + + /** + * @return int + */ + public static function getSharedMemorySize() + { + return self::$sharedMemSize; + } + + + /** + * @return int + */ + public static function getSharedMemoryKey() + { + /* + * Use the filename as an identifier to create the + * System V IPC key. + */ + if (is_null(self::$sharedMemKey)) + self::$sharedMemKey = ftok(__FILE__, 't'); + + return self::$sharedMemKey; } @@ -34,4 +72,36 @@ class Runtime return !self::$inParentRuntime; } + + /* + * 'Semi' private. + * To be used by internal classes. + */ + + + /** + * @param int $size_mb + */ + public static function _setSharedMemorySizeMB(int $size_mb) + { + self::$sharedMemSize = abs($size_mb) * (1024 ** 2); + } + + + /** + * @param int $size_b + */ + public static function _setSharedMemorySizeB(int $size_b) + { + self::$sharedMemSize = $size_b; + } + + + /** + * + */ + public static function markAsChild() + { + self::$inParentRuntime = false; + } } \ No newline at end of file