From d2dbcc5078de9c227099584f8482895d90814550 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Thu, 14 Apr 2022 22:41:41 -0400 Subject: [PATCH] removed `Channels` class, PHP libuv thread feature has bugs --- Spawn/Channels.php | 435 ---------------------------------- Spawn/ChannelsInterface.php | 41 ---- Spawn/Thread.php | 73 +----- tests/ZThreadChannelsTest.php | 367 ---------------------------- tests/ZThreadMultiTest.php | 55 ----- tests/ZThreadTest.php | 38 +++ 6 files changed, 40 insertions(+), 969 deletions(-) delete mode 100644 Spawn/Channels.php delete mode 100644 Spawn/ChannelsInterface.php delete mode 100644 tests/ZThreadChannelsTest.php delete mode 100644 tests/ZThreadMultiTest.php diff --git a/Spawn/Channels.php b/Spawn/Channels.php deleted file mode 100644 index e0d2181..0000000 --- a/Spawn/Channels.php +++ /dev/null @@ -1,435 +0,0 @@ -Channels] */ - protected static $channels = []; - protected static $anonymous = 0; - - /** - * Current `Thread` PHP owner's `UID` - * @var integer - */ - protected static $uid = null; - - - /** @var callable */ - protected static $tickLoop = null; - protected static $uv = null; - - /** - * `Parent's` _Thread_ instance - * @var Thread - */ - protected static $thread; - - protected $name = ''; - protected $index = 0; - protected $capacity = null; - protected $type = null; - protected $buffered = null; - protected $open = true; - protected $counter = 0; - protected $started = false; - - /** - * Channel status, either `reading` or `writing` - * @var string - */ - protected $state = 'waiting'; - - /** - * Current thread id - * @var integer|string - */ - protected $tid; - - /** - * @var resource - stream - */ - protected $input = null; - - /** - * @var resource - stream - */ - protected $output = null; - public $value; - public $message; - protected $__write; - protected $__read; - - public function __destruct() - { - if (\is_resource($this->input)) - \fclose($this->input); - - if (\is_resource($this->output)) - \fclose($this->output); - - unset($this->buffered); - $this->open = false; - $this->buffered = null; - $this->input = null; - $this->output = null; - $this->state = null; - $this->buffered = null; - $this->name = ''; - $this->index = 0; - $this->capacity = null; - $this->type = null; - $this->tid = null; - $this->started = null; - $this->value = null; - $this->message = null; - $this->__write = null; - $this->__read = null; - self::$uid = null; - self::$thread = null; - } - - public function __construct(int $capacity = -1, ?string $name = null, bool $anonymous = true) - { - if (($capacity < -1) || ($capacity == 0)) - throw new \TypeError('capacity may be -1 for unlimited, or a positive integer'); - - [$this->input, $this->output] = \stream_socket_pair((\IS_WINDOWS ? \STREAM_PF_INET : \STREAM_PF_UNIX), - \STREAM_SOCK_STREAM, - \STREAM_IPPROTO_IP - ); - - \stream_set_blocking($this->input, true); - \stream_set_blocking($this->output, true); - \stream_set_read_buffer($this->input, 0); - \stream_set_write_buffer($this->output, 0); - - $this->type = $capacity < 0 ? 'unbuffered' : 'buffered'; - $this->capacity = $capacity; - $this->buffered = new \SplQueue; - - if ($anonymous) { - self::$anonymous++; - $this->index = self::$anonymous; - $name = empty($name) ? $_SERVER['SCRIPT_NAME'] : $name; - $this->name = \sprintf("%s#%u@%d[%d]", $name, __LINE__, \strlen($name), $this->index); - self::$channels[$this->index] = $this; - } else { - $this->name = $name; - self::$channels[$name] = $this; - } - } - - public function __toString() - { - return $this->name; - } - - /** - * Destroy `All` Channel instances. - * - * @return void - * - * @codeCoverageIgnore - */ - public static function destroy() - { - foreach (static::$channels as $key => $instance) { - if (static::isChannel($key)) { - unset($instance); - static::$channels[$key] = null; - } - } - } - - public static function throwExistence(string $errorMessage): void - { - throw new \Error($errorMessage); - } - - public static function throwClosed(string $errorMessage): void - { - throw new \Error($errorMessage); - } - - /** - * @codeCoverageIgnore - */ - public static function throwIllegalValue(string $errorMessage): void - { - throw new \InvalidArgumentException($errorMessage); - } - - public static function make(string $name, int $capacity = -1): self - { - if (static::isChannel($name)) { - if (!static::$channels[$name]->isClose() && !static::$channels[$name]->isThread()) - return static::$channels[$name]; - - static::throwExistence(\sprintf('channel named %s already exists', $name)); - } - - return new static($capacity, $name, false); - } - - public static function open(string $name): self - { - if (static::isChannel($name)) - return static::$channels[$name]; - - if (isset(static::$channels[$name]) || static::$uid !== \getmyuid()) - return new static(-1, $name, false); - - static::throwExistence(\sprintf('channel named %s not found', $name)); - } - - /** - * Check for a valid `Channels` instance. - * - * @param string|int $name - * @return boolean - */ - public static function isChannel($name): bool - { - return isset(static::$channels[$name]) && static::$channels[$name] instanceof self; - } - - /** - * Set `Channels` to parent's `Thread` handle, current _thread_ `id`, and the PHP owners `UID`. - * - * @param Thread $handle Use by `send()`, `recv()`, and `kill()` - * @param string|integer $tid Thread ID - * @param integer $uid PHP owner's UID - * @param \UVLoop $uvLoop - * - * @return self - */ - public function setThread($handle, $tid, int $uid, \UVLoop $uvLoop = null): self - { - self::$thread = $handle; - self::$uid = $uid; - self::$uv = $uvLoop; - $this->tid = $tid; - - return $this; - } - - /** - * Check if `Channels` started by a **Thread**. - * @internal - * - * @return boolean - */ - public function isThread(): bool - { - return isset($this->tid) && \is_scalar($this->tid); - } - - /** - * Sets the `Channels` current state, Either `reading`, or `writing`. - * - * @param integer $status. - * @return self - */ - public function setState(string $status): self - { - $this->state = $status; - - return $this; - } - - /** - * Compare `Channels` current state to `status`. - * - * @param string $status. - * @return bool - */ - public function isState(string $status): bool - { - return $this->state === $status; - } - - /** - * Check if `channels` currently in a `send/recv` state. - * - * @return boolean - */ - public function isChanneling(): bool - { - return $this->state !== 'waiting'; - } - - /** - * Add a `send/recv` channel call. - * - * @return void - */ - protected function add(): void - { - $this->counter++; - } - - /** - * Remove a `send/recv` channel call. - * - * @return void - */ - protected function remove(): void - { - $this->counter--; - } - - /** - * Return total added `send/recv` channel calls. - * - * @return integer - */ - protected function count(): int - { - return $this->counter; - } - - public function close(): void - { - if ($this->isClosed()) - static::throwClosed(\sprintf('channel(%s) already closed', $this->name)); - - $this->open = false; - } - - /** - * Check if the channel has been closed yet. - */ - public function isClosed(): bool - { - return !$this->open; - } - - public function kill(): void - { - $this->__destruct(); - self::destroy(); - } - - public function send($value): void - { - if ($this->isClosed()) - static::throwClosed(\sprintf('channel(%s) closed', $this->name)); - - if (!$this->started) { - if ($this->isState('waiting')) - $this->setState('writing'); - - $this->started = true; - } - if ( - !$this->isThread() && null !== $value - && ($this->capacity > $this->buffered->count() || $this->capacity == -1) - && $this->type === 'buffered' - ) { - try { - $this->put($value); - } catch (\Error $e) { - static::throwIllegalValue($e->getMessage()); - } - } else { - if (self::$uid !== \getmyuid()) { - $this->kill(); - throw new \RuntimeException('`Channel->send()` only useable between two separate threads, not `uvLoop` main thread!' . \EOL); - } elseif (null !== $value) { - $this->__write($value); - } - } - } - - public function recv() - { - echo 'hereby'; - if (!$this->started) { - if ($this->isState('waiting')) - $this->setState('reading'); - - $this->started = true; - } - - if (!$this->isThread() && $this->type === 'buffered' && !$this->buffered->isEmpty()) { - try { - return $this->pop(); - } catch (\Error $e) { - static::throwIllegalValue($e->getMessage()); - } - } - - if ($this->isClosed()) - static::throwClosed(\sprintf('channel(%s) closed', $this->name)); - - if (self::$uid !== \getmyuid()) { - $this->kill(); - throw new \RuntimeException('`Channel->recv()` only useable between two separate threads, not `uvLoop` main thread!' . \EOL); - } - - $message = $this->__read(); - return $message; - } - - protected function __write($value) - { - $mLock = \uv_mutex_init(); - \uv_mutex_lock($mLock); - $this->add(); - \fwrite($this->output, \serializer($value, true)); - \uv_mutex_unlock($mLock); - unset($mLock); - do { - \usleep(1); - echo 'wait '; - } while ($this->count() !== 0); - echo 'working'; - } - - protected function __read() - { - $mLock = \uv_mutex_init(); - \uv_mutex_lock($mLock); - echo 'Got '; - $message = \trim(\fgets($this->input), \EOL); - if (!\is_null($message)) { - $message = \deserializer($message, true); - $this->remove(); - } - - \uv_mutex_unlock($mLock); - unset($mLock); - return $message; - } - - protected function put($value): void - { - $mLock = \uv_mutex_init(); - \uv_mutex_lock($mLock); - $this->buffered->enqueue($value); - \uv_mutex_unlock($mLock); - unset($mLock); - } - - protected function pop() - { - $mLock = \uv_mutex_init(); - \uv_mutex_lock($mLock); - $message = $this->buffered->dequeue(); - \uv_mutex_unlock($mLock); - unset($mLock); - return $message; - } -} diff --git a/Spawn/ChannelsInterface.php b/Spawn/ChannelsInterface.php deleted file mode 100644 index 31d4e36..0000000 --- a/Spawn/ChannelsInterface.php +++ /dev/null @@ -1,41 +0,0 @@ -isClosed) @@ -89,12 +71,6 @@ public function close() $this->errorCallbacks = []; $this->isYield = false; $this->isClosed = true; - if (!$this->hasLoop && self::$uv instanceof \UVLoop) { - @\uv_stop(self::$uv); - @\uv_run(self::$uv); - @\uv_loop_delete(self::$uv); - self::$uv = null; - } $this->hasLoop = null; } @@ -145,36 +121,8 @@ public function create($tid, callable $task, ...$args): self $async->handlers($tid); }); - \uv_queue_work(self::$uv, function () use (&$async, $task, $tid, &$args) { + \uv_queue_work(self::$uv, function () use (&$async, &$task, $tid, &$args) { include 'vendor/autoload.php'; - $lock = \mutex_lock(); - // @codeCoverageIgnoreStart - if (!empty($args)) { - foreach ($args as $channel) { - if ($async->isChannel($channel) || $channel instanceof Channels) { - if (!$channel->isThread()) - $channel->setThread($async, $tid, \getmyuid(), $async::getUv()); - elseif (!$async->isChanneled($tid)) - $async->setChanneled($tid, $channel); - break; - } elseif (\is_string($channel)) { - try { - $channel = Channels::open($channel); - if ($channel instanceof Channels || $async->isChannel($channel)) { - if (!$channel->isThread()) - $channel->setThread($async, $tid, \getmyuid(), $async::getUv()); - elseif (!$async->isChanneled($tid)) - $async->setChanneled($tid, $channel); - break; - } - } catch (\Throwable $e) { - } - } - } - } - // @codeCoverageIgnoreEnd - \mutex_unlock($lock); - try { $result = $task(...$args); $async->setResult($tid, $result); @@ -262,23 +210,6 @@ public function isEmpty(): bool return empty($this->threads); } - protected function setChanneled($tid, $channel) - { - if ($this->isRunning($tid)) - $this->status[$tid] = $channel; - } - - /** - * Tell if the referenced `tid` is executing a channel - * - * @param string|int $tid Thread ID - * @return bool - */ - public function isChanneled($tid): bool - { - return isset($this->status[$tid]) && \is_object($this->status[$tid]); - } - /** * Tell if the referenced `tid` is executing. * @@ -287,7 +218,7 @@ public function isChanneled($tid): bool */ public function isRunning($tid): bool { - return isset($this->status[$tid]) && (\is_string($this->status[$tid]) || \is_object($this->status[$tid])); + return isset($this->status[$tid]) && \is_string($this->status[$tid]); } /** diff --git a/tests/ZThreadChannelsTest.php b/tests/ZThreadChannelsTest.php deleted file mode 100644 index ed35ac8..0000000 --- a/tests/ZThreadChannelsTest.php +++ /dev/null @@ -1,367 +0,0 @@ -closure = $closure; - } - - public function call() - { - return ($this->closure)(); - } -} - -class ZThreadChannelsTest extends TestCase -{ - protected function setUp(): void - { - // if (!\IS_THREADED_UV) - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - - Channel::destroy(); - } - - public function testChannelMake() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $thread = new Thread(); - $channel = Channel::make("io"); - $thread->create(44, function ($channel) { - return $channel; - }, $channel)->then(function (Channel $output) { - var_dump($output); - })->join(); - - $this->expectOutputRegex('/[string(2) "io"]/'); - } - - public function testChannelOpen() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $thread = new Thread(); - $channel = Channel::make("io"); - - $thread->create(14, function ($channel) { - $channel = Channel::open($channel); - - return (string)$channel; - }, (string) $channel)->then(function (string $output) { - var_dump($output); - })->join(); - - $this->expectOutputRegex('/[string(2) "io"]/'); - } - - public function testChannelSendError() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $thread = new Thread(); - $channel = Channel::make("io"); - - $thread->create(34, function ($channel) use ($thread) { - $channel = Channel::open($channel); - - for ($count = 0; $count <= 10; $count++) { - $channel->send($count); - } - - $channel->send(false); - }, (string) $channel); - - $this->expectException(\RuntimeException::class); - $counter = 0; - while (($value = $channel->recv()) !== false) { - $counter++; - } - } - - public function testChannelSend() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $thread = new Thread(); - $channel = Channel::make("io"); - - $thread->create(35, function ($channel) { - $channel = Channel::open($channel); - - for ($count = 0; $count <= 10; $count++) { - $channel->send($count); - } - - $channel->send(false); - }, (string) $channel); - - $that = $this; - $thread->create(36, function ($channel) use ($that) { - $counter = 0; - while (($value = $channel->recv()) !== false) { - echo 'closure' . $value; - $that->assertEquals($value, $counter); - $counter++; - } - }, $channel); - - $thread->join(); - } - - - public function testChannelRecv() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("channel"); - - parallel(function ($channel) { - $data = $channel->recv(); - echo $data; - }, $channel); - - $this->expectOutputString('OK'); - $channel->send("OK"); - } - - public function testChannelDuplicateName() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $this->expectException(\Error::class); - $this->expectExceptionMessageMatches('/[channel named io already exists]/'); - - $channel = Channel::make("io"); - Channel::make("io"); - } - - public function testChannelNonExistentName() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $this->expectException(\Error::class); - $this->expectExceptionMessageMatches('/[channel named io not found]/'); - - Channel::open("io"); - } - - public function testChannelSendClosed() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $this->expectException(\Error::class); - $this->expectExceptionMessageMatches('/[channel(io) closed]/'); - - $channel = Channel::make("io"); - $channel->close(); - $channel->send(42); - } - - public function testChannelRecvClosed() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $this->expectException(\Error::class); - $this->expectExceptionMessageMatches('/[channel(io) closed]/'); - - $channel = Channel::make("io"); - $channel->close(); - $channel->recv(); - } - - public function testChannelCloseClosed() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $this->expectException(\Error::class); - $this->expectExceptionMessageMatches('/[channel(io) already closed]/'); - - $channel = Channel::make("io"); - $channel->close(); - $channel->close(); - } - - public function testChannelMakeArguments() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $this->expectException(\TypeError::class); - $this->expectExceptionMessageMatches('/[capacity may be -1 for unlimited, or a positive integer]/'); - - Channel::make("name", -2); - } - - public function testChannelReturnObjects() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("buffer", Channel::Infinite); - - $future = parallel(function ($channel) { - $data = $channel->recv(); - return $data; - }, $channel); - - $channel->send(new \DateTime); - $this->assertInstanceOf(\DateTime::class, $future->getResult()); - } - - public function testChannelSendClosure() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("function"); - - parallel(function ($channel) { - $data = $channel->recv(); - $data(); - }, $channel); - - $this->expectOutputString('closure!'); - $channel->send(function () { - echo 'closure!'; - }); - } - - public function testChannelClosureArrays() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("channel"); - - parallel(function ($channel) { - $data = $channel->recv(); - - ($data["closure"])(); - }, $channel); - - $this->expectOutputString('OK'); - $channel->send(["closure" => function () { - echo "OK"; - }]); - } - - public function testChannelInsideObjectProperties() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("channel"); - - parallel(function ($channel) { - $data = $channel->recv(); - - ($data->closure)(); - }, $channel); - - $std = new \stdClass; - $std->closure = function () { - echo "OK"; - }; - - $this->expectOutputString('OK'); - $channel->send($std); - } - - public function testChannelDelclaredInsideObjectProperties() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("channel"); - - parallel(function ($channel) { - $foo = $channel->recv(); - - $foo->call(); - }, $channel); - - $foo = new ZFoo(function () { - echo "OK"; - }); - - $this->expectOutputString('OK'); - $channel->send($foo); - } - - public function testChannelDrains() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $chan = Channel::make("hi", 10001); - $limit = 10; - - for ($i = 0; $i <= $limit; ++$i) { - $chan->send($i); - } - - $chan->close(); - - $counter = 0; - while (($value = $chan->recv()) > -1) { - $this->assertEquals($value, $counter); - $counter++; - - if ($value == $limit) { - break; - } - } - - $this->expectException(\Error::class); - $this->expectExceptionMessageMatches('/[channel(hi) closed]/'); - $chan->recv(); - } - - public function testParallelingInclude() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $future = \paralleling(function () { - return foo(); - }, \sprintf("%s/ChannelInclude.inc", __DIR__)); - - $this->expectOutputString('OK'); - echo $future->yielding()->current(); - } - - public function testParallelingNoInclude() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $future = \paralleling(function () { - echo 'foo'; - }, \sprintf("%s/nope.inc", __DIR__)); - - $this->expectOutputRegex('/[failed to open stream: No such file or directory]/'); - echo $future->yielding()->next(); - } - - public function testChannelRecvYield() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("channel"); - - $future = paralleling(function ($channel) { - $data = $channel->recv(); - echo $data; - }, null, $channel); - - $this->expectOutputString('OK'); - $channel->send("OK"); - - $this->assertSame($future->getChannel(), $channel); - } - - public function testChannelSendYield() - { - $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - $channel = Channel::make("io"); - - paralleling(function ($channel) { - $channel = Channel::open($channel); - - for ($count = 0; $count <= 10; $count++) { - $channel->send($count); - } - - $channel->send(false); - }, null, (string) $channel); - - $counter = 0; - while (($value = $channel->recv()) !== false) { - $this->assertEquals($value, $counter); - $counter++; - } - } -} diff --git a/tests/ZThreadMultiTest.php b/tests/ZThreadMultiTest.php deleted file mode 100644 index 423f6ba..0000000 --- a/tests/ZThreadMultiTest.php +++ /dev/null @@ -1,55 +0,0 @@ -markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); - } - - public function testIt_can_handle_multi() - { - $thread = new Thread(); - - $counter = 0; - - $i = 5; - $thread->create($i, function () { - return 2; - })->then(function (int $output) use (&$counter) { - $counter += $output; - }); - - $i++; - $thread->create($i, function () use ($i) { - return 2; - })->then(function (int $output) use (&$counter) { - $counter += $output; - }); - - $i++; - $thread->create($i, function () use ($i) { - usleep(20000); - return 2; - })->then(function (int $output) use (&$counter) { - $counter += $output; - }); - - $thread->join(6); - $this->assertCount(2, $thread->getSuccess()); - $this->assertEquals(2, $thread->getResult(6)); - $this->assertEquals(6, $counter); - - $thread->join(); - $this->assertCount(3, $thread->getSuccess()); - $this->assertEquals(18, $counter); - - $thread->close(); - } -} diff --git a/tests/ZThreadTest.php b/tests/ZThreadTest.php index 3379aa9..983e581 100644 --- a/tests/ZThreadTest.php +++ b/tests/ZThreadTest.php @@ -14,6 +14,44 @@ protected function setUp(): void $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); } + public function testIt_can_handle_multi() + { + $this->markTestSkipped('Test skipped "uv_loop_new" and "PHP ZTS" missing. currently buggy - zend_mm_heap corrupted'); + $thread = new Thread(); + + $counter = 0; + + $thread->create(5, function () { + return 2; + })->then(function (int $output) use (&$counter) { + $counter += $output; + }); + + $thread->create(6, function () { + return 2; + })->then(function (int $output) use (&$counter) { + $counter += $output; + }); + + $thread->create(7, function () { + usleep(20000); + return 2; + })->then(function (int $output) use (&$counter) { + $counter += $output; + }); + + $thread->join(6); + $this->assertCount(2, $thread->getSuccess()); + $this->assertEquals(2, $thread->getResult(6)); + $this->assertEquals(6, $counter); + + $thread->join(); + $this->assertCount(3, $thread->getSuccess()); + $this->assertEquals(18, $counter); + + $thread->close(); + } + public function testIt_can_create_and_return_results() { $thread = new Thread();