├── .valgrindrc
├── src
├── Strand.php
├── Exception
│ ├── Error.php
│ ├── StatusError.php
│ ├── Throwable.php
│ ├── ForkException.php
│ ├── MutexException.php
│ ├── Exception.php
│ ├── ProcessException.php
│ ├── SerializationException.php
│ ├── SynchronizationError.php
│ ├── ThreadException.php
│ ├── SemaphoreException.php
│ ├── LockAlreadyReleasedError.php
│ ├── SharedMemoryException.php
│ ├── ChannelException.php
│ ├── WorkerException.php
│ ├── PanicError.php
│ └── TaskException.php
├── Worker
│ ├── WorkerFactory.php
│ ├── WorkerProcess.php
│ ├── WorkerFork.php
│ ├── WorkerThread.php
│ ├── Task.php
│ ├── DefaultWorkerFactory.php
│ ├── Environment.php
│ ├── Internal
│ │ ├── TaskFailure.php
│ │ ├── TaskRunner.php
│ │ └── PooledWorker.php
│ ├── Worker.php
│ ├── Pool.php
│ ├── functions.php
│ ├── AbstractWorker.php
│ ├── BasicEnvironment.php
│ └── DefaultPool.php
├── Process.php
├── Sync
│ ├── Internal
│ │ ├── ExitStatus.php
│ │ ├── ExitSuccess.php
│ │ └── ExitFailure.php
│ ├── Mutex.php
│ ├── Synchronizable.php
│ ├── Parcel.php
│ ├── Semaphore.php
│ ├── Channel.php
│ ├── Lock.php
│ ├── FileMutex.php
│ ├── ChannelledStream.php
│ ├── PosixSemaphore.php
│ └── SharedMemoryParcel.php
├── Context.php
├── Threading
│ ├── Internal
│ │ ├── Storage.php
│ │ ├── Mutex.php
│ │ ├── Semaphore.php
│ │ └── Thread.php
│ ├── Mutex.php
│ ├── Semaphore.php
│ ├── Parcel.php
│ └── Thread.php
├── Process
│ ├── ChannelledProcess.php
│ └── Process.php
└── Forking
│ └── Fork.php
├── CONTRIBUTING.md
├── Vagrantfile
├── phpdoc.dist.xml
├── examples
├── worker.php
├── BlockingTask.php
├── fork.php
├── thread.php
└── worker-pool.php
├── LICENSE
├── composer.json
├── bin
└── worker.php
├── README.md
└── CHANGELOG.md
/.valgrindrc:
--------------------------------------------------------------------------------
1 | --error-limit=no
2 | --trace-children=yes
3 | --track-fds=yes
4 | --undef-value-errors=no
5 |
--------------------------------------------------------------------------------
/src/Strand.php:
--------------------------------------------------------------------------------
1 | result = $result;
14 | }
15 |
16 | /**
17 | * {@inheritdoc}
18 | */
19 | public function getResult()
20 | {
21 | return $this->result;
22 | }
23 | }
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | Vagrant.configure(2) do |config|
2 | config.vm.box = "rasmus/php7dev"
3 |
4 | config.vm.provision "shell", inline: <<-SHELL
5 | newphp 7 zts
6 |
7 | # Install pthreads from master
8 | git clone https://github.com/krakjoe/pthreads
9 | cd pthreads
10 | git checkout master
11 | phpize
12 | ./configure
13 | make
14 | sudo make install
15 | echo 'extension=pthreads.so' >> `php -i | grep php-cli.ini | awk '{print $5}'`
16 | SHELL
17 | end
18 |
--------------------------------------------------------------------------------
/src/Worker/WorkerFork.php:
--------------------------------------------------------------------------------
1 | run();
17 | }));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Context.php:
--------------------------------------------------------------------------------
1 | run();
17 | }));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Worker/Task.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Icicle
4 |
5 | build/docs
6 | utf8
7 |
8 |
9 | build/docs
10 |
11 |
12 | warn
13 |
14 | build/log/docs/{DATE}.log
15 |
16 |
17 |
18 |
19 |
20 |
21 | src
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/worker.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | create();
14 | $worker->start();
15 |
16 | $result = yield from $worker->enqueue(new BlockingTask('file_get_contents', 'https://google.com'));
17 | printf("Read %d bytes\n", strlen($result));
18 |
19 | $code = yield from $worker->shutdown();
20 | printf("Code: %d\n", $code);
21 | })->done();
22 |
23 | Loop\run();
24 |
--------------------------------------------------------------------------------
/src/Threading/Internal/Storage.php:
--------------------------------------------------------------------------------
1 | value = $value;
20 | }
21 |
22 | /**
23 | * @return mixed
24 | */
25 | public function get()
26 | {
27 | return $this->value;
28 | }
29 |
30 | /**
31 | * @param mixed $value
32 | */
33 | public function set($value)
34 | {
35 | $this->value = $value;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Sync/Mutex.php:
--------------------------------------------------------------------------------
1 | trace = $trace;
22 | }
23 |
24 | /**
25 | * Gets the stack trace at the point the panic occurred.
26 | *
27 | * @return string
28 | */
29 | public function getPanicTrace()
30 | {
31 | return $this->trace;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Worker/DefaultWorkerFactory.php:
--------------------------------------------------------------------------------
1 | function = $function;
26 | $this->args = $args;
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function run(Environment $environment)
33 | {
34 | return ($this->function)(...$this->args);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Sync/Synchronizable.php:
--------------------------------------------------------------------------------
1 | $callback The synchronized callback to invoke.
18 | * The callback may be a regular function or a coroutine.
19 | *
20 | * @return \Generator
21 | *
22 | * @resolve mixed The return value of $callback.
23 | */
24 | public function synchronized(callable $callback): \Generator;
25 | }
26 |
--------------------------------------------------------------------------------
/src/Exception/TaskException.php:
--------------------------------------------------------------------------------
1 | trace = $trace;
22 | }
23 |
24 | /**
25 | * Gets the stack trace at the point the panic occurred.
26 | *
27 | * @return string
28 | */
29 | public function getWorkerTrace(): string
30 | {
31 | return $this->trace;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Worker/Environment.php:
--------------------------------------------------------------------------------
1 | lock ? $this->lock = false : true);
28 | };
29 |
30 | while (!$this->lock || $this->synchronized($tsl)) {
31 | yield from Coroutine\sleep(self::LATENCY_TIMEOUT);
32 | }
33 |
34 | return new Lock(function () {
35 | $this->release();
36 | });
37 | }
38 |
39 | /**
40 | * Releases the lock.
41 | */
42 | protected function release()
43 | {
44 | $this->lock = true;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Threading/Mutex.php:
--------------------------------------------------------------------------------
1 | init();
24 | }
25 |
26 | /**
27 | * Initializes the mutex.
28 | */
29 | private function init()
30 | {
31 | $this->mutex = new Internal\Mutex();
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function acquire(): \Generator
38 | {
39 | return $this->mutex->acquire();
40 | }
41 |
42 | /**
43 | * Makes a copy of the mutex in the unlocked state.
44 | */
45 | public function __clone()
46 | {
47 | $this->init();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Worker/Internal/TaskFailure.php:
--------------------------------------------------------------------------------
1 | type = get_class($exception);
31 | $this->message = $exception->getMessage();
32 | $this->code = $exception->getCode();
33 | $this->trace = $exception->getTraceAsString();
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function getException()
40 | {
41 | return new TaskException(
42 | sprintf('Uncaught exception in worker of type "%s" with message "%s"', $this->type, $this->message),
43 | $this->code,
44 | $this->trace
45 | );
46 | }
47 | }
--------------------------------------------------------------------------------
/examples/fork.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | send('Data sent from child.');
15 |
16 | print "Child sleeping for 2 seconds...\n";
17 | sleep(2);
18 |
19 | return 42;
20 | });
21 |
22 | $timer = Loop\periodic(1, function () use ($context) {
23 | static $i;
24 | $i = $i ? ++$i : 1;
25 | print "Demonstrating how alive the parent is for the {$i}th time.\n";
26 | });
27 |
28 | try {
29 | printf("Received the following from child: %s\n", yield from $context->receive());
30 | printf("Child ended with value %d!\n", yield from $context->join());
31 | } catch (Exception $e) {
32 | print "Error from child!\n";
33 | print $e."\n";
34 | } finally {
35 | $timer->stop();
36 | }
37 | });
38 |
39 | Loop\run();
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Aaron Piotrowski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Sync/Internal/ExitFailure.php:
--------------------------------------------------------------------------------
1 | type = get_class($exception);
31 | $this->message = $exception->getMessage();
32 | $this->code = $exception->getCode();
33 | $this->trace = $exception->getTraceAsString();
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function getResult()
40 | {
41 | throw new PanicError(
42 | sprintf(
43 | 'Uncaught exception in execution context of type "%s" with message "%s"',
44 | $this->type,
45 | $this->message
46 | ),
47 | $this->code,
48 | $this->trace
49 | );
50 | }
51 | }
--------------------------------------------------------------------------------
/src/Worker/Worker.php:
--------------------------------------------------------------------------------
1 | receive());
21 |
22 | print "Sleeping for 3 seconds...\n";
23 | sleep(3); // Blocking call in thread.
24 |
25 | yield from $this->send('Data sent from child.');
26 |
27 | print "Sleeping for 2 seconds...\n";
28 | sleep(2); // Blocking call in thread.
29 |
30 | return 42;
31 | });
32 |
33 | print "Waiting 2 seconds to send start data...\n";
34 | yield Coroutine\sleep(2);
35 |
36 | yield from $context->send('Start data');
37 |
38 | printf("Received the following from child: %s\n", yield from $context->receive());
39 | printf("Thread ended with value %d!\n", yield from $context->join());
40 | })->cleanup([$timer, 'stop'])->done();
41 |
42 | Loop\run();
43 |
--------------------------------------------------------------------------------
/examples/worker-pool.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | start();
14 |
15 | $coroutines = [];
16 |
17 | $coroutines[] = Coroutine\create(function () use ($pool) {
18 | $url = 'https://google.com';
19 | $result = yield from $pool->enqueue(new BlockingTask('file_get_contents', $url));
20 | printf("Read from %s: %d bytes\n", $url, strlen($result));
21 | });
22 |
23 | $coroutines[] = Coroutine\create(function () use ($pool) {
24 | $url = 'https://icicle.io';
25 | $result = yield from $pool->enqueue(new BlockingTask('file_get_contents', $url));
26 | printf("Read from %s: %d bytes\n", $url, strlen($result));
27 | });
28 |
29 | $coroutines[] = Coroutine\create(function () use ($pool) {
30 | $url = 'https://github.com';
31 | $result = yield from $pool->enqueue(new BlockingTask('file_get_contents', $url));
32 | printf("Read from %s: %d bytes\n", $url, strlen($result));
33 | });
34 |
35 | yield Awaitable\all($coroutines);
36 |
37 | return yield from $pool->shutdown();
38 | })->done();
39 |
40 | Loop\periodic(0.1, function () {
41 | printf(".\n");
42 | })->unreference();
43 |
44 | Loop\run();
45 |
--------------------------------------------------------------------------------
/src/Sync/Channel.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
28 | $this->environment = $environment;
29 | }
30 |
31 | /**
32 | * @coroutine
33 | *
34 | * @return \Generator
35 | */
36 | public function run(): \Generator
37 | {
38 | $task = yield from $this->channel->receive();
39 |
40 | while ($task instanceof Task) {
41 | $this->idle = false;
42 |
43 | try {
44 | $result = yield $task->run($this->environment);
45 | } catch (\Throwable $exception) {
46 | $result = new TaskFailure($exception);
47 | }
48 |
49 | yield from $this->channel->send($result);
50 |
51 | $this->idle = true;
52 |
53 | $task = yield from $this->channel->receive();
54 | }
55 |
56 | return $task;
57 | }
58 |
59 | /**
60 | * @return bool
61 | */
62 | public function isIdle(): bool
63 | {
64 | return $this->idle;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Worker/Pool.php:
--------------------------------------------------------------------------------
1 | worker = $worker;
26 | $this->push = $push;
27 | }
28 |
29 | /**
30 | * Automatically pushes the worker back into the queue.
31 | */
32 | public function __destruct()
33 | {
34 | ($this->push)($this->worker);
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | public function isRunning(): bool
41 | {
42 | return $this->worker->isRunning();
43 | }
44 |
45 | /**
46 | * {@inheritdoc}
47 | */
48 | public function isIdle(): bool
49 | {
50 | return $this->worker->isIdle();
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function start()
57 | {
58 | $this->worker->start();
59 | }
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | public function enqueue(Task $task): \Generator
65 | {
66 | return $this->worker->enqueue($task);
67 | }
68 |
69 | /**
70 | * {@inheritdoc}
71 | */
72 | public function shutdown(): \Generator
73 | {
74 | return $this->worker->shutdown();
75 | }
76 |
77 | /**
78 | * {@inheritdoc}
79 | */
80 | public function kill()
81 | {
82 | $this->worker->kill();
83 | }
84 | }
--------------------------------------------------------------------------------
/src/Sync/Lock.php:
--------------------------------------------------------------------------------
1 | $releaser A function to be called upon release.
29 | */
30 | public function __construct(callable $releaser)
31 | {
32 | $this->releaser = $releaser;
33 | }
34 |
35 | /**
36 | * Checks if the lock has already been released.
37 | *
38 | * @return bool True if the lock has already been released, otherwise false.
39 | */
40 | public function isReleased(): bool
41 | {
42 | return $this->released;
43 | }
44 |
45 | /**
46 | * Releases the lock.
47 | *
48 | * @throws LockAlreadyReleasedError If the lock was already released.
49 | */
50 | public function release()
51 | {
52 | if ($this->released) {
53 | throw new LockAlreadyReleasedError('The lock has already been released!');
54 | }
55 |
56 | // Invoke the releaser function given to us by the synchronization source
57 | // to release the lock.
58 | ($this->releaser)($this);
59 | $this->released = true;
60 | }
61 |
62 | /**
63 | * Releases the lock when there are no more references to it.
64 | */
65 | public function __destruct()
66 | {
67 | if (!$this->released) {
68 | $this->release();
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/bin/worker.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run());
45 | } catch (Throwable $exception) {
46 | $result = new ExitFailure($exception);
47 | }
48 |
49 | // Attempt to return the result.
50 | try {
51 | try {
52 | return yield from $channel->send($result);
53 | } catch (SerializationException $exception) {
54 | // Serializing the result failed. Send the reason why.
55 | return yield from $channel->send(new ExitFailure($exception));
56 | }
57 | } catch (ChannelException $exception) {
58 | // The result was not sendable! The parent context must have died or killed the context.
59 | return 0;
60 | }
61 | })->done();
62 |
63 | Loop\run();
64 |
--------------------------------------------------------------------------------
/src/Threading/Semaphore.php:
--------------------------------------------------------------------------------
1 | init($locks);
34 | }
35 |
36 | /**
37 | * Initializes the semaphore with a given number of locks.
38 | *
39 | * @param int $locks
40 | */
41 | private function init(int $locks)
42 | {
43 | $locks = (int) $locks;
44 | if ($locks < 1) {
45 | $locks = 1;
46 | }
47 |
48 | $this->semaphore = new Internal\Semaphore($locks);
49 | $this->maxLocks = $locks;
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | public function count(): int
56 | {
57 | return $this->semaphore->count();
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function getSize(): int
64 | {
65 | return $this->maxLocks;
66 | }
67 |
68 | /**
69 | * {@inheritdoc}
70 | */
71 | public function acquire(): \Generator
72 | {
73 | return $this->semaphore->acquire();
74 | }
75 |
76 | /**
77 | * Clones the semaphore, creating a new instance with the same number of locks, all available.
78 | */
79 | public function __clone()
80 | {
81 | $this->init($this->getSize());
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Threading/Parcel.php:
--------------------------------------------------------------------------------
1 | init($value);
29 | }
30 |
31 | /**
32 | * @param mixed $value
33 | */
34 | private function init($value)
35 | {
36 | $this->mutex = new Mutex();
37 | $this->storage = new Internal\Storage($value);
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function unwrap()
44 | {
45 | return $this->storage->get();
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | protected function wrap($value)
52 | {
53 | $this->storage->set($value);
54 | }
55 |
56 | /**
57 | * @coroutine
58 | *
59 | * Asynchronously invokes a callable while maintaining an exclusive lock on the container.
60 | *
61 | * @param callable $callback The function to invoke. The value in the container will be passed as the first
62 | * argument.
63 | *
64 | * @return \Generator
65 | */
66 | public function synchronized(callable $callback): \Generator
67 | {
68 | /** @var \Icicle\Concurrent\Sync\Lock $lock */
69 | $lock = yield from $this->mutex->acquire();
70 |
71 | try {
72 | $value = $this->unwrap();
73 | $result = yield $callback($value);
74 | $this->wrap(null === $result ? $value : $result);
75 | } finally {
76 | $lock->release();
77 | }
78 |
79 | return $result;
80 | }
81 |
82 | /**
83 | * {@inheritdoc}
84 | */
85 | public function __clone()
86 | {
87 | $this->init($this->unwrap());
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Sync/FileMutex.php:
--------------------------------------------------------------------------------
1 | fileName = tempnam(sys_get_temp_dir(), 'mutex-') . '.lock';
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function acquire(): \Generator
43 | {
44 | // Try to create the lock file. If the file already exists, someone else
45 | // has the lock, so set an asynchronous timer and try again.
46 | while (($handle = @fopen($this->fileName, 'x')) === false) {
47 | yield from Coroutine\sleep(self::LATENCY_TIMEOUT);
48 | }
49 |
50 | // Return a lock object that can be used to release the lock on the mutex.
51 | $lock = new Lock(function (Lock $lock) {
52 | $this->release();
53 | });
54 |
55 | fclose($handle);
56 |
57 | return $lock;
58 | }
59 |
60 | /**
61 | * Releases the lock on the mutex.
62 | *
63 | * @throws MutexException If the unlock operation failed.
64 | */
65 | protected function release()
66 | {
67 | $success = @unlink($this->fileName);
68 |
69 | if (!$success) {
70 | throw new MutexException('Failed to unlock the mutex file.');
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Worker/functions.php:
--------------------------------------------------------------------------------
1 | isRunning()) {
23 | $instance->start();
24 | }
25 |
26 | return $instance;
27 | }
28 |
29 | /**
30 | * @coroutine
31 | *
32 | * Enqueues a task to be executed by the global worker pool.
33 | *
34 | * @param \Icicle\Concurrent\Worker\Task $task The task to enqueue.
35 | *
36 | * @return \Generator
37 | *
38 | * @resolve mixed The return value of the task.
39 | */
40 | function enqueue(Task $task): \Generator
41 | {
42 | return pool()->enqueue($task);
43 | }
44 |
45 | /**
46 | * Creates a worker using the global worker factory.
47 | *
48 | * @return \Icicle\Concurrent\Worker\Worker
49 | */
50 | function create(): Worker
51 | {
52 | $worker = factory()->create();
53 | $worker->start();
54 | return $worker;
55 | }
56 |
57 | /**
58 | * Gets or sets the global worker factory.
59 | *
60 | * @param \Icicle\Concurrent\Worker\WorkerFactory|null $factory
61 | *
62 | * @return \Icicle\Concurrent\Worker\WorkerFactory
63 | */
64 | function factory(WorkerFactory $factory = null): WorkerFactory
65 | {
66 | static $instance;
67 |
68 | if (null !== $factory) {
69 | $instance = $factory;
70 | } elseif (null === $instance) {
71 | $instance = new DefaultWorkerFactory();
72 | }
73 |
74 | return $instance;
75 | }
76 |
77 | /**
78 | * Gets a worker from the global worker pool.
79 | *
80 | * @return \Icicle\Concurrent\Worker\Worker
81 | */
82 | function get(): Worker
83 | {
84 | return pool()->get();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Threading/Internal/Semaphore.php:
--------------------------------------------------------------------------------
1 | locks = $locks;
29 | }
30 |
31 | /**
32 | * Gets the number of currently available locks.
33 | *
34 | * @return int The number of available locks.
35 | */
36 | public function count(): int
37 | {
38 | return $this->locks;
39 | }
40 |
41 | /**
42 | * Uses a double locking mechanism to acquire a lock without blocking. A
43 | * synchronous mutex is used to make sure that the semaphore is queried one
44 | * at a time to preserve the integrity of the semaphore itself. Then a lock
45 | * count is used to check if a lock is available without blocking.
46 | *
47 | * If a lock is not available, we add the request to a queue and set a timer
48 | * to check again in the future.
49 | */
50 | public function acquire(): \Generator
51 | {
52 | $tsl = function () {
53 | // If there are no locks available or the wait queue is not empty,
54 | // we need to wait our turn to acquire a lock.
55 | if ($this->locks > 0) {
56 | --$this->locks;
57 | return false;
58 | }
59 | return true;
60 | };
61 |
62 | while ($this->locks < 1 || $this->synchronized($tsl)) {
63 | yield from Coroutine\sleep(self::LATENCY_TIMEOUT);
64 | }
65 |
66 | return new Lock(function () {
67 | $this->release();
68 | });
69 | }
70 |
71 | /**
72 | * Releases a lock from the semaphore.
73 | */
74 | protected function release()
75 | {
76 | $this->synchronized(function () {
77 | ++$this->locks;
78 | });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Process/ChannelledProcess.php:
--------------------------------------------------------------------------------
1 | process = new Process($command, $cwd, $env);
31 | }
32 |
33 | /**
34 | * Resets process values.
35 | */
36 | public function __clone()
37 | {
38 | $this->process = clone $this->process;
39 | $this->channel = null;
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public function start()
46 | {
47 | $this->process->start();
48 |
49 | $this->channel = new ChannelledStream($this->process->getStdOut(), $this->process->getStdIn());
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | public function isRunning(): bool
56 | {
57 | return $this->process->isRunning();
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function receive(): \Generator
64 | {
65 | if (null === $this->channel) {
66 | throw new StatusError('The process has not been started.');
67 | }
68 |
69 | $data = yield from $this->channel->receive();
70 |
71 | if ($data instanceof ExitStatus) {
72 | $data = $data->getResult();
73 | throw new SynchronizationError(sprintf(
74 | 'Thread unexpectedly exited with result of type: %s',
75 | is_object($data) ? get_class($data) : gettype($data)
76 | ));
77 | }
78 |
79 | return $data;
80 | }
81 |
82 | /**
83 | * {@inheritdoc}
84 | */
85 | public function send($data): \Generator
86 | {
87 | if (null === $this->channel) {
88 | throw new StatusError('The process has not been started.');
89 | }
90 |
91 | if ($data instanceof ExitStatus) {
92 | throw new InvalidArgumentError('Cannot send exit status objects.');
93 | }
94 |
95 | return yield from $this->channel->send($data);
96 | }
97 |
98 | /**
99 | * {@inheritdoc}
100 | */
101 | public function join(): \Generator
102 | {
103 | return $this->process->join();
104 | }
105 |
106 | /**
107 | * {@inheritdoc}
108 | */
109 | public function kill()
110 | {
111 | $this->process->kill();
112 | }
113 |
114 | /**
115 | * {@inheritdoc}
116 | */
117 | public function getPid(): int
118 | {
119 | return $this->process->getPid();
120 | }
121 |
122 | /**
123 | * {@inheritdoc}
124 | */
125 | public function signal(int $signo)
126 | {
127 | $this->process->signal($signo);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Concurrency for Icicle
2 |
3 | **True concurrency using native threading and multiprocessing for parallelizing code, *without* blocking.**
4 |
5 | This library is a component for [Icicle](https://github.com/icicleio/icicle) that provides native threading, multiprocessing, process synchronization, shared memory, and task workers. Like other Icicle components, this library uses [Coroutines](https://icicle.io/docs/manual/coroutines/) built from [Awaitables](https://icicle.io/docs/manual/awaitables/) and [Generators](http://www.php.net/manual/en/language.generators.overview.php) to make writing asynchronous code more like writing synchronous code.
6 |
7 | [](https://travis-ci.org/icicleio/concurrent)
8 | [](https://coveralls.io/r/icicleio/concurrent)
9 | [](http://semver.org)
10 | [](LICENSE)
11 | [](https://twitter.com/icicleio)
12 |
13 | This library provides a means of parallelizing code without littering your application with complicated lock checking and inter-process communication.
14 |
15 | To be as flexible as possible, this library comes with a collection of non-blocking concurrency tools that can be used independently as needed, as well as an "opinionated" worker API that allows you to assign units of work to a pool of worker threads or processes.
16 |
17 | #### Documentation and Support
18 |
19 | - [Full API Documentation](https://icicle.io/docs/api/Concurrent/)
20 | - [Official Twitter](https://twitter.com/icicleio)
21 | - [Gitter Chat](https://gitter.im/icicleio/icicle)
22 |
23 | ##### Requirements
24 |
25 | - PHP 5.5+ for v0.3.x branch (current stable) and v1.x branch (mirrors current stable)
26 | - PHP 7 for v2.0 (master) branch supporting generator delegation and return expressions
27 |
28 | ##### Suggested
29 |
30 | - [pthreads extension](https://pecl.php.net/package/pthreads): Best extension option for concurrency in PHP, but it requires PHP to be compiled with `--enable-maintainer-zts` to enable thread-safety.
31 | - [pcntl extension](http://php.net/manual/en/book.pcntl.php): Enables forking concurrency method.
32 | - [sysvmsg extension](http://php.net/manual/en/book.sem.php): Required for sharing memory between forks or processes.
33 |
34 | ##### Installation
35 |
36 | The recommended way to install is with the [Composer](http://getcomposer.org/) package manager. (See the [Composer installation guide](https://getcomposer.org/doc/00-intro.md) for information on installing and using Composer.)
37 |
38 | Run the following command to use this package in your project:
39 |
40 | ```bash
41 | composer require icicleio/concurrent
42 | ```
43 |
44 | You can also manually edit `composer.json` to add this library as a project requirement.
45 |
46 | ```js
47 | // composer.json
48 | {
49 | "require": {
50 | "icicleio/concurrent": "^0.3"
51 | }
52 | }
53 | ```
54 |
55 | ### Development and Contributing
56 |
57 | Interested in contributing to Icicle? Please see our [contributing guidelines](https://github.com/icicleio/icicle/blob/master/CONTRIBUTING.md) in the [Icicle repository](https://github.com/icicleio/icicle).
58 |
59 | Want to hack on the source? A [Vagrant](http://vagrantup.com) box is provided with the repository to give a common development environment for running concurrent threads and processes, and comes with a bunch of handy tools and scripts for testing and experimentation.
60 |
61 | Starting up and logging into the virtual machine is as simple as
62 |
63 | vagrant up && vagrant ssh
64 |
65 | Once inside the VM, you can install PHP extensions with [Pickle](https://github.com/FriendsOfPHP/pickle), switch versions with `newphp VERSION`, and test for memory leaks with [Valgrind](http://valgrind.org).
66 |
--------------------------------------------------------------------------------
/src/Sync/ChannelledStream.php:
--------------------------------------------------------------------------------
1 | write = $read;
48 | } else {
49 | $this->write = $write;
50 | }
51 |
52 | $this->read = $read;
53 |
54 | $this->errorHandler = function ($errno, $errstr) {
55 | throw new ChannelException(sprintf('Received corrupted data. Errno: %d; %s', $errno, $errstr));
56 | };
57 | }
58 |
59 | /**
60 | * {@inheritdoc}
61 | */
62 | public function send($data): \Generator
63 | {
64 | // Serialize the data to send into the channel.
65 | try {
66 | $serialized = serialize($data);
67 | } catch (\Throwable $exception) {
68 | throw new SerializationException(
69 | 'The given data cannot be sent because it is not serializable.', $exception
70 | );
71 | }
72 |
73 | $length = strlen($serialized);
74 |
75 | try {
76 | yield from $this->write->write(pack('CL', 0, $length) . $serialized);
77 | } catch (\Throwable $exception) {
78 | throw new ChannelException('Sending on the channel failed. Did the context die?', $exception);
79 | }
80 |
81 | return $length;
82 | }
83 |
84 | /**
85 | * {@inheritdoc}
86 | */
87 | public function receive(): \Generator
88 | {
89 | // Read the message length first to determine how much needs to be read from the stream.
90 | $length = self::HEADER_LENGTH;
91 | $buffer = '';
92 | $remaining = $length;
93 |
94 | try {
95 | do {
96 | $buffer .= yield from $this->read->read($remaining);
97 | } while ($remaining = $length - strlen($buffer));
98 |
99 | $data = unpack('Cprefix/Llength', $buffer);
100 |
101 | if (0 !== $data['prefix']) {
102 | throw new ChannelException('Invalid header received.');
103 | }
104 |
105 | $buffer = '';
106 | $remaining = $length = $data['length'];
107 |
108 | do {
109 | $buffer .= yield from $this->read->read($remaining);
110 | } while ($remaining = $length - strlen($buffer));
111 | } catch (\Throwable $exception) {
112 | throw new ChannelException('Reading from the channel failed. Did the context die?', $exception);
113 | }
114 |
115 | set_error_handler($this->errorHandler);
116 |
117 | // Attempt to unserialize the received data.
118 | try {
119 | $data = unserialize($buffer);
120 | } catch (\Throwable $exception) {
121 | throw new SerializationException('Exception thrown when unserializing data.', $exception);
122 | } finally {
123 | restore_error_handler();
124 | }
125 |
126 | return $data;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Worker/AbstractWorker.php:
--------------------------------------------------------------------------------
1 | context = $strand;
41 | $this->busyQueue = new \SplQueue();
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function isRunning(): bool
48 | {
49 | return $this->context->isRunning();
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | public function isIdle(): bool
56 | {
57 | return null === $this->active;
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function start()
64 | {
65 | $this->context->start();
66 | }
67 |
68 | /**
69 | * {@inheritdoc}
70 | */
71 | public function enqueue(Task $task): \Generator
72 | {
73 | if (!$this->context->isRunning()) {
74 | throw new StatusError('The worker has not been started.');
75 | }
76 |
77 | if ($this->shutdown) {
78 | throw new StatusError('The worker has been shut down.');
79 | }
80 |
81 | // If the worker is currently busy, store the task in a busy queue.
82 | if (null !== $this->active) {
83 | $delayed = new Delayed();
84 | $this->busyQueue->enqueue($delayed);
85 | yield $delayed;
86 | }
87 |
88 | $this->active = new Coroutine($this->send($task));
89 |
90 | try {
91 | $result = yield $this->active;
92 | } catch (\Throwable $exception) {
93 | $this->kill();
94 | throw new WorkerException('Sending the task to the worker failed.', $exception);
95 | } finally {
96 | $this->active = null;
97 | }
98 |
99 | // We're no longer busy at the moment, so dequeue a waiting task.
100 | if (!$this->busyQueue->isEmpty()) {
101 | $this->busyQueue->dequeue()->resolve();
102 | }
103 |
104 | if ($result instanceof TaskFailure) {
105 | throw $result->getException();
106 | }
107 |
108 | return $result;
109 | }
110 |
111 | /**
112 | * @coroutine
113 | *
114 | * @param \Icicle\Concurrent\Worker\Task $task
115 | *
116 | * @return \Generator
117 | *
118 | * @resolve mixed
119 | */
120 | private function send(Task $task): \Generator
121 | {
122 | yield from $this->context->send($task);
123 | return yield from $this->context->receive();
124 | }
125 |
126 | /**
127 | * {@inheritdoc}
128 | */
129 | public function shutdown(): \Generator
130 | {
131 | if (!$this->context->isRunning() || $this->shutdown) {
132 | throw new StatusError('The worker is not running.');
133 | }
134 |
135 | $this->shutdown = true;
136 |
137 | // Cancel any waiting tasks.
138 | $this->cancelPending();
139 |
140 | // If a task is currently running, wait for it to finish.
141 | if (null !== $this->active) {
142 | try {
143 | yield $this->active;
144 | } catch (\Throwable $exception) {
145 | // Ignore failure in this context.
146 | }
147 | }
148 |
149 | yield from $this->context->send(0);
150 | return yield from $this->context->join();
151 | }
152 |
153 | /**
154 | * {@inheritdoc}
155 | */
156 | public function kill()
157 | {
158 | $this->cancelPending();
159 | $this->context->kill();
160 | }
161 |
162 | /**
163 | * Cancels all pending tasks.
164 | */
165 | private function cancelPending()
166 | {
167 | if (!$this->busyQueue->isEmpty()) {
168 | $exception = new WorkerException('Worker was shut down.');
169 |
170 | do {
171 | $this->busyQueue->dequeue()->cancel($exception);
172 | } while (!$this->busyQueue->isEmpty());
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Threading/Internal/Thread.php:
--------------------------------------------------------------------------------
1 | function = $function;
49 | $this->args = $args;
50 | $this->socket = $socket;
51 | }
52 |
53 | /**
54 | * Runs the thread code and the initialized function.
55 | *
56 | * @codeCoverageIgnore Only executed in thread.
57 | */
58 | public function run()
59 | {
60 | /* First thing we need to do is re-initialize the class autoloader. If
61 | * we don't do this first, any object of a class that was loaded after
62 | * the thread started will just be garbage data and unserializable
63 | * values (like resources) will be lost. This happens even with
64 | * thread-safe objects.
65 | */
66 | foreach (get_declared_classes() as $className) {
67 | if (strpos($className, 'ComposerAutoloaderInit') === 0) {
68 | // Calling getLoader() will register the class loader for us
69 | $className::getLoader();
70 | break;
71 | }
72 | }
73 |
74 | Loop\loop($loop = Loop\create(false)); // Disable signals in thread.
75 |
76 | // At this point, the thread environment has been prepared so begin using the thread.
77 |
78 | try {
79 | $channel = new ChannelledStream(new DuplexPipe($this->socket, false));
80 | } catch (\Throwable $exception) {
81 | return; // Parent has destroyed Thread object, so just exit.
82 | }
83 |
84 | $coroutine = new Coroutine($this->execute($channel));
85 | $coroutine->done();
86 |
87 | $timer = $loop->timer(self::KILL_CHECK_FREQUENCY, true, function () use ($loop) {
88 | if ($this->killed) {
89 | $loop->stop();
90 | }
91 | });
92 | $timer->unreference();
93 |
94 | $loop->run();
95 | }
96 |
97 | /**
98 | * Sets a local variable to true so the running event loop can check for a kill signal.
99 | */
100 | public function kill()
101 | {
102 | return $this->killed = true;
103 | }
104 |
105 | /**
106 | * @coroutine
107 | *
108 | * @param \Icicle\Concurrent\Sync\Channel $channel
109 | *
110 | * @return \Generator
111 | *
112 | * @resolve int
113 | *
114 | * @codeCoverageIgnore Only executed in thread.
115 | */
116 | private function execute(Channel $channel): \Generator
117 | {
118 | try {
119 | if ($this->function instanceof \Closure) {
120 | $function = $this->function->bindTo($channel, Channel::class);
121 | }
122 |
123 | if (empty($function)) {
124 | $function = $this->function;
125 | }
126 |
127 | $result = new ExitSuccess(yield $function(...$this->args));
128 | } catch (\Throwable $exception) {
129 | $result = new ExitFailure($exception);
130 | }
131 |
132 | // Attempt to return the result.
133 | try {
134 | try {
135 | return yield from $channel->send($result);
136 | } catch (SerializationException $exception) {
137 | // Serializing the result failed. Send the reason why.
138 | return yield from $channel->send(new ExitFailure($exception));
139 | }
140 | } catch (ChannelException $exception) {
141 | // The result was not sendable! The parent context must have died or killed the context.
142 | return 0;
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/Worker/BasicEnvironment.php:
--------------------------------------------------------------------------------
1 | queue = new \SplPriorityQueue();
36 |
37 | $this->timer = Loop\periodic(1, function () {
38 | $time = time();
39 | while (!$this->queue->isEmpty()) {
40 | $key = $this->queue->top();
41 |
42 | if (isset($this->expire[$key])) {
43 | if ($time <= $this->expire[$key]) {
44 | break;
45 | }
46 |
47 | unset($this->data[$key], $this->expire[$key], $this->ttl[$key]);
48 | }
49 |
50 | $this->queue->extract();
51 | }
52 |
53 | if ($this->queue->isEmpty()) {
54 | $this->timer->stop();
55 | }
56 | });
57 |
58 | $this->timer->stop();
59 | $this->timer->unreference();
60 | }
61 |
62 | /**
63 | * @param string $key
64 | *
65 | * @return bool
66 | */
67 | public function exists(string $key): bool
68 | {
69 | return isset($this->data[$key]);
70 | }
71 |
72 | /**
73 | * @param string $key
74 | *
75 | * @return mixed|null Returns null if the key does not exist.
76 | */
77 | public function get(string $key)
78 | {
79 | if (isset($this->ttl[$key]) && 0 !== $this->ttl[$key]) {
80 | $this->expire[$key] = time() + $this->ttl[$key];
81 | $this->queue->insert($key, -$this->expire[$key]);
82 | }
83 |
84 | return isset($this->data[$key]) ? $this->data[$key] : null;
85 | }
86 |
87 | /**
88 | * @param string $key
89 | * @param mixed $value Using null for the value deletes the key.
90 | * @param int $ttl Number of seconds until data is automatically deleted. Use 0 for unlimited TTL.
91 | */
92 | public function set(string $key, $value, int $ttl = 0)
93 | {
94 | if (null === $value) {
95 | $this->delete($key);
96 | return;
97 | }
98 |
99 | $ttl = (int) $ttl;
100 | if (0 > $ttl) {
101 | $ttl = 0;
102 | }
103 |
104 | if (0 !== $ttl) {
105 | $this->ttl[$key] = $ttl;
106 | $this->expire[$key] = time() + $ttl;
107 | $this->queue->insert($key, -$this->expire[$key]);
108 |
109 | if (!$this->timer->isPending()) {
110 | $this->timer->start();
111 | }
112 | } else {
113 | unset($this->expire[$key], $this->ttl[$key]);
114 | }
115 |
116 | $this->data[$key] = $value;
117 | }
118 |
119 | /**
120 | * @param string $key
121 | */
122 | public function delete(string $key)
123 | {
124 | $key = (string) $key;
125 | unset($this->data[$key], $this->expire[$key], $this->ttl[$key]);
126 | }
127 |
128 | /**
129 | * Alias of exists().
130 | *
131 | * @param $key
132 | *
133 | * @return bool
134 | */
135 | public function offsetExists($key)
136 | {
137 | return $this->exists($key);
138 | }
139 |
140 | /**
141 | * Alias of get().
142 | *
143 | * @param string $key
144 | *
145 | * @return mixed
146 | */
147 | public function offsetGet($key)
148 | {
149 | return $this->get($key);
150 | }
151 |
152 | /**
153 | * Alias of set() with $ttl = 0.
154 | *
155 | * @param string $key
156 | * @param mixed $value
157 | */
158 | public function offsetSet($key, $value)
159 | {
160 | $this->set($key, $value);
161 | }
162 |
163 | /**
164 | * Alias of delete().
165 | *
166 | * @param string $key
167 | */
168 | public function offsetUnset($key)
169 | {
170 | $this->delete($key);
171 | }
172 |
173 | /**
174 | * @return int
175 | */
176 | public function count(): int
177 | {
178 | return count($this->data);
179 | }
180 |
181 | /**
182 | * Removes all values.
183 | */
184 | public function clear()
185 | {
186 | $this->data = [];
187 | $this->expire = [];
188 | $this->ttl = [];
189 |
190 | $this->timer->stop();
191 | $this->queue = new \SplPriorityQueue();
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
3 |
4 | ## [0.3.0] - 2016-01-15
5 | ### Added
6 | - Added `Icicle\Concurrent\Worker\factory()` function that accesses or sets the global worker factory.
7 | - Added `Icicle\Concurrent\Worker\get()` function that returns a worker from the global worker pool.
8 |
9 | ### Changed
10 | - `Icicle\Concurrent\Worker\Environment` is now an interface, with `Icicle\Concurrent\Worker\BasicEnvironment` being the default implementation provided to workers that is then provided to `Icicle\Concurrent\Worker\Task::run()`. Workers with different implementations of `Environment` can be easily created for particular applications.
11 | - `Icicle\Concurrent\Worker\Queue` has been removed. The functionality of queues has been merged into `Icicle\Concurrent\Worker\Pool` through a new `get()` method that returns a worker from the pool. The returned worker is marked as busy until all references have been destroyed. See the example code below.
12 |
13 | ```php
14 | use Icicle\Concurrent\Worker\DefaultPool;
15 |
16 | $pool = new DefaultPool();
17 | $pool->start();
18 | $worker = $pool->get(); // Marks $worker as busy in the pool.
19 |
20 | // Use $worker for a series of tasks.
21 |
22 | $worker = null; // Marks worker as idle in the pool.
23 | ```
24 |
25 | ## [0.2.2] - 2015-12-21
26 | ### Added
27 | - Added the `Icicle\Concurrent\Strand` interface that combines `Icicle\Concurrent\Context` and `Icicle\Concurrent\Sync\Channel`. This interface is implemented by the following classes (note that these classes implemented the two component interface separately, so no changes were made to the implementation):
28 | - `Icicle\Concurrent\Forking\Fork`
29 | - `Icicle\Concurrent\Threading\Thread`
30 | - `Icicle\Concurrent\Process\ChannelledProcess`
31 |
32 | ### Changed
33 | - `Icicle\Concurrent\Strand` interface is now required by the constructor of `Icicle\Concurrent\Worker\AbstractWorker`.
34 |
35 |
36 | ## [0.2.1] - 2015-12-16
37 | ### Added
38 | - Added `Icicle\Concurrent\Worker\DefaultQueue` implementing `Icicle\Concurrent\Worker\Queue` that provides a queue of workers that can be pulled and pushed from the queue as needed. Pulling a worker marks it as busy and pushing the worker back into the queue marks it as idle. If no idle workers remain in the queue, a worker is selected from those marked as busy. A worker queue allows a set of interdependent tasks (for example, tasks that depend on an environment value in the worker) to be run on a single worker without having to create and start separate workers for each task.
39 |
40 | ### Fixed
41 | - Fixed bug where exit status was not being read in `Icicle\Concurrent\Process\Process`, which also caused `Icicle\Concurrent\Worker\WorkerProcess` to fail.
42 |
43 | ## [0.2.0] - 2015-12-13
44 | ### Changed
45 | - Updated to Icicle `0.9.x` packages.
46 | - All exceptions now implement the `Icicle\Exception\Throwable` interface.
47 | - All interface names have been changed to remove the Interface suffix.
48 | - `Sync\Channel` was renamed to `Sync\ChannelledStream`.
49 | - `Sync\Parcel` was renamed to `Sync\SharedMemoryParcel`.
50 | - `Worker\Worker` has been renamed to `Worker\AbstractWorker`.
51 | - `Worker\Pool` has been renamed to `Worker\DefaultPool`.
52 | - `Worker\WorkerFactory` is now an interface, with the default implementation as `Worker\DefaultWorkerFactory`.
53 |
54 | ### Fixed
55 | - Fixed bug where workers would begin throwing `BusyError`s when tasks are enqueued simultaneously or between multiple coroutines.
56 | - Fixed bugs with worker shutdowns conflicting with tasks already running.
57 | - Fixed race conditions with pools occurring when enqueuing many tasks at once.
58 | - Fixed issue with contexts failing without explanation if the returned value could not be serialized.
59 |
60 |
61 | ## [0.1.1] - 2015-11-13
62 | ### Added
63 | - Runtime support for forks and threads can now be checked with `Forking\Fork::enabled()` and `Threading\Thread::enabled()`, respectively.
64 |
65 | ### Changed
66 | - Creating a fork will now throw an `UnsupportedError` if forking is not available.
67 | - Creating a thread will now throw an `UnsupportedError` if threading is not available.
68 | - Creating a `Sync\Parcel` will now throw an `UnsupportedError` if the `shmop` extension is not enabled.
69 | - Creating a `Sync\PosixSemaphore` will now throw an `UnsupportedError` if the `sysvmsg` extension is not enabled.
70 |
71 | ### Fixed
72 | - Fixed `Worker\Pool::__construct()` using `$minSize` as the maximum pool size instead.
73 | - `Process\Process` no longer reports as running during process destruction after calling `kill()`.
74 |
75 |
76 | ## [0.1.0] - 2015-10-28
77 | ### Changed
78 | - `Sync\ParcelInterface::wrap()` was removed in favor of `synchronized()`, which now passes the value of the parcel to the callback function. The value returned by the callback will be wrapped.
79 | - Both channel interfaces were combined into `Sync\Channel`.
80 | - `ContextInterface` no longer extends a channel interface
81 | - `Forking\Fork` and `Process\Process` now implement `ProcessInterface`.
82 | - Updated `icicleio/stream` to v0.4.1.
83 |
84 | ### Fixed
85 | - Fixed issue with error handler in `Sync\Channel` catching unrelated errors until the next tick.
86 |
87 |
88 | ## [0.1.0-beta1] - 2015-09-28
89 | First release.
90 |
91 | ### Added
92 | - Creating and controlling multiple threads with `Threading\Thread`.
93 | - Creating and controlling multiple forked processes with `Forking\Fork`.
94 | - Workers and tasks, which can use either threading, forks, or a separate PHP process.
95 | - A global worker pool that any tasks can be run in.
96 | - Channels for sending messages across execution contexts.
97 | - Parcels for storing values and objects in shared memory locations for use across contexts.
98 | - Non-blocking mutexes and semaphores for protecting parcels.
99 |
100 |
101 | [0.3.0]: https://github.com/icicleio/concurrent/releases/tag/v0.3.0
102 | [0.2.2]: https://github.com/icicleio/concurrent/releases/tag/v0.2.2
103 | [0.2.1]: https://github.com/icicleio/concurrent/releases/tag/v0.2.1
104 | [0.2.0]: https://github.com/icicleio/concurrent/releases/tag/v0.2.0
105 | [0.1.1]: https://github.com/icicleio/concurrent/releases/tag/v0.1.1
106 | [0.1.0]: https://github.com/icicleio/concurrent/releases/tag/v0.1.0
107 | [0.1.0-beta1]: https://github.com/icicleio/concurrent/releases/tag/v0.1.0-beta1
108 |
--------------------------------------------------------------------------------
/src/Sync/PosixSemaphore.php:
--------------------------------------------------------------------------------
1 | init($maxLocks, $permissions);
50 | }
51 |
52 | /**
53 | * @param int $maxLocks The maximum number of locks that can be acquired from the semaphore.
54 | * @param int $permissions Permissions to access the semaphore.
55 | *
56 | * @throws SemaphoreException If the semaphore could not be created due to an internal error.
57 | */
58 | private function init($maxLocks, $permissions)
59 | {
60 | $maxLocks = (int) $maxLocks;
61 | if ($maxLocks < 1) {
62 | $maxLocks = 1;
63 | }
64 |
65 | $this->key = abs(crc32(spl_object_hash($this)));
66 | $this->maxLocks = $maxLocks;
67 |
68 | $this->queue = msg_get_queue($this->key, $permissions);
69 | if (!$this->queue) {
70 | throw new SemaphoreException('Failed to create the semaphore.');
71 | }
72 |
73 | // Fill the semaphore with locks.
74 | while (--$maxLocks >= 0) {
75 | $this->release();
76 | }
77 | }
78 |
79 | /**
80 | * Checks if the semaphore has been freed.
81 | *
82 | * @return bool True if the semaphore has been freed, otherwise false.
83 | */
84 | public function isFreed(): bool
85 | {
86 | return !is_resource($this->queue) || !msg_queue_exists($this->key);
87 | }
88 |
89 | /**
90 | * Gets the maximum number of locks held by the semaphore.
91 | *
92 | * @return int The maximum number of locks held by the semaphore.
93 | */
94 | public function getSize(): int
95 | {
96 | return $this->maxLocks;
97 | }
98 |
99 | /**
100 | * Gets the access permissions of the semaphore.
101 | *
102 | * @return int A permissions mode.
103 | */
104 | public function getPermissions(): int
105 | {
106 | $stat = msg_stat_queue($this->queue);
107 | return $stat['msg_perm.mode'];
108 | }
109 |
110 | /**
111 | * Sets the access permissions of the semaphore.
112 | *
113 | * The current user must have access to the semaphore in order to change the permissions.
114 | *
115 | * @param int $mode A permissions mode to set.
116 | *
117 | * @throws SemaphoreException If the operation failed.
118 | */
119 | public function setPermissions(int $mode)
120 | {
121 | if (!msg_set_queue($this->queue, [
122 | 'msg_perm.mode' => $mode
123 | ])) {
124 | throw new SemaphoreException('Failed to change the semaphore permissions.');
125 | }
126 | }
127 |
128 | /**
129 | * {@inheritdoc}
130 | */
131 | public function count(): int
132 | {
133 | $stat = msg_stat_queue($this->queue);
134 | return $stat['msg_qnum'];
135 | }
136 |
137 | /**
138 | * {@inheritdoc}
139 | */
140 | public function acquire(): \Generator
141 | {
142 | do {
143 | // Attempt to acquire a lock from the semaphore.
144 | if (@msg_receive($this->queue, 0, $type, 1, $chr, false, MSG_IPC_NOWAIT, $errno)) {
145 | // A free lock was found, so resolve with a lock object that can
146 | // be used to release the lock.
147 | return new Lock(function (Lock $lock) {
148 | $this->release();
149 | });
150 | }
151 |
152 | // Check for unusual errors.
153 | if ($errno !== MSG_ENOMSG) {
154 | throw new SemaphoreException('Failed to acquire a lock.');
155 | }
156 | } while (yield from Coroutine\sleep(self::LATENCY_TIMEOUT));
157 | }
158 |
159 | /**
160 | * Removes the semaphore if it still exists.
161 | *
162 | * @throws SemaphoreException If the operation failed.
163 | */
164 | public function free()
165 | {
166 | if (is_resource($this->queue) && msg_queue_exists($this->key)) {
167 | if (!msg_remove_queue($this->queue)) {
168 | throw new SemaphoreException('Failed to free the semaphore.');
169 | }
170 |
171 | $this->queue = null;
172 | }
173 | }
174 |
175 | /**
176 | * Serializes the semaphore.
177 | *
178 | * @return string The serialized semaphore.
179 | */
180 | public function serialize(): string
181 | {
182 | return serialize([$this->key, $this->maxLocks]);
183 | }
184 |
185 | /**
186 | * Unserializes a serialized semaphore.
187 | *
188 | * @param string $serialized The serialized semaphore.
189 | */
190 | public function unserialize($serialized)
191 | {
192 | // Get the semaphore key and attempt to re-connect to the semaphore in memory.
193 | list($this->key, $this->maxLocks) = unserialize($serialized);
194 |
195 | if (msg_queue_exists($this->key)) {
196 | $this->queue = msg_get_queue($this->key);
197 | }
198 | }
199 |
200 | /**
201 | * Clones the semaphore, creating a new semaphore with the same size and permissions.
202 | */
203 | public function __clone()
204 | {
205 | $this->init($this->maxLocks, $this->getPermissions());
206 | }
207 |
208 | /**
209 | * Releases a lock from the semaphore.
210 | *
211 | * @throws SemaphoreException If the operation failed.
212 | */
213 | protected function release()
214 | {
215 | // Call send in non-blocking mode. If the call fails because the queue
216 | // is full, then the number of locks configured is too large.
217 | if (!@msg_send($this->queue, 1, "\0", false, false, $errno)) {
218 | if ($errno === MSG_EAGAIN) {
219 | throw new SemaphoreException('The semaphore size is larger than the system allows.');
220 | }
221 |
222 | throw new SemaphoreException('Failed to release the lock.');
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/Threading/Thread.php:
--------------------------------------------------------------------------------
1 | start();
77 | return $thread;
78 | }
79 |
80 | /**
81 | * Creates a new thread.
82 | *
83 | * @param callable $function The callable to invoke in the thread when run.
84 | *
85 | * @throws InvalidArgumentError If the given function cannot be safely invoked in a thread.
86 | * @throws UnsupportedError Thrown if the pthreads extension is not available.
87 | */
88 | public function __construct(callable $function, ...$args)
89 | {
90 | if (!self::enabled()) {
91 | throw new UnsupportedError("The pthreads extension is required to create threads.");
92 | }
93 |
94 | $this->function = $function;
95 | $this->args = $args;
96 | }
97 |
98 | /**
99 | * Returns the thread to the condition before starting. The new thread can be started and run independently of the
100 | * first thread.
101 | */
102 | public function __clone()
103 | {
104 | $this->thread = null;
105 | $this->socket = null;
106 | $this->pipe = null;
107 | $this->channel = null;
108 | $this->oid = 0;
109 | }
110 |
111 | /**
112 | * Kills the thread if it is still running.
113 | *
114 | * @throws \Icicle\Concurrent\Exception\ThreadException
115 | */
116 | public function __destruct()
117 | {
118 | if (getmypid() === $this->oid) {
119 | $this->kill();
120 | }
121 | }
122 |
123 | /**
124 | * Checks if the context is running.
125 | *
126 | * @return bool True if the context is running, otherwise false.
127 | */
128 | public function isRunning(): bool
129 | {
130 | return null !== $this->pipe && $this->pipe->isOpen();
131 | }
132 |
133 | /**
134 | * Spawns the thread and begins the thread's execution.
135 | *
136 | * @throws \Icicle\Concurrent\Exception\StatusError If the thread has already been started.
137 | * @throws \Icicle\Concurrent\Exception\ThreadException If starting the thread was unsuccessful.
138 | * @throws \Icicle\Stream\Exception\FailureException If creating a socket pair fails.
139 | */
140 | public function start()
141 | {
142 | if (0 !== $this->oid) {
143 | throw new StatusError('The thread has already been started.');
144 | }
145 |
146 | $this->oid = getmypid();
147 |
148 | list($channel, $this->socket) = Stream\pair();
149 |
150 | $this->thread = new Internal\Thread($this->socket, $this->function, $this->args);
151 |
152 | if (!$this->thread->start(PTHREADS_INHERIT_INI | PTHREADS_INHERIT_FUNCTIONS | PTHREADS_INHERIT_CLASSES)) {
153 | throw new ThreadException('Failed to start the thread.');
154 | }
155 |
156 | $this->channel = new ChannelledStream($this->pipe = new DuplexPipe($channel));
157 | }
158 |
159 | /**
160 | * Immediately kills the context.
161 | *
162 | * @throws ThreadException If killing the thread was unsuccessful.
163 | */
164 | public function kill()
165 | {
166 | if (null !== $this->thread) {
167 | try {
168 | if ($this->thread->isRunning() && !$this->thread->kill()) {
169 | throw new ThreadException('Could not kill thread.');
170 | }
171 | } finally {
172 | $this->close();
173 | }
174 | }
175 | }
176 |
177 | /**
178 | * Closes channel and socket if still open.
179 | */
180 | private function close()
181 | {
182 | if (null !== $this->pipe && $this->pipe->isOpen()) {
183 | $this->pipe->close();
184 | }
185 |
186 | if (is_resource($this->socket)) {
187 | fclose($this->socket);
188 | }
189 |
190 | $this->thread = null;
191 | $this->channel = null;
192 | }
193 |
194 | /**
195 | * @coroutine
196 | *
197 | * Gets a promise that resolves when the context ends and joins with the
198 | * parent context.
199 | *
200 | * @return \Generator
201 | *
202 | * @resolve mixed Resolved with the return or resolution value of the context once it has completed execution.
203 | *
204 | * @throws StatusError Thrown if the context has not been started.
205 | * @throws SynchronizationError Thrown if an exit status object is not received.
206 | */
207 | public function join(): \Generator
208 | {
209 | if (null === $this->channel || null === $this->thread) {
210 | throw new StatusError('The thread has not been started or has already finished.');
211 | }
212 |
213 | try {
214 | $response = yield from $this->channel->receive();
215 |
216 | if (!$response instanceof ExitStatus) {
217 | throw new SynchronizationError('Did not receive an exit status from thread.');
218 | }
219 |
220 | $result = $response->getResult();
221 |
222 | $this->thread->join();
223 | } catch (\Throwable $exception) {
224 | $this->kill();
225 | throw $exception;
226 | }
227 |
228 | $this->close();
229 |
230 | return $result;
231 | }
232 |
233 | /**
234 | * {@inheritdoc}
235 | */
236 | public function receive(): \Generator
237 | {
238 | if (null === $this->channel) {
239 | throw new StatusError('The thread has not been started or has already finished.');
240 | }
241 |
242 | $data = yield from $this->channel->receive();
243 |
244 | if ($data instanceof ExitStatus) {
245 | $this->kill();
246 | $data = $data->getResult();
247 | throw new SynchronizationError(sprintf(
248 | 'Thread unexpectedly exited with result of type: %s',
249 | is_object($data) ? get_class($data) : gettype($data)
250 | ));
251 | }
252 |
253 | return $data;
254 | }
255 |
256 | /**
257 | * {@inheritdoc}
258 | */
259 | public function send($data): \Generator
260 | {
261 | if (null === $this->channel) {
262 | throw new StatusError('The thread has not been started or has already finished.');
263 | }
264 |
265 | if ($data instanceof ExitStatus) {
266 | $this->kill();
267 | throw new InvalidArgumentError('Cannot send exit status objects.');
268 | }
269 |
270 | return yield from $this->channel->send($data);
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/src/Worker/DefaultPool.php:
--------------------------------------------------------------------------------
1 | maxSize = $maxSize;
84 | $this->minSize = $minSize;
85 |
86 | // Use the global factory if none is given.
87 | $this->factory = $factory ?: factory();
88 |
89 | $this->workers = new \SplObjectStorage();
90 | $this->idleWorkers = new \SplQueue();
91 | $this->busyQueue = new \SplQueue();
92 |
93 | $this->push = function (Worker $worker) {
94 | $this->push($worker);
95 | };
96 | }
97 |
98 | /**
99 | * Checks if the pool is running.
100 | *
101 | * @return bool True if the pool is running, otherwise false.
102 | */
103 | public function isRunning(): bool
104 | {
105 | return $this->running;
106 | }
107 |
108 | /**
109 | * Checks if the pool has any idle workers.
110 | *
111 | * @return bool True if the pool has at least one idle worker, otherwise false.
112 | */
113 | public function isIdle(): bool
114 | {
115 | return $this->idleWorkers->count() > 0;
116 | }
117 |
118 | /**
119 | * {@inheritdoc}
120 | */
121 | public function getMinSize(): int
122 | {
123 | return $this->minSize;
124 | }
125 |
126 | /**
127 | * {@inheritdoc}
128 | */
129 | public function getMaxSize(): int
130 | {
131 | return $this->maxSize;
132 | }
133 |
134 | /**
135 | * {@inheritdoc}
136 | */
137 | public function getWorkerCount(): int
138 | {
139 | return $this->workers->count();
140 | }
141 |
142 | /**
143 | * {@inheritdoc}
144 | */
145 | public function getIdleWorkerCount(): int
146 | {
147 | return $this->idleWorkers->count();
148 | }
149 |
150 | /**
151 | * Starts the worker pool execution.
152 | *
153 | * When the worker pool starts up, the minimum number of workers will be created. This adds some overhead to
154 | * starting the pool, but allows for greater performance during runtime.
155 | */
156 | public function start()
157 | {
158 | if ($this->isRunning()) {
159 | throw new StatusError('The worker pool has already been started.');
160 | }
161 |
162 | // Start up the pool with the minimum number of workers.
163 | $count = $this->minSize;
164 | while (--$count >= 0) {
165 | $worker = $this->createWorker();
166 | $this->idleWorkers->enqueue($worker);
167 | }
168 |
169 | $this->running = true;
170 | }
171 |
172 | /**
173 | * Enqueues a task to be executed by the worker pool.
174 | *
175 | * @coroutine
176 | *
177 | * @param Task $task The task to enqueue.
178 | *
179 | * @return \Generator
180 | *
181 | * @resolve mixed The return value of the task.
182 | *
183 | * @throws \Icicle\Concurrent\Exception\StatusError If the pool has not been started.
184 | * @throws \Icicle\Concurrent\Exception\TaskException If the task throws an exception.
185 | */
186 | public function enqueue(Task $task): \Generator
187 | {
188 | $worker = $this->get();
189 | return yield from $worker->enqueue($task);
190 | }
191 |
192 | /**
193 | * Shuts down the pool and all workers in it.
194 | *
195 | * @coroutine
196 | *
197 | * @return \Generator
198 | *
199 | * @throws \Icicle\Concurrent\Exception\StatusError If the pool has not been started.
200 | */
201 | public function shutdown(): \Generator
202 | {
203 | if (!$this->isRunning()) {
204 | throw new StatusError('The pool is not running.');
205 | }
206 |
207 | $this->running = false;
208 |
209 | $shutdowns = [];
210 |
211 | foreach ($this->workers as $worker) {
212 | if ($worker->isRunning()) {
213 | $shutdowns[] = new Coroutine($worker->shutdown());
214 | }
215 | }
216 |
217 | return yield Awaitable\reduce($shutdowns, function ($carry, $value) {
218 | return $carry ?: $value;
219 | }, 0);
220 | }
221 |
222 | /**
223 | * Kills all workers in the pool and halts the worker pool.
224 | */
225 | public function kill()
226 | {
227 | $this->running = false;
228 |
229 | foreach ($this->workers as $worker) {
230 | $worker->kill();
231 | }
232 | }
233 |
234 | /**
235 | * Creates a worker and adds them to the pool.
236 | *
237 | * @return Worker The worker created.
238 | */
239 | private function createWorker()
240 | {
241 | $worker = $this->factory->create();
242 | $worker->start();
243 |
244 | $this->workers->attach($worker, 0);
245 | return $worker;
246 | }
247 |
248 | /**
249 | * {@inheritdoc}
250 | */
251 | public function get(): Worker
252 | {
253 | if (!$this->isRunning()) {
254 | throw new StatusError('The queue is not running.');
255 | }
256 |
257 | do {
258 | if ($this->idleWorkers->isEmpty()) {
259 | if ($this->getWorkerCount() >= $this->maxSize) {
260 | // All possible workers busy, so shift from head (will be pushed back onto tail below).
261 | $worker = $this->busyQueue->shift();
262 | } else {
263 | // Max worker count has not been reached, so create another worker.
264 | $worker = $this->createWorker();
265 | }
266 | } else {
267 | // Shift a worker off the idle queue.
268 | $worker = $this->idleWorkers->shift();
269 | }
270 |
271 | if ($worker->isRunning()) {
272 | break;
273 | }
274 |
275 | $this->workers->detach($worker);
276 | } while (true);
277 |
278 | $this->busyQueue->push($worker);
279 | $this->workers[$worker] += 1;
280 |
281 | return new Internal\PooledWorker($worker, $this->push);
282 | }
283 |
284 | /**
285 | * Pushes the worker back into the queue.
286 | *
287 | * @param \Icicle\Concurrent\Worker\Worker $worker
288 | *
289 | * @throws \Icicle\Exception\InvalidArgumentError If the worker was not part of this queue.
290 | */
291 | private function push(Worker $worker)
292 | {
293 | if (!$this->workers->contains($worker)) {
294 | throw new InvalidArgumentError(
295 | 'The provided worker was not part of this queue.'
296 | );
297 | }
298 |
299 | if (0 === ($this->workers[$worker] -= 1)) {
300 | // Worker is completely idle, remove from busy queue and add to idle queue.
301 | foreach ($this->busyQueue as $key => $busy) {
302 | if ($busy === $worker) {
303 | unset($this->busyQueue[$key]);
304 | break;
305 | }
306 | }
307 |
308 | $this->idleWorkers->push($worker);
309 | }
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/src/Process/Process.php:
--------------------------------------------------------------------------------
1 | command = $command;
83 |
84 | if ('' !== $cwd) {
85 | $this->cwd = $cwd;
86 | }
87 |
88 | foreach ($env as $key => $value) {
89 | if (!is_array($value)) { // $env cannot accept array values.
90 | $this->env[(string) $key] = (string) $value;
91 | }
92 | }
93 |
94 | $this->options = $options;
95 | }
96 |
97 | /**
98 | * Stops the process if it is still running.
99 | */
100 | public function __destruct()
101 | {
102 | if (getmypid() === $this->oid) {
103 | $this->kill(); // Will only terminate if the process is still running.
104 |
105 | if (null !== $this->stdin) {
106 | $this->stdin->close();
107 | }
108 |
109 | if (null !== $this->stdout) {
110 | $this->stdout->close();
111 | }
112 |
113 | if (null !== $this->stderr) {
114 | $this->stderr->close();
115 | }
116 | }
117 | }
118 |
119 | /**
120 | * Resets process values.
121 | */
122 | public function __clone()
123 | {
124 | $this->process = null;
125 | $this->delayed = null;
126 | $this->poll = null;
127 | $this->pid = 0;
128 | $this->oid = 0;
129 | $this->stdin = null;
130 | $this->stdout = null;
131 | $this->stderr = null;
132 | }
133 |
134 | /**
135 | * @throws \Icicle\Concurrent\Exception\ProcessException If starting the process fails.
136 | * @throws \Icicle\Concurrent\Exception\StatusError If the process is already running.
137 | */
138 | public function start()
139 | {
140 | if (null !== $this->delayed) {
141 | throw new StatusError('The process has already been started.');
142 | }
143 |
144 | $this->delayed = new Delayed();
145 |
146 | $fd = [
147 | ['pipe', 'r'], // stdin
148 | ['pipe', 'w'], // stdout
149 | ['pipe', 'a'], // stderr
150 | ['pipe', 'w'], // exit code pipe
151 | ];
152 |
153 | $nd = 0 === strncasecmp(PHP_OS, 'WIN', 3) ? 'NUL' : '/dev/null';
154 |
155 | $command = sprintf('(%s) 3>%s; code=$?; echo $code >&3; exit $code', $this->command, $nd);
156 |
157 | $this->process = proc_open($command, $fd, $pipes, $this->cwd ?: null, $this->env ?: null, $this->options);
158 |
159 | if (!is_resource($this->process)) {
160 | throw new ProcessException('Could not start process.');
161 | }
162 |
163 | $this->oid = getmypid();
164 |
165 | $status = proc_get_status($this->process);
166 |
167 | if (!$status) {
168 | proc_close($this->process);
169 | $this->process = null;
170 | throw new ProcessException('Could not get process status.');
171 | }
172 |
173 | $this->pid = $status['pid'];
174 |
175 | $this->stdin = new WritablePipe($pipes[0]);
176 | $this->stdout = new ReadablePipe($pipes[1]);
177 | $this->stderr = new ReadablePipe($pipes[2]);
178 |
179 | $stream = $pipes[3];
180 | stream_set_blocking($stream, 0);
181 |
182 | $this->poll = Loop\poll($stream, function ($resource) {
183 | if (!is_resource($resource) || feof($resource)) {
184 | $this->close($resource);
185 | $this->delayed->reject(new ProcessException('Process ended unexpectedly.'));
186 | } else {
187 | $code = fread($resource, 1);
188 | $this->close($resource);
189 | if (!strlen($code) || !is_numeric($code)) {
190 | $this->delayed->reject(new ProcessException('Process ended without providing a status code.'));
191 | } else {
192 | $this->delayed->resolve((int) $code);
193 | }
194 | }
195 |
196 | $this->poll->free();
197 | });
198 | }
199 |
200 | /**
201 | * Closes the stream resource provided, the open process handle, and stdin.
202 | *
203 | * @param resource $resource
204 | */
205 | private function close($resource)
206 | {
207 | if (is_resource($resource)) {
208 | fclose($resource);
209 | }
210 |
211 | if (is_resource($this->process)) {
212 | proc_close($this->process);
213 | $this->process = null;
214 | }
215 |
216 | $this->stdin->close();
217 | }
218 |
219 | /**
220 | * @coroutine
221 | *
222 | * @return \Generator
223 | *
224 | * @throws \Icicle\Concurrent\Exception\StatusError If the process has not been started.
225 | */
226 | public function join(): \Generator
227 | {
228 | if (null === $this->delayed) {
229 | throw new StatusError('The process has not been started.');
230 | }
231 |
232 | $this->poll->listen();
233 |
234 | try {
235 | return yield $this->delayed;
236 | } finally {
237 | $this->stdout->close();
238 | $this->stderr->close();
239 | }
240 | }
241 |
242 | /**
243 | * {@inheritdoc}
244 | */
245 | public function kill()
246 | {
247 | if (is_resource($this->process)) {
248 | // Forcefully kill the process using SIGKILL.
249 | proc_terminate($this->process, 9);
250 |
251 | // "Detach" from the process and let it die asynchronously.
252 | $this->process = null;
253 | }
254 | }
255 |
256 | /**
257 | * Sends the given signal to the process.
258 | *
259 | * @param int $signo Signal number to send to process.
260 | *
261 | * @throws \Icicle\Concurrent\Exception\StatusError If the process is not running.
262 | */
263 | public function signal(int $signo)
264 | {
265 | if (!$this->isRunning()) {
266 | throw new StatusError('The process is not running.');
267 | }
268 |
269 | proc_terminate($this->process, (int) $signo);
270 | }
271 |
272 | /**
273 | * Returns the PID of the child process. Value is only meaningful if the process has been started and PHP was not
274 | * compiled with --enable-sigchild.
275 | *
276 | * @return int
277 | */
278 | public function getPid(): int
279 | {
280 | return $this->pid;
281 | }
282 |
283 | /**
284 | * Returns the command to execute.
285 | *
286 | * @return string The command to execute.
287 | */
288 | public function getCommand(): string
289 | {
290 | return $this->command;
291 | }
292 |
293 | /**
294 | * Gets the current working directory.
295 | *
296 | * @return string The current working directory or null if inherited from the current PHP process.
297 | */
298 | public function getWorkingDirectory(): string
299 | {
300 | if ('' === $this->cwd) {
301 | return getcwd() ?: '';
302 | }
303 |
304 | return $this->cwd;
305 | }
306 |
307 | /**
308 | * Gets the environment variables array.
309 | *
310 | * @return mixed[] Array of environment variables.
311 | */
312 | public function getEnv(): array
313 | {
314 | return $this->env;
315 | }
316 |
317 | /**
318 | * Gets the options to pass to proc_open().
319 | *
320 | * @return mixed[] Array of options.
321 | */
322 | public function getOptions(): array
323 | {
324 | return $this->options;
325 | }
326 |
327 | /**
328 | * Determines if the process is still running.
329 | *
330 | * @return bool
331 | */
332 | public function isRunning(): bool
333 | {
334 | return is_resource($this->process);
335 | }
336 |
337 | /**
338 | * Gets the process input stream (STDIN).
339 | *
340 | * @return \Icicle\Stream\WritableStream
341 | *
342 | * @throws \Icicle\Concurrent\Exception\StatusError If the process is not running.
343 | */
344 | public function getStdIn(): WritableStream
345 | {
346 | if (null === $this->stdin) {
347 | throw new StatusError('The process has not been started.');
348 | }
349 |
350 | return $this->stdin;
351 | }
352 |
353 | /**
354 | * Gets the process output stream (STDOUT).
355 | *
356 | * @return \Icicle\Stream\ReadableStream
357 | *
358 | * @throws \Icicle\Concurrent\Exception\StatusError If the process is not running.
359 | */
360 | public function getStdOut(): ReadableStream
361 | {
362 | if (null === $this->stdout) {
363 | throw new StatusError('The process has not been started.');
364 | }
365 |
366 | return $this->stdout;
367 | }
368 |
369 | /**
370 | * Gets the process error stream (STDERR).
371 | *
372 | * @return \Icicle\Stream\ReadableStream
373 | *
374 | * @throws \Icicle\Concurrent\Exception\StatusError If the process is not running.
375 | */
376 | public function getStdErr(): ReadableStream
377 | {
378 | if (null === $this->stderr) {
379 | throw new StatusError('The process has not been started.');
380 | }
381 |
382 | return $this->stderr;
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/src/Forking/Fork.php:
--------------------------------------------------------------------------------
1 | start();
70 | return $fork;
71 | }
72 |
73 | public function __construct(callable $function, ...$args)
74 | {
75 | if (!self::enabled()) {
76 | throw new UnsupportedError("The pcntl extension is required to create forks.");
77 | }
78 |
79 | $this->function = $function;
80 | $this->args = $args;
81 | }
82 |
83 | public function __clone()
84 | {
85 | $this->pid = 0;
86 | $this->oid = 0;
87 | $this->pipe = null;
88 | $this->channel = null;
89 | }
90 |
91 | public function __destruct()
92 | {
93 | if (0 !== $this->pid && posix_getpid() === $this->oid) { // Only kill in owner process.
94 | $this->kill(); // Will only terminate if the process is still running.
95 | }
96 | }
97 |
98 | /**
99 | * Checks if the context is running.
100 | *
101 | * @return bool True if the context is running, otherwise false.
102 | */
103 | public function isRunning(): bool
104 | {
105 | return 0 !== $this->pid && false !== posix_getpgid($this->pid);
106 | }
107 |
108 | /**
109 | * Gets the forked process's process ID.
110 | *
111 | * @return int The process ID.
112 | */
113 | public function getPid(): int
114 | {
115 | return $this->pid;
116 | }
117 |
118 | /**
119 | * Gets the fork's scheduling priority as a percentage.
120 | *
121 | * The priority is a float between 0 and 1 that indicates the relative priority for the forked process, where 0 is
122 | * very low priority, 1 is very high priority, and 0.5 is considered a "normal" priority. The value is based on the
123 | * forked process's "nice" value. The priority affects the operating system's scheduling of processes. How much the
124 | * priority actually affects the amount of CPU time the process gets is ultimately system-specific.
125 | *
126 | * @return float A priority value between 0 and 1.
127 | *
128 | * @throws ForkException If the operation failed.
129 | *
130 | * @see Fork::setPriority()
131 | * @see http://linux.die.net/man/2/getpriority
132 | */
133 | public function getPriority(): float
134 | {
135 | if (($nice = pcntl_getpriority($this->pid)) === false) {
136 | throw new ForkException('Failed to get the fork\'s priority.');
137 | }
138 |
139 | return (19 - $nice) / 39;
140 | }
141 |
142 | /**
143 | * Sets the fork's scheduling priority as a percentage.
144 | *
145 | * Note that on many systems, only the superuser can increase the priority of a process.
146 | *
147 | * @param float $priority A priority value between 0 and 1.
148 | *
149 | * @throws InvalidArgumentError If the given priority is an invalid value.
150 | * @throws ForkException If the operation failed.
151 | *
152 | * @see Fork::getPriority()
153 | */
154 | public function setPriority(float $priority): float
155 | {
156 | if ($priority < 0 || $priority > 1) {
157 | throw new InvalidArgumentError('Priority value must be between 0.0 and 1.0.');
158 | }
159 |
160 | $nice = round(19 - ($priority * 39));
161 |
162 | if (!pcntl_setpriority($nice, $this->pid, PRIO_PROCESS)) {
163 | throw new ForkException('Failed to set the fork\'s priority.');
164 | }
165 | }
166 |
167 | /**
168 | * Starts the context execution.
169 | *
170 | * @throws \Icicle\Concurrent\Exception\ForkException If forking fails.
171 | * @throws \Icicle\Stream\Exception\FailureException If creating a socket pair fails.
172 | */
173 | public function start()
174 | {
175 | if (0 !== $this->oid) {
176 | throw new StatusError('The context has already been started.');
177 | }
178 |
179 | list($parent, $child) = Stream\pair();
180 |
181 | switch ($pid = pcntl_fork()) {
182 | case -1: // Failure
183 | throw new ForkException('Could not fork process!');
184 |
185 | case 0: // Child
186 | // @codeCoverageIgnoreStart
187 |
188 | // Create a new event loop in the fork.
189 | Loop\loop($loop = Loop\create(false));
190 |
191 | $channel = new ChannelledStream($pipe = new DuplexPipe($parent));
192 | fclose($child);
193 |
194 | $coroutine = new Coroutine($this->execute($channel));
195 | $coroutine->done();
196 |
197 | try {
198 | $loop->run();
199 | $code = 0;
200 | } catch (\Throwable $exception) {
201 | $code = 1;
202 | }
203 |
204 | $pipe->close();
205 |
206 | exit($code);
207 |
208 | // @codeCoverageIgnoreEnd
209 |
210 | default: // Parent
211 | $this->pid = $pid;
212 | $this->oid = posix_getpid();
213 | $this->channel = new ChannelledStream($this->pipe = new DuplexPipe($child));
214 | fclose($parent);
215 | }
216 | }
217 |
218 | /**
219 | * @coroutine
220 | *
221 | * This method is run only on the child.
222 | *
223 | * @param \Icicle\Concurrent\Sync\Channel $channel
224 | *
225 | * @return \Generator
226 | *
227 | * @codeCoverageIgnore Only executed in the child.
228 | */
229 | private function execute(Channel $channel): \Generator
230 | {
231 | try {
232 | if ($this->function instanceof \Closure) {
233 | $function = $this->function->bindTo($channel, Channel::class);
234 | }
235 |
236 | if (empty($function)) {
237 | $function = $this->function;
238 | }
239 |
240 | $result = new ExitSuccess(yield $function(...$this->args));
241 | } catch (\Throwable $exception) {
242 | $result = new ExitFailure($exception);
243 | }
244 |
245 | // Attempt to return the result.
246 | try {
247 | try {
248 | return yield from $channel->send($result);
249 | } catch (SerializationException $exception) {
250 | // Serializing the result failed. Send the reason why.
251 | return yield from $channel->send(new ExitFailure($exception));
252 | }
253 | } catch (ChannelException $exception) {
254 | // The result was not sendable! The parent context must have died or killed the context.
255 | return 0;
256 | }
257 | }
258 |
259 | /**
260 | * {@inheritdoc}
261 | */
262 | public function kill()
263 | {
264 | if ($this->isRunning()) {
265 | // Forcefully kill the process using SIGKILL.
266 | posix_kill($this->pid, SIGKILL);
267 | }
268 |
269 | if (null !== $this->pipe && $this->pipe->isOpen()) {
270 | $this->pipe->close();
271 | }
272 |
273 | // "Detach" from the process and let it die asynchronously.
274 | $this->pid = 0;
275 | $this->channel = null;
276 | }
277 |
278 | /**
279 | * @param int $signo
280 | *
281 | * @throws \Icicle\Concurrent\Exception\StatusError
282 | */
283 | public function signal(int $signo)
284 | {
285 | if (0 === $this->pid) {
286 | throw new StatusError('The fork has not been started or has already finished.');
287 | }
288 |
289 | posix_kill($this->pid, (int) $signo);
290 | }
291 |
292 | /**
293 | * @coroutine
294 | *
295 | * Gets a promise that resolves when the context ends and joins with the
296 | * parent context.
297 | *
298 | * @return \Generator
299 | *
300 | * @resolve mixed Resolved with the return or resolution value of the context once it has completed execution.
301 | *
302 | * @throws \Icicle\Concurrent\Exception\StatusError Thrown if the context has not been started.
303 | * @throws \Icicle\Concurrent\Exception\SynchronizationError Thrown if an exit status object is not received.
304 | */
305 | public function join(): \Generator
306 | {
307 | if (null === $this->channel) {
308 | throw new StatusError('The fork has not been started or has already finished.');
309 | }
310 |
311 | try {
312 | $response = yield from $this->channel->receive();
313 |
314 | if (!$response instanceof ExitStatus) {
315 | throw new SynchronizationError(sprintf(
316 | 'Did not receive an exit status from fork. Instead received data of type %s',
317 | is_object($response) ? get_class($response) : gettype($response)
318 | ));
319 | }
320 |
321 | return $response->getResult();
322 | } finally {
323 | $this->kill();
324 | }
325 | }
326 |
327 | /**
328 | * {@inheritdoc}
329 | */
330 | public function receive(): \Generator
331 | {
332 | if (null === $this->channel) {
333 | throw new StatusError('The fork has not been started or has already finished.');
334 | }
335 |
336 | $data = yield from $this->channel->receive();
337 |
338 | if ($data instanceof ExitStatus) {
339 | $data = $data->getResult();
340 | throw new SynchronizationError(sprintf(
341 | 'Fork unexpectedly exited with result of type: %s',
342 | is_object($data) ? get_class($data) : gettype($data)
343 | ));
344 | }
345 |
346 | return $data;
347 | }
348 |
349 | /**
350 | * {@inheritdoc}
351 | */
352 | public function send($data): \Generator
353 | {
354 | if (null === $this->channel) {
355 | throw new StatusError('The fork has not been started or has already finished.');
356 | }
357 |
358 | if ($data instanceof ExitStatus) {
359 | throw new InvalidArgumentError('Cannot send exit status objects.');
360 | }
361 |
362 | return yield from $this->channel->send($data);
363 | }
364 | }
365 |
--------------------------------------------------------------------------------
/src/Sync/SharedMemoryParcel.php:
--------------------------------------------------------------------------------
1 | init($value, $size, $permissions);
76 | }
77 |
78 | /**
79 | * @param mixed $value
80 | * @param int $size
81 | * @param int $permissions
82 | */
83 | private function init($value, int $size = 16384, int $permissions = 0600)
84 | {
85 | $this->key = abs(crc32(spl_object_hash($this)));
86 | $this->memOpen($this->key, 'n', $permissions, $size + self::MEM_DATA_OFFSET);
87 | $this->setHeader(self::STATE_ALLOCATED, 0, $permissions);
88 | $this->wrap($value);
89 |
90 | $this->semaphore = new PosixSemaphore(1);
91 | }
92 |
93 | /**
94 | * Checks if the object has been freed.
95 | *
96 | * Note that this does not check if the object has been destroyed; it only
97 | * checks if this handle has freed its reference to the object.
98 | *
99 | * @return bool True if the object is freed, otherwise false.
100 | */
101 | public function isFreed(): bool
102 | {
103 | // If we are no longer connected to the memory segment, check if it has
104 | // been invalidated.
105 | if ($this->handle !== null) {
106 | $this->handleMovedMemory();
107 | $header = $this->getHeader();
108 | return $header['state'] === static::STATE_FREED;
109 | }
110 |
111 | return true;
112 | }
113 |
114 | /**
115 | * {@inheritdoc}
116 | */
117 | public function unwrap()
118 | {
119 | if ($this->isFreed()) {
120 | throw new SharedMemoryException('The object has already been freed.');
121 | }
122 |
123 | $header = $this->getHeader();
124 |
125 | // Make sure the header is in a valid state and format.
126 | if ($header['state'] !== self::STATE_ALLOCATED || $header['size'] <= 0) {
127 | throw new SharedMemoryException('Shared object memory is corrupt.');
128 | }
129 |
130 | // Read the actual value data from memory and unserialize it.
131 | $data = $this->memGet(self::MEM_DATA_OFFSET, $header['size']);
132 | return unserialize($data);
133 | }
134 |
135 | /**
136 | * If the value requires more memory to store than currently allocated, a
137 | * new shared memory segment will be allocated with a larger size to store
138 | * the value in. The previous memory segment will be cleaned up and marked
139 | * for deletion. Other processes and threads will be notified of the new
140 | * memory segment on the next read attempt. Once all running processes and
141 | * threads disconnect from the old segment, it will be freed by the OS.
142 | */
143 | protected function wrap($value)
144 | {
145 | if ($this->isFreed()) {
146 | throw new SharedMemoryException('The object has already been freed.');
147 | }
148 |
149 | $serialized = serialize($value);
150 | $size = strlen($serialized);
151 | $header = $this->getHeader();
152 |
153 | /* If we run out of space, we need to allocate a new shared memory
154 | segment that is larger than the current one. To coordinate with other
155 | processes, we will leave a message in the old segment that the segment
156 | has moved and along with the new key. The old segment will be discarded
157 | automatically after all other processes notice the change and close
158 | the old handle.
159 | */
160 | if (shmop_size($this->handle) < $size + self::MEM_DATA_OFFSET) {
161 | $this->key = $this->key < 0xffffffff ? $this->key + 1 : mt_rand(0x10, 0xfffffffe);
162 | $this->setHeader(self::STATE_MOVED, $this->key, 0);
163 |
164 | $this->memDelete();
165 | shmop_close($this->handle);
166 |
167 | $this->memOpen($this->key, 'n', $header['permissions'], $size * 2);
168 | }
169 |
170 | // Rewrite the header and the serialized value to memory.
171 | $this->setHeader(self::STATE_ALLOCATED, $size, $header['permissions']);
172 | $this->memSet(self::MEM_DATA_OFFSET, $serialized);
173 | }
174 |
175 | /**
176 | * {@inheritdoc}
177 | */
178 | public function synchronized(callable $callback): \Generator
179 | {
180 | /** @var \Icicle\Concurrent\Sync\Lock $lock */
181 | $lock = yield from $this->semaphore->acquire();
182 |
183 | try {
184 | $value = $this->unwrap();
185 | $result = yield $callback($value);
186 | $this->wrap(null === $result ? $value : $result);
187 | } finally {
188 | $lock->release();
189 | }
190 |
191 | return $result;
192 | }
193 |
194 | /**
195 | * Frees the shared object from memory.
196 | *
197 | * The memory containing the shared value will be invalidated. When all
198 | * process disconnect from the object, the shared memory block will be
199 | * destroyed by the OS.
200 | *
201 | * Calling `free()` on an object already freed will have no effect.
202 | */
203 | public function free()
204 | {
205 | if (!$this->isFreed()) {
206 | // Invalidate the memory block by setting its state to FREED.
207 | $this->setHeader(static::STATE_FREED, 0, 0);
208 |
209 | // Request the block to be deleted, then close our local handle.
210 | $this->memDelete();
211 | shmop_close($this->handle);
212 | $this->handle = null;
213 |
214 | $this->semaphore->free();
215 | }
216 | }
217 |
218 | /**
219 | * Serializes the local object handle.
220 | *
221 | * Note that this does not serialize the object that is referenced, just the
222 | * object handle.
223 | *
224 | * @return string The serialized object handle.
225 | */
226 | public function serialize(): string
227 | {
228 | return serialize([$this->key, $this->semaphore]);
229 | }
230 |
231 | /**
232 | * Unserializes the local object handle.
233 | *
234 | * @param string $serialized The serialized object handle.
235 | */
236 | public function unserialize($serialized)
237 | {
238 | list($this->key, $this->semaphore) = unserialize($serialized);
239 | $this->memOpen($this->key, 'w', 0, 0);
240 | }
241 |
242 | /**
243 | * {@inheritdoc}
244 | */
245 | public function __clone()
246 | {
247 | $value = $this->unwrap();
248 | $header = $this->getHeader();
249 | $this->init($value, $header['size'], $header['permissions']);
250 | }
251 |
252 | /**
253 | * Gets information about the object for debugging purposes.
254 | *
255 | * @return array An array of debugging information.
256 | */
257 | public function __debugInfo()
258 | {
259 | if ($this->isFreed()) {
260 | return [
261 | 'id' => $this->key,
262 | 'object' => null,
263 | 'freed' => true,
264 | ];
265 | }
266 |
267 | return [
268 | 'id' => $this->key,
269 | 'object' => $this->unwrap(),
270 | 'freed' => false,
271 | ];
272 | }
273 |
274 | /**
275 | * Updates the current memory segment handle, handling any moves made on the
276 | * data.
277 | */
278 | private function handleMovedMemory()
279 | {
280 | // Read from the memory block and handle moved blocks until we find the
281 | // correct block.
282 | while (true) {
283 | $header = $this->getHeader();
284 |
285 | // If the state is STATE_MOVED, the memory is stale and has been moved
286 | // to a new location. Move handle and try to read again.
287 | if ($header['state'] !== self::STATE_MOVED) {
288 | break;
289 | }
290 |
291 | shmop_close($this->handle);
292 | $this->key = $header['size'];
293 | $this->memOpen($this->key, 'w', 0, 0);
294 | }
295 | }
296 |
297 | /**
298 | * Reads and returns the data header at the current memory segment.
299 | *
300 | * @return array An associative array of header data.
301 | */
302 | private function getHeader(): array
303 | {
304 | $data = $this->memGet(0, self::MEM_DATA_OFFSET);
305 | return unpack('Cstate/Lsize/Spermissions', $data);
306 | }
307 |
308 | /**
309 | * Sets the header data for the current memory segment.
310 | *
311 | * @param int $state An object state.
312 | * @param int $size The size of the stored data, or other value.
313 | * @param int $permissions The permissions mask on the memory segment.
314 | */
315 | private function setHeader(int $state, int $size, int $permissions)
316 | {
317 | $header = pack('CLS', $state, $size, $permissions);
318 | $this->memSet(0, $header);
319 | }
320 |
321 | /**
322 | * Opens a shared memory handle.
323 | *
324 | * @param int $key The shared memory key.
325 | * @param string $mode The mode to open the shared memory in.
326 | * @param int $permissions Process permissions on the shared memory.
327 | * @param int $size The size to crate the shared memory in bytes.
328 | */
329 | private function memOpen(int $key, string $mode, int $permissions, int $size)
330 | {
331 | $this->handle = @shmop_open($key, $mode, $permissions, $size);
332 | if ($this->handle === false) {
333 | throw new SharedMemoryException('Failed to create shared memory block.');
334 | }
335 | }
336 |
337 | /**
338 | * Reads binary data from shared memory.
339 | *
340 | * @param int $offset The offset to read from.
341 | * @param int $size The number of bytes to read.
342 | *
343 | * @return string The binary data at the given offset.
344 | */
345 | private function memGet(int $offset, int $size): string
346 | {
347 | $data = shmop_read($this->handle, $offset, $size);
348 | if ($data === false) {
349 | throw new SharedMemoryException('Failed to read from shared memory block.');
350 | }
351 | return $data;
352 | }
353 |
354 | /**
355 | * Writes binary data to shared memory.
356 | *
357 | * @param int $offset The offset to write to.
358 | * @param string $data The binary data to write.
359 | */
360 | private function memSet(int $offset, string $data)
361 | {
362 | if (!shmop_write($this->handle, $data, $offset)) {
363 | throw new SharedMemoryException('Failed to write to shared memory block.');
364 | }
365 | }
366 |
367 | /**
368 | * Requests the shared memory segment to be deleted.
369 | */
370 | private function memDelete()
371 | {
372 | if (!shmop_delete($this->handle)) {
373 | throw new SharedMemoryException('Failed to discard shared memory block.');
374 | }
375 | }
376 | }
377 |
--------------------------------------------------------------------------------