2015-07-13 17:30:59 -05:00
|
|
|
<?php
|
|
|
|
namespace Icicle\Concurrent\Threading;
|
|
|
|
|
2015-08-27 09:10:08 -05:00
|
|
|
use Icicle\Concurrent\ContextInterface;
|
2015-08-22 16:27:44 -05:00
|
|
|
use Icicle\Concurrent\Exception\InvalidArgumentError;
|
2015-08-27 13:06:39 -05:00
|
|
|
use Icicle\Concurrent\Exception\StatusError;
|
2015-08-22 16:27:44 -05:00
|
|
|
use Icicle\Concurrent\Exception\SynchronizationError;
|
2015-08-28 16:18:02 -05:00
|
|
|
use Icicle\Concurrent\Exception\ThreadException;
|
2015-08-05 02:48:43 -05:00
|
|
|
use Icicle\Concurrent\Sync\Channel;
|
2015-08-29 01:40:10 -05:00
|
|
|
use Icicle\Concurrent\Sync\Internal\ExitStatusInterface;
|
2015-08-22 16:27:44 -05:00
|
|
|
use Icicle\Coroutine;
|
2015-09-04 16:22:41 -05:00
|
|
|
use Icicle\Socket;
|
2015-08-25 09:37:22 -05:00
|
|
|
use Icicle\Socket\Stream\DuplexStream;
|
2015-07-26 17:53:00 -05:00
|
|
|
|
2015-07-15 12:36:32 -05:00
|
|
|
/**
|
2015-08-22 16:27:44 -05:00
|
|
|
* Implements an execution context using native multi-threading.
|
2015-08-10 17:38:58 -05:00
|
|
|
*
|
2015-08-22 16:27:44 -05:00
|
|
|
* 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.
|
2015-07-15 12:36:32 -05:00
|
|
|
*/
|
2015-09-02 17:23:22 -05:00
|
|
|
class Thread implements ContextInterface
|
2015-07-13 17:30:59 -05:00
|
|
|
{
|
2015-08-25 09:39:58 -05:00
|
|
|
const LATENCY_TIMEOUT = 0.01; // 10 ms
|
|
|
|
|
2015-07-26 17:53:00 -05:00
|
|
|
/**
|
2015-08-30 17:52:00 -05:00
|
|
|
* @var Internal\Thread An internal thread instance.
|
2015-07-26 17:53:00 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
private $thread;
|
2015-07-26 17:53:00 -05:00
|
|
|
|
2015-08-06 23:25:04 -05:00
|
|
|
/**
|
2015-08-30 17:52:00 -05:00
|
|
|
* @var Channel A channel for communicating with the thread.
|
2015-08-06 23:25:04 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
private $channel;
|
2015-08-06 23:25:04 -05:00
|
|
|
|
2015-08-24 19:35:42 -05:00
|
|
|
/**
|
|
|
|
* @var resource
|
|
|
|
*/
|
|
|
|
private $socket;
|
|
|
|
|
2015-08-27 13:06:39 -05:00
|
|
|
/**
|
2015-09-04 16:22:41 -05:00
|
|
|
* @var callable
|
2015-08-27 13:06:39 -05:00
|
|
|
*/
|
2015-09-04 16:22:41 -05:00
|
|
|
private $function;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var mixed[]
|
|
|
|
*/
|
|
|
|
private $args;
|
2015-08-27 13:06:39 -05:00
|
|
|
|
2015-08-06 18:59:25 -05:00
|
|
|
/**
|
2015-08-22 16:27:44 -05:00
|
|
|
* Spawns a new thread and runs it.
|
|
|
|
*
|
2015-08-30 17:52:00 -05:00
|
|
|
* @param callable $function The callable to invoke in the thread.
|
2015-08-22 16:27:44 -05:00
|
|
|
*
|
2015-08-30 17:52:00 -05:00
|
|
|
* @return Thread The thread object that was spawned.
|
2015-08-06 18:59:25 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
public static function spawn(callable $function /* , ...$args */)
|
|
|
|
{
|
2015-08-24 10:47:36 -05:00
|
|
|
$class = new \ReflectionClass(__CLASS__);
|
|
|
|
$thread = $class->newInstanceArgs(func_get_args());
|
2015-08-22 16:27:44 -05:00
|
|
|
$thread->start();
|
|
|
|
return $thread;
|
|
|
|
}
|
2015-08-05 02:48:43 -05:00
|
|
|
|
2015-08-18 10:12:06 -05:00
|
|
|
/**
|
2015-08-30 17:52:00 -05:00
|
|
|
* Creates a new thread.
|
2015-08-22 16:27:44 -05:00
|
|
|
*
|
2015-08-30 17:52:00 -05:00
|
|
|
* @param callable $function The callable to invoke in the thread when run.
|
|
|
|
*
|
|
|
|
* @throws InvalidArgumentError If the given function cannot be safely invoked in a thread.
|
2015-08-18 10:12:06 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
public function __construct(callable $function /* , ...$args */)
|
|
|
|
{
|
|
|
|
$args = array_slice(func_get_args(), 1);
|
|
|
|
|
2015-08-27 15:32:57 -05:00
|
|
|
// Make sure closures don't `use` other variables or have statics.
|
|
|
|
if ($function instanceof \Closure) {
|
|
|
|
$reflector = new \ReflectionFunction($function);
|
|
|
|
if (!empty($reflector->getStaticVariables())) {
|
|
|
|
throw new InvalidArgumentError('Closures with static variables cannot be passed to thread.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-04 16:22:41 -05:00
|
|
|
$this->function = $function;
|
|
|
|
$this->args = $args;
|
|
|
|
}
|
2015-08-22 16:27:44 -05:00
|
|
|
|
2015-09-04 16:22:41 -05:00
|
|
|
/**
|
|
|
|
* 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->thread = null;
|
|
|
|
$this->socket = null;
|
|
|
|
$this->channel = null;
|
2015-08-22 16:27:44 -05:00
|
|
|
}
|
2015-08-18 10:12:06 -05:00
|
|
|
|
2015-07-26 17:53:00 -05:00
|
|
|
/**
|
2015-08-22 16:27:44 -05:00
|
|
|
* Checks if the context is running.
|
2015-07-26 17:53:00 -05:00
|
|
|
*
|
2015-08-22 16:27:44 -05:00
|
|
|
* @return bool True if the context is running, otherwise false.
|
2015-07-26 17:53:00 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
public function isRunning()
|
2015-07-14 17:15:10 -05:00
|
|
|
{
|
2015-09-04 16:22:41 -05:00
|
|
|
return null !== $this->thread && $this->thread->isRunning() && $this->channel->isOpen();
|
2015-08-06 18:59:25 -05:00
|
|
|
}
|
|
|
|
|
2015-08-05 02:48:43 -05:00
|
|
|
/**
|
2015-08-30 17:52:00 -05:00
|
|
|
* Spawns the thread and begins the thread's execution.
|
|
|
|
*
|
2015-09-04 16:22:41 -05:00
|
|
|
* @throws \Icicle\Concurrent\Exception\StatusError If the thread has already been started.
|
|
|
|
* @throws \Icicle\Concurrent\Exception\ThreadException If starting the thread was unsuccessful.
|
|
|
|
* @throws \Icicle\Socket\Exception\FailureException If creating a socket pair fails.
|
2015-08-05 02:48:43 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
public function start()
|
2015-07-13 17:30:59 -05:00
|
|
|
{
|
2015-09-04 16:22:41 -05:00
|
|
|
if (null !== $this->thread) {
|
2015-08-27 13:06:39 -05:00
|
|
|
throw new StatusError('The thread has already been started.');
|
2015-08-05 02:48:43 -05:00
|
|
|
}
|
|
|
|
|
2015-09-04 16:22:41 -05:00
|
|
|
list($channel, $this->socket) = Socket\pair();
|
|
|
|
|
|
|
|
$this->thread = new Internal\Thread($this->socket, $this->function, $this->args);
|
|
|
|
|
2015-08-30 18:25:44 -05:00
|
|
|
if (!$this->thread->start(PTHREADS_INHERIT_INI | PTHREADS_INHERIT_FUNCTIONS | PTHREADS_INHERIT_CLASSES)) {
|
|
|
|
throw new ThreadException('Failed to start the thread.');
|
|
|
|
}
|
2015-08-27 13:06:39 -05:00
|
|
|
|
2015-09-04 16:22:41 -05:00
|
|
|
$this->channel = new Channel(new DuplexStream($channel));
|
2015-08-22 16:27:44 -05:00
|
|
|
}
|
2015-08-10 17:38:58 -05:00
|
|
|
|
2015-08-22 16:27:44 -05:00
|
|
|
/**
|
|
|
|
* Immediately kills the context.
|
2015-08-30 17:52:00 -05:00
|
|
|
*
|
|
|
|
* @throws ThreadException If killing the thread was unsuccessful.
|
2015-08-22 16:27:44 -05:00
|
|
|
*/
|
|
|
|
public function kill()
|
|
|
|
{
|
2015-09-05 12:52:56 -05:00
|
|
|
if (null === $this->thread) {
|
|
|
|
throw new StatusError('The thread has not been started.');
|
|
|
|
}
|
|
|
|
|
2015-09-02 17:24:01 -05:00
|
|
|
$this->close();
|
|
|
|
|
2015-09-05 12:52:56 -05:00
|
|
|
$this->thread->kill();
|
2015-09-02 17:24:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes channel and socket if still open.
|
|
|
|
*/
|
|
|
|
private function close()
|
|
|
|
{
|
2015-09-04 16:22:41 -05:00
|
|
|
if (null !== $this->channel && $this->channel->isOpen()) {
|
2015-09-02 17:24:01 -05:00
|
|
|
$this->channel->close();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is_resource($this->socket)) {
|
|
|
|
fclose($this->socket);
|
2015-08-28 16:18:02 -05:00
|
|
|
}
|
2015-08-05 02:48:43 -05:00
|
|
|
}
|
|
|
|
|
2015-08-18 10:12:06 -05:00
|
|
|
/**
|
2015-08-22 16:27:44 -05:00
|
|
|
* @coroutine
|
|
|
|
*
|
|
|
|
* Gets a promise that resolves when the context ends and joins with the
|
|
|
|
* parent context.
|
2015-08-18 10:12:06 -05:00
|
|
|
*
|
2015-08-22 16:27:44 -05:00
|
|
|
* @return \Generator
|
|
|
|
*
|
|
|
|
* @resolve mixed Resolved with the return or resolution value of the context once it has completed execution.
|
2015-08-24 19:35:42 -05:00
|
|
|
*
|
2015-09-02 17:24:01 -05:00
|
|
|
* @throws StatusError Thrown if the context has not been started.
|
2015-08-30 17:52:00 -05:00
|
|
|
* @throws SynchronizationError Thrown if an exit status object is not received.
|
2015-08-18 10:12:06 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
public function join()
|
2015-08-18 10:12:06 -05:00
|
|
|
{
|
2015-09-04 16:22:41 -05:00
|
|
|
if (null === $this->thread) {
|
2015-08-30 18:25:44 -05:00
|
|
|
throw new StatusError('The thread has not been started.');
|
2015-08-27 13:06:39 -05:00
|
|
|
}
|
|
|
|
|
2015-08-18 10:12:06 -05:00
|
|
|
try {
|
2015-08-22 16:27:44 -05:00
|
|
|
$response = (yield $this->channel->receive());
|
|
|
|
|
|
|
|
if (!$response instanceof ExitStatusInterface) {
|
|
|
|
throw new SynchronizationError('Did not receive an exit status from thread.');
|
2015-08-18 10:12:06 -05:00
|
|
|
}
|
2015-08-22 16:27:44 -05:00
|
|
|
|
|
|
|
yield $response->getResult();
|
2015-09-02 17:24:01 -05:00
|
|
|
|
2015-08-22 16:27:44 -05:00
|
|
|
$this->thread->join();
|
2015-09-02 17:24:01 -05:00
|
|
|
} catch (\Exception $exception) {
|
|
|
|
$this->kill();
|
|
|
|
throw $exception;
|
2015-08-18 10:12:06 -05:00
|
|
|
}
|
2015-09-02 17:24:01 -05:00
|
|
|
|
|
|
|
$this->close();
|
2015-08-18 10:12:06 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-08-22 16:27:44 -05:00
|
|
|
* {@inheritdoc}
|
2015-08-18 10:12:06 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
public function receive()
|
2015-08-18 10:12:06 -05:00
|
|
|
{
|
2015-09-04 16:22:41 -05:00
|
|
|
if (null === $this->thread) {
|
2015-08-30 18:25:44 -05:00
|
|
|
throw new StatusError('The thread has not been started.');
|
2015-08-27 13:06:39 -05:00
|
|
|
}
|
|
|
|
|
2015-08-22 16:27:44 -05:00
|
|
|
$data = (yield $this->channel->receive());
|
|
|
|
|
|
|
|
if ($data instanceof ExitStatusInterface) {
|
2015-09-02 16:34:42 -05:00
|
|
|
$this->kill();
|
2015-08-22 16:27:44 -05:00
|
|
|
$data = $data->getResult();
|
|
|
|
throw new SynchronizationError(sprintf(
|
|
|
|
'Thread unexpectedly exited with result of type: %s',
|
|
|
|
is_object($data) ? get_class($data) : gettype($data)
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
yield $data;
|
2015-08-18 10:12:06 -05:00
|
|
|
}
|
|
|
|
|
2015-08-06 18:59:25 -05:00
|
|
|
/**
|
2015-08-22 16:27:44 -05:00
|
|
|
* {@inheritdoc}
|
2015-08-06 18:59:25 -05:00
|
|
|
*/
|
2015-08-22 16:27:44 -05:00
|
|
|
public function send($data)
|
2015-08-05 02:48:43 -05:00
|
|
|
{
|
2015-09-04 16:22:41 -05:00
|
|
|
if (null === $this->thread) {
|
2015-08-30 18:25:44 -05:00
|
|
|
throw new StatusError('The thread has not been started.');
|
2015-08-27 13:06:39 -05:00
|
|
|
}
|
|
|
|
|
2015-08-22 16:27:44 -05:00
|
|
|
if ($data instanceof ExitStatusInterface) {
|
2015-09-02 16:34:42 -05:00
|
|
|
$this->kill();
|
2015-08-22 16:27:44 -05:00
|
|
|
throw new InvalidArgumentError('Cannot send exit status objects.');
|
2015-07-26 17:53:00 -05:00
|
|
|
}
|
2015-07-13 17:30:59 -05:00
|
|
|
|
2015-08-27 13:06:39 -05:00
|
|
|
yield $this->channel->send($data);
|
2015-08-22 16:27:44 -05:00
|
|
|
}
|
2015-07-13 17:30:59 -05:00
|
|
|
}
|