2019-02-13 21:19:01 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Amp\Parallel\Context;
|
|
|
|
|
2019-02-13 23:30:06 +01:00
|
|
|
use Amp\Failure;
|
2019-02-13 21:19:01 +01:00
|
|
|
use Amp\Loop;
|
|
|
|
use Amp\Parallel\Sync\ChannelException;
|
|
|
|
use Amp\Parallel\Sync\ChannelledSocket;
|
|
|
|
use Amp\Parallel\Sync\ExitResult;
|
2019-02-13 23:30:06 +01:00
|
|
|
use Amp\Parallel\Sync\SerializationException;
|
2019-02-13 21:19:01 +01:00
|
|
|
use Amp\Parallel\Sync\SynchronizationError;
|
|
|
|
use Amp\Promise;
|
|
|
|
use parallel\Runtime;
|
|
|
|
use function Amp\call;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements an execution context using native multi-threading.
|
|
|
|
*
|
|
|
|
* The thread context is not itself threaded. A local instance of the context is
|
|
|
|
* maintained both in the context that creates the thread and in the thread
|
|
|
|
* itself.
|
|
|
|
*/
|
|
|
|
final class Parallel implements Context
|
|
|
|
{
|
|
|
|
const KEY_LENGTH = 32;
|
|
|
|
|
|
|
|
/** @var string|null */
|
|
|
|
private static $autoloadPath;
|
|
|
|
|
|
|
|
/** @var Internal\ProcessHub */
|
|
|
|
private $hub;
|
|
|
|
|
2019-02-13 23:30:06 +01:00
|
|
|
/** @var Runtime|null */
|
2019-02-13 21:19:01 +01:00
|
|
|
private $runtime;
|
|
|
|
|
2019-02-13 23:30:06 +01:00
|
|
|
/** @var ChannelledSocket|null A channel for communicating with the parallel thread. */
|
2019-02-15 01:12:51 +01:00
|
|
|
private $communicationChannel;
|
|
|
|
|
|
|
|
/** @var ChannelledSocket|null */
|
|
|
|
private $signalChannel;
|
2019-02-13 21:19:01 +01:00
|
|
|
|
2019-02-13 23:30:06 +01:00
|
|
|
/** @var string Script path. */
|
2019-02-13 21:19:01 +01:00
|
|
|
private $script;
|
|
|
|
|
|
|
|
/** @var mixed[] */
|
|
|
|
private $args;
|
|
|
|
|
|
|
|
/** @var int */
|
|
|
|
private $oid = 0;
|
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
/** @var bool */
|
|
|
|
private $killed = false;
|
|
|
|
|
2019-02-13 21:19:01 +01:00
|
|
|
/** @var \parallel\Future|null */
|
|
|
|
private $future;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if threading is enabled.
|
|
|
|
*
|
|
|
|
* @return bool True if threading is enabled, otherwise false.
|
|
|
|
*/
|
|
|
|
public static function isSupported(): bool
|
|
|
|
{
|
|
|
|
return \extension_loaded('parallel');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates and starts a new thread.
|
|
|
|
*
|
|
|
|
* @param callable $function The callable to invoke in the thread. First argument is an instance of
|
|
|
|
* \Amp\Parallel\Sync\Channel.
|
|
|
|
* @param mixed ...$args Additional arguments to pass to the given callable.
|
|
|
|
*
|
|
|
|
* @return Promise<Thread> The thread object that was spawned.
|
|
|
|
*/
|
|
|
|
public static function run(string $path, ...$args): Promise
|
|
|
|
{
|
|
|
|
$thread = new self($path, ...$args);
|
|
|
|
return call(function () use ($thread) {
|
|
|
|
yield $thread->start();
|
|
|
|
return $thread;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new thread.
|
|
|
|
*
|
|
|
|
* @param callable $function The callable to invoke in the thread. First argument is an instance of
|
|
|
|
* \Amp\Parallel\Sync\Channel.
|
|
|
|
* @param mixed ...$args Additional arguments to pass to the given callable.
|
|
|
|
*
|
|
|
|
* @throws \Error Thrown if the pthreads extension is not available.
|
|
|
|
*/
|
|
|
|
public function __construct(string $script, ...$args)
|
|
|
|
{
|
|
|
|
$this->hub = Loop::getState(self::class);
|
|
|
|
if (!$this->hub instanceof Internal\ProcessHub) {
|
|
|
|
$this->hub = new Internal\ProcessHub;
|
|
|
|
Loop::setState(self::class, $this->hub);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!self::isSupported()) {
|
|
|
|
throw new \Error("The parallel extension is required to create parallel threads.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->script = $script;
|
|
|
|
$this->args = $args;
|
|
|
|
|
|
|
|
if (self::$autoloadPath === null) {
|
|
|
|
$paths = [
|
|
|
|
\dirname(__DIR__, 2) . \DIRECTORY_SEPARATOR . "vendor" . \DIRECTORY_SEPARATOR . "autoload.php",
|
|
|
|
\dirname(__DIR__, 4) . \DIRECTORY_SEPARATOR . "autoload.php",
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($paths as $path) {
|
|
|
|
if (\file_exists($path)) {
|
|
|
|
self::$autoloadPath = $path;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self::$autoloadPath === null) {
|
|
|
|
throw new \Error("Could not locate autoload.php");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the thread to the condition before starting. The new thread can be started and run independently of the
|
|
|
|
* first thread.
|
|
|
|
*/
|
|
|
|
public function __clone()
|
|
|
|
{
|
|
|
|
$this->runtime = null;
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->future = null;
|
|
|
|
$this->communicationChannel = null;
|
|
|
|
$this->signalChannel = null;
|
2019-02-13 21:19:01 +01:00
|
|
|
$this->oid = 0;
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->killed = false;
|
2019-02-13 21:19:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Kills the thread if it is still running.
|
|
|
|
*
|
|
|
|
* @throws \Amp\Parallel\Context\ContextException
|
|
|
|
*/
|
|
|
|
public function __destruct()
|
|
|
|
{
|
|
|
|
if (\getmypid() === $this->oid) {
|
|
|
|
$this->kill();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the context is running.
|
|
|
|
*
|
|
|
|
* @return bool True if the context is running, otherwise false.
|
|
|
|
*/
|
|
|
|
public function isRunning(): bool
|
|
|
|
{
|
2019-02-15 01:12:51 +01:00
|
|
|
return $this->communicationChannel !== null && $this->signalChannel !== null;
|
2019-02-13 21:19:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Spawns the thread and begins the thread's execution.
|
|
|
|
*
|
|
|
|
* @return Promise<null> Resolved once the thread has started.
|
|
|
|
*
|
|
|
|
* @throws \Amp\Parallel\Context\StatusError If the thread has already been started.
|
|
|
|
* @throws \Amp\Parallel\Context\ContextException If starting the thread was unsuccessful.
|
|
|
|
*/
|
|
|
|
public function start(): Promise
|
|
|
|
{
|
|
|
|
if ($this->oid !== 0) {
|
|
|
|
throw new StatusError('The thread has already been started.');
|
|
|
|
}
|
|
|
|
|
2019-02-13 23:30:06 +01:00
|
|
|
try {
|
|
|
|
$arguments = \serialize($this->args);
|
|
|
|
} catch (\Throwable $exception) {
|
|
|
|
return new Failure(new SerializationException("Arguments must be serializable.", 0, $exception));
|
|
|
|
}
|
|
|
|
|
2019-02-13 21:19:01 +01:00
|
|
|
$this->oid = \getmypid();
|
|
|
|
|
|
|
|
$this->runtime = new Runtime(self::$autoloadPath);
|
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
$cid = \random_int(\PHP_INT_MIN, \PHP_INT_MAX);
|
|
|
|
|
|
|
|
do {
|
|
|
|
$sid = \random_int(\PHP_INT_MIN, \PHP_INT_MAX);
|
|
|
|
} while ($sid === $cid);
|
2019-02-13 21:19:01 +01:00
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->future = $this->runtime->run(static function (string $uri, string $channelKey, string $signalKey, string $path, string $arguments): int {
|
2019-02-13 21:19:01 +01:00
|
|
|
\define("AMP_CONTEXT", "parallel");
|
|
|
|
|
|
|
|
if (!$socket = \stream_socket_client($uri, $errno, $errstr, 5, \STREAM_CLIENT_CONNECT)) {
|
|
|
|
\trigger_error("Could not connect to IPC socket", E_USER_ERROR);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
$communicationChannel = new ChannelledSocket($socket, $socket);
|
|
|
|
|
|
|
|
try {
|
|
|
|
Promise\wait($communicationChannel->send($channelKey));
|
|
|
|
} catch (\Throwable $exception) {
|
|
|
|
\trigger_error("Could not send key to parent", E_USER_ERROR);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$socket = \stream_socket_client($uri, $errno, $errstr, 5, \STREAM_CLIENT_CONNECT)) {
|
|
|
|
\trigger_error("Could not connect to IPC socket", E_USER_ERROR);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
$signalChannel = new ChannelledSocket($socket, $socket);
|
|
|
|
$signalChannel->unreference();
|
2019-02-13 21:19:01 +01:00
|
|
|
|
|
|
|
try {
|
2019-02-15 01:12:51 +01:00
|
|
|
Promise\wait($signalChannel->send($signalKey));
|
2019-02-13 21:19:01 +01:00
|
|
|
} catch (\Throwable $exception) {
|
|
|
|
\trigger_error("Could not send key to parent", E_USER_ERROR);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
Promise\rethrow(Internal\ParallelRunner::handleSignals($signalChannel));
|
|
|
|
|
|
|
|
return Internal\ParallelRunner::execute($communicationChannel, $path, $arguments);
|
2019-02-13 21:19:01 +01:00
|
|
|
}, [
|
|
|
|
$this->hub->getUri(),
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->hub->generateKey($cid, self::KEY_LENGTH),
|
|
|
|
$this->hub->generateKey($sid, self::KEY_LENGTH),
|
2019-02-13 21:19:01 +01:00
|
|
|
$this->script,
|
2019-02-13 23:30:06 +01:00
|
|
|
$arguments
|
2019-02-13 21:19:01 +01:00
|
|
|
]);
|
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
return call(function () use ($cid, $sid) {
|
2019-02-13 21:19:01 +01:00
|
|
|
try {
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->communicationChannel = yield $this->hub->accept($cid);
|
|
|
|
$this->signalChannel = yield $this->hub->accept($sid);
|
2019-02-13 21:19:01 +01:00
|
|
|
} catch (\Throwable $exception) {
|
2019-02-13 23:30:06 +01:00
|
|
|
$this->kill();
|
2019-02-13 21:19:01 +01:00
|
|
|
throw new ContextException("Starting the parallel runtime failed", 0, $exception);
|
|
|
|
}
|
2019-02-15 01:12:51 +01:00
|
|
|
|
|
|
|
if ($this->killed) {
|
|
|
|
$this->kill();
|
|
|
|
}
|
2019-02-13 21:19:01 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Immediately kills the context.
|
|
|
|
*/
|
|
|
|
public function kill()
|
|
|
|
{
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->killed = true;
|
|
|
|
|
|
|
|
if ($this->signalChannel !== null) {
|
2019-02-13 23:30:06 +01:00
|
|
|
try {
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->signalChannel->send(Internal\ParallelRunner::KILL);
|
|
|
|
$this->signalChannel->close();
|
2019-02-13 23:30:06 +01:00
|
|
|
} finally {
|
|
|
|
$this->close();
|
|
|
|
}
|
|
|
|
}
|
2019-02-13 21:19:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes channel and socket if still open.
|
|
|
|
*/
|
|
|
|
private function close()
|
|
|
|
{
|
2019-02-15 01:12:51 +01:00
|
|
|
if ($this->communicationChannel !== null) {
|
|
|
|
$this->communicationChannel->close();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->signalChannel !== null) {
|
|
|
|
$this->signalChannel->close();
|
2019-02-13 21:19:01 +01:00
|
|
|
}
|
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
$this->communicationChannel = null;
|
|
|
|
$this->signalChannel = null;
|
2019-02-13 21:19:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a promise that resolves when the context ends and joins with the
|
|
|
|
* parent context.
|
|
|
|
*
|
|
|
|
* @return \Amp\Promise<mixed>
|
|
|
|
*
|
|
|
|
* @throws StatusError Thrown if the context has not been started.
|
|
|
|
* @throws SynchronizationError Thrown if an exit status object is not received.
|
|
|
|
* @throws ContextException If the context stops responding.
|
|
|
|
*/
|
|
|
|
public function join(): Promise
|
|
|
|
{
|
2019-02-15 01:12:51 +01:00
|
|
|
if ($this->communicationChannel === null) {
|
2019-02-13 21:19:01 +01:00
|
|
|
throw new StatusError('The thread has not been started or has already finished.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return call(function () {
|
|
|
|
try {
|
2019-02-15 01:12:51 +01:00
|
|
|
$response = yield $this->communicationChannel->receive();
|
2019-02-13 21:19:01 +01:00
|
|
|
|
|
|
|
if (!$response instanceof ExitResult) {
|
|
|
|
throw new SynchronizationError('Did not receive an exit result from thread.');
|
|
|
|
}
|
|
|
|
} catch (ChannelException $exception) {
|
|
|
|
$this->kill();
|
|
|
|
throw new ContextException(
|
|
|
|
"The context stopped responding, potentially due to a fatal error or calling exit",
|
|
|
|
0,
|
|
|
|
$exception
|
|
|
|
);
|
|
|
|
} catch (\Throwable $exception) {
|
|
|
|
$this->kill();
|
|
|
|
throw $exception;
|
|
|
|
} finally {
|
|
|
|
$this->close();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $response->getResult();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function receive(): Promise
|
|
|
|
{
|
2019-02-15 01:12:51 +01:00
|
|
|
if ($this->communicationChannel === null) {
|
2019-02-13 21:19:01 +01:00
|
|
|
throw new StatusError('The process has not been started.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return call(function () {
|
2019-02-15 01:12:51 +01:00
|
|
|
$data = yield $this->communicationChannel->receive();
|
2019-02-13 21:19:01 +01:00
|
|
|
|
|
|
|
if ($data instanceof ExitResult) {
|
|
|
|
$data = $data->getResult();
|
|
|
|
throw new SynchronizationError(\sprintf(
|
|
|
|
'Thread process unexpectedly exited with result of type: %s',
|
|
|
|
\is_object($data) ? \get_class($data) : \gettype($data)
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function send($data): Promise
|
|
|
|
{
|
2019-02-15 01:12:51 +01:00
|
|
|
if ($this->communicationChannel === null) {
|
2019-02-13 21:19:01 +01:00
|
|
|
throw new StatusError('The thread has not been started or has already finished.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($data instanceof ExitResult) {
|
|
|
|
throw new \Error('Cannot send exit result objects.');
|
|
|
|
}
|
|
|
|
|
2019-02-15 01:12:51 +01:00
|
|
|
return $this->communicationChannel->send($data);
|
2019-02-13 21:19:01 +01:00
|
|
|
}
|
|
|
|
}
|