├── .gitignore ├── src ├── Collectable.php ├── Volatile.php ├── Thread.php ├── Pool.php ├── Worker.php └── Threaded.php ├── .travis.yml ├── phpunit.xml.dist ├── tests ├── VolatileTest.php ├── WorkerTest.php ├── ThreadTest.php ├── PoolTest.php └── ThreadedTest.php ├── composer.json ├── example.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | phpunit.xml 4 | -------------------------------------------------------------------------------- /src/Collectable.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | 12 | ./src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Volatile.php: -------------------------------------------------------------------------------- 1 | data); 8 | } 9 | 10 | if (is_array($value)) { 11 | $safety = 12 | new Volatile(); 13 | $safety->merge( 14 | $this->convertToVolatile($value)); 15 | $value = $safety; 16 | } 17 | 18 | return $this->data[$offset] = $value; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/VolatileTest.php: -------------------------------------------------------------------------------- 1 | member = new Threaded(); 9 | $volatile->member = new Threaded(); 10 | } 11 | 12 | public function testVolatileArrays() { 13 | $threaded = new Threaded(); 14 | $threaded->test = [ 15 | "hello" => ["world"]]; 16 | 17 | $this->assertTrue($threaded["test"] instanceof Volatile); 18 | $this->assertTrue($threaded["test"]["hello"] instanceof Volatile); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "krakjoe/pthreads-polyfill", 3 | "type": "library", 4 | "description": "A polyfill for pthreads", 5 | "keywords": ["pthreads", "polyfill"], 6 | "homepage": "http://pthreads.org/", 7 | "license": "PHP", 8 | "authors": [ 9 | { 10 | "name": "Joe Watkins", 11 | "email": "krakjoe@php.net" 12 | } 13 | ], 14 | "autoload": { 15 | "files": [ 16 | "src/Collectable.php", 17 | "src/Threaded.php", 18 | "src/Volatile.php", 19 | "src/Thread.php", 20 | "src/Worker.php", 21 | "src/Pool.php" 22 | ] 23 | }, 24 | "minimum-stability": "dev", 25 | "require-dev": { 26 | "phpunit/phpunit": "^5" 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | submit(new class extends Threaded implements Collectable { 13 | private $garbage = false; 14 | 15 | public function run() { 16 | echo "Hello World\n"; 17 | $this->garbage = true; 18 | } 19 | 20 | public function isGarbage(): bool { 21 | return $this->garbage; 22 | } 23 | }); 24 | 25 | while ($pool->collect(function($task){ 26 | return $task->isGarbage(); 27 | })) continue; 28 | 29 | $pool->shutdown(); 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pthreads-polyfill 2 | 3 | [![Build Status](https://travis-ci.org/krakjoe/pthreads-polyfill.svg)](https://travis-ci.org/krakjoe/pthreads-polyfill) 4 | 5 | *pthreads-polyfill* aims to satisfy the API requirements of *pthreads*, such that code written to depend on *pthreads* will work when *pthreads* is not, or can not be loaded. 6 | 7 | *pthreads-polyfill* does not implement the same execution model, for obvious reasons, and has no external dependencies. 8 | 9 | *pthreads-polyfill* will fill for v2 or v3, however behaviour is consistent with v3, which is the version new projects should target. 10 | 11 | Testing 12 | ------ 13 | 14 | *pthreads-polyfill* is distributed with some unit tests, these tests should pass with and without *pthreads* loaded. 15 | 16 | Testing *pthreads-polyfill* 17 | 18 | phpunit tests 19 | 20 | If *pthreads* is loaded by your configuration the polyfill will not be used. 21 | 22 | Testing code coverage for *pthreads-polyfill* 23 | 24 | phpdbg -nqrr vendor/bin/phpunit tests --coverage-text 25 | -------------------------------------------------------------------------------- /src/Thread.php: -------------------------------------------------------------------------------- 1 | state & THREAD::STARTED); } 6 | public function isJoined() { return (bool) ($this->state & THREAD::JOINED); } 7 | public function kill() { 8 | $this->state |= THREAD::ERROR; 9 | return true; 10 | } 11 | 12 | public static function getCurrentThreadId() { return 1; } 13 | public function getThreadId() { return 1; } 14 | 15 | public function start() { 16 | if ($this->state & THREAD::STARTED) { 17 | throw new \RuntimeException(); 18 | } 19 | 20 | $this->state |= THREAD::STARTED; 21 | $this->state |= THREAD::RUNNING; 22 | 23 | try { 24 | $this->run(); 25 | } catch(Exception $t) { 26 | $this->state |= THREAD::ERROR; 27 | } 28 | 29 | $this->state &= ~THREAD::RUNNING; 30 | return true; 31 | } 32 | 33 | public function join() { 34 | if ($this->state & THREAD::JOINED) { 35 | throw new \RuntimeException(); 36 | } 37 | 38 | $this->state |= THREAD::JOINED; 39 | return true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/WorkerTest.php: -------------------------------------------------------------------------------- 1 | synchronized(function() { 5 | $this->hasWorker = 6 | $this->worker instanceof Worker; 7 | $this->notify(); 8 | }); 9 | } 10 | } 11 | 12 | class WorkerTest extends PHPUnit_Framework_TestCase { 13 | 14 | public function testWorkerStack() { 15 | $worker = new Worker(); 16 | $work = new WorkerTestWork(); 17 | $worker->start(); 18 | $worker->stack($work); 19 | $worker->shutdown(); 20 | 21 | $this->assertTrue($work->hasWorker); 22 | } 23 | 24 | public function testWorkerGc() { 25 | $worker = new Worker(); 26 | $work = new WorkerTestWork(); 27 | $worker->start(); 28 | $worker->stack($work); 29 | $worker->shutdown(); 30 | $this->assertEquals(1, $worker->collect(function ($task){ 31 | return false; 32 | })); 33 | $this->assertEquals(0, $worker->collect(function ($task){ 34 | return $task->isGarbage(); 35 | })); 36 | } 37 | 38 | public function testGetStacked() 39 | { 40 | $worker = new Worker(); 41 | $work = new WorkerTestWork(); 42 | 43 | $worker->stack($work); 44 | $this->assertEquals(1, $worker->getStacked()); 45 | 46 | $worker->stack($work); 47 | $this->assertEquals(2, $worker->getStacked()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Pool.php: -------------------------------------------------------------------------------- 1 | size = $size; 8 | $this->clazz = $class; 9 | $this->ctor = $ctor; 10 | } 11 | 12 | public function submit(Threaded $collectable) { 13 | if ($this->last > $this->size) { 14 | $this->last = 0; 15 | } 16 | 17 | if (!isset($this->workers[$this->last])) { 18 | $this->workers[$this->last] = 19 | new $this->clazz(...$this->ctor); 20 | $this->workers[$this->last]->start(); 21 | } 22 | 23 | $this->workers[$this->last++]->stack($collectable); 24 | } 25 | 26 | public function submitTo($worker, Threaded $collectable) { 27 | if (isset($this->workers[$worker])) { 28 | $this->workers[$worker]->stack($collectable); 29 | } 30 | } 31 | 32 | public function collect(Closure $collector = null) { 33 | $total = 0; 34 | foreach ($this->workers as $worker) 35 | $total += $worker->collect($collector); 36 | return $total; 37 | } 38 | 39 | public function resize($size) { 40 | if ($size < $this->size) { 41 | while ($this->size > $size) { 42 | if (isset($this->workers[$this->size-1])) 43 | $this->workers[$this->size-1]->shutdown(); 44 | unset($this->workers[$this->size-1]); 45 | $this->size--; 46 | } 47 | } 48 | } 49 | 50 | public function shutdown() { 51 | $this->workers = null; 52 | } 53 | 54 | protected $workers; 55 | protected $size; 56 | protected $last; 57 | protected $clazz; 58 | protected $ctor; 59 | } 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/Worker.php: -------------------------------------------------------------------------------- 1 | gc as $idx => $collectable) { 7 | if ($collector) { 8 | if ($collector($collectable)) { 9 | unset($this->gc[$idx]); 10 | } 11 | } else { 12 | if ($this->collector($collectable)) { 13 | unset($this->gc[$idx]); 14 | } 15 | } 16 | } 17 | 18 | return count($this->gc) + count($this->stack); 19 | } 20 | public function collector(Collectable $collectable) { return $collectable->isGarbage(); } 21 | public function shutdown() { return $this->join(); } 22 | public function isShutdown() { return $this->isJoined(); } 23 | public function getStacked() { return count($this->stack); } 24 | public function unstack() { return array_shift($this->stack); } 25 | public function stack(Threaded $collectable) { 26 | $this->stack[] = $collectable; 27 | if ($this->isStarted()) { 28 | $this->runCollectable(count($this->stack)-1, $collectable); 29 | } 30 | } 31 | 32 | public function run() { 33 | foreach ($this->stack as $idx => $collectable) { 34 | $this 35 | ->runCollectable($idx, $collectable); 36 | } 37 | } 38 | 39 | private function runCollectable($idx, Collectable $collectable) { 40 | $collectable->worker = $this; 41 | $collectable->state |= THREAD::RUNNING; 42 | $collectable->run(); 43 | $collectable->state &= ~THREAD::RUNNING; 44 | $this->gc[] = $collectable; 45 | unset($this->stack[$idx]); 46 | } 47 | 48 | private $stack = []; 49 | private $gc = []; 50 | } 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/ThreadTest.php: -------------------------------------------------------------------------------- 1 | member = "something"; 6 | $this->running = 7 | $this->isRunning(); 8 | } 9 | 10 | public $member; 11 | } 12 | 13 | class ThreadTest extends PHPUnit_Framework_TestCase { 14 | 15 | public function testThreadStartAndJoin() { 16 | $thread = new TestThread(); 17 | $this->assertTrue($thread->start()); 18 | $this->assertTrue($thread->isStarted()); 19 | $this->assertTrue($thread->join()); 20 | $this->assertTrue($thread->isJoined()); 21 | $this->assertEquals("something", $thread->member); 22 | } 23 | 24 | /** 25 | * @expectedException RuntimeException 26 | */ 27 | public function testThreadAlreadyStarted() { 28 | $thread = new Thread(); 29 | $this->assertTrue($thread->start()); 30 | $this->assertFalse($thread->start()); 31 | } 32 | 33 | /** 34 | * @expectedException RuntimeException 35 | */ 36 | public function testThreadAlreadyJoined() { 37 | $thread = new Thread(); 38 | $this->assertTrue($thread->start()); 39 | $this->assertTrue($thread->join()); 40 | $this->assertFalse($thread->join()); 41 | } 42 | 43 | public function testThreadIsRunning() { 44 | $thread = new TestThread(); 45 | $this->assertTrue($thread->start()); 46 | $this->assertTrue($thread->join()); 47 | $this->assertTrue((bool) $thread->running); 48 | } 49 | 50 | public function testThreadIds() { 51 | $thread = new Thread(); 52 | $this->assertInternalType("int", $thread->getThreadId()); 53 | $this->assertInternalType("int", Thread::getCurrentThreadId()); 54 | } 55 | } 56 | ?> 57 | -------------------------------------------------------------------------------- /tests/PoolTest.php: -------------------------------------------------------------------------------- 1 | size; 5 | } 6 | } 7 | 8 | class PoolTestWorker extends Worker { 9 | public function __construct($std, Threaded $threaded) { 10 | $this->std = $std; 11 | $this->threaded = $threaded; 12 | } 13 | } 14 | 15 | class PoolTestWork extends Threaded implements Collectable { 16 | public function run() { 17 | $this->hasWorker = 18 | $this->worker instanceof Worker; 19 | $this->hasWorkerStd = 20 | $this->worker->std instanceof stdClass; 21 | $this->hasWorkerThreaded = 22 | $this->worker->threaded instanceof Threaded; 23 | $this->setGarbage(); 24 | } 25 | 26 | public function isGarbage() : bool { return true; } 27 | private function setGarbage() { $this->garbage = true; } 28 | private $garbage = false; 29 | } 30 | 31 | class PoolTestSync extends Threaded implements Collectable { 32 | public function run() { 33 | $this->synchronized(function(){ 34 | $this->finished = true; 35 | $this->notify(); 36 | }); 37 | $this->setGarbage(); 38 | } 39 | 40 | public function isGarbage() : bool { return true; } 41 | private function setGarbage() { $this->garbage = true; } 42 | private $garbage = false; 43 | } 44 | 45 | class PoolTest extends PHPUnit_Framework_TestCase { 46 | 47 | public function testPool() { 48 | $pool = new Pool(1, PoolTestWorker::class, [new stdClass, new Threaded]); 49 | $work = new PoolTestWork(); 50 | $pool->submit($work); 51 | while (@$i++<2) { 52 | $pool->submit(new PoolTestWork()); # nothing to assert, no exceptions please 53 | } 54 | $pool->submitTo(0, new PoolTestWork()); # nothing to assert, no exceptions please 55 | $pool->shutdown(); 56 | 57 | $this->assertTrue($work->hasWorker); 58 | $this->assertTrue($work->hasWorkerStd); 59 | $this->assertTrue($work->hasWorkerThreaded); 60 | } 61 | 62 | 63 | public function testPoolGc() { 64 | $pool = new Pool(1, PoolTestWorker::class, [new stdClass, new Threaded]); 65 | $work = new PoolTestWork(); 66 | $pool->submit($work); 67 | while (@$i++<2) { 68 | $pool->submit(new PoolTestWork()); # nothing to assert, no exceptions please 69 | } 70 | $pool->submitTo(0, new PoolTestWork()); # nothing to assert, no exceptions please 71 | 72 | /* synchronize with pool */ 73 | $sync = new PoolTestSync(); 74 | $pool->submit($sync); 75 | $sync->synchronized(function($sync){ 76 | if (!$sync->finished) 77 | $sync->wait(); 78 | }, $sync); 79 | 80 | $pool->collect(function($task){ 81 | $this->assertTrue($task->isGarbage()); 82 | return true; 83 | }); 84 | $pool->shutdown(); 85 | } 86 | 87 | public function testPoolResize() { 88 | $pool = new PoolTestPool(2, PoolTestWorker::class, [new stdClass, new Threaded]); 89 | $pool->submit(new PoolTestWork()); 90 | $pool->submit(new PoolTestWork()); 91 | $pool->resize(1); 92 | $this->assertEquals(1, $pool->getSize()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/ThreadedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("something", $threaded[0]); 7 | } 8 | 9 | public function testThreadedOverloadSetUnset() { 10 | $threaded = new Threaded(); 11 | $threaded->something = "something"; 12 | $this->assertEquals("something", $threaded->something); 13 | } 14 | 15 | public function testThreadedArrayAccessExistsUnset() { 16 | $threaded = new Threaded(); 17 | $threaded[] = "something"; 18 | $this->assertTrue(isset($threaded[0])); 19 | unset($threaded[0]); 20 | $this->assertFalse(isset($threaded[0])); 21 | } 22 | 23 | public function testThreadedOverloadExistsUnset() { 24 | $threaded = new Threaded(); 25 | $threaded->something = "something"; 26 | $this->assertTrue(isset($threaded->something)); 27 | unset($threaded->something); 28 | $this->assertFalse(isset($threaded->something)); 29 | } 30 | 31 | public function testThreadedCountable() { 32 | $threaded = new Threaded(); 33 | $threaded[] = "something"; 34 | $this->assertEquals(1, count($threaded)); 35 | } 36 | 37 | public function testThreadedShift() { 38 | $threaded = new Threaded(); 39 | $threaded[] = "something"; 40 | $threaded[] = "else"; 41 | $this->assertEquals("something", $threaded->shift()); 42 | $this->assertEquals(1, count($threaded)); 43 | } 44 | 45 | public function testThreadedChunk() { 46 | $threaded = new Threaded(); 47 | while (count($threaded) < 10) { 48 | $threaded[] = count($threaded); 49 | } 50 | $this->assertEquals([0, 1, 2, 3, 4], $threaded->chunk(5)); 51 | $this->assertEquals(5, count($threaded)); 52 | } 53 | 54 | public function testThreadedPop() { 55 | $threaded = new Threaded(); 56 | $threaded[] = "something"; 57 | $threaded[] = "else"; 58 | $this->assertEquals("else", $threaded->pop()); 59 | $this->assertEquals(1, count($threaded)); 60 | } 61 | 62 | public function testThreadedMerge() { 63 | $threaded = new Threaded(); 64 | $threaded->merge([0, 1, 2, 3, 4]); 65 | $this->assertEquals(5, count($threaded)); 66 | } 67 | 68 | public function testThreadedIterator() { 69 | $threaded = new Threaded(); 70 | while (count($threaded) < 10) { 71 | $threaded[] = count($threaded); 72 | } 73 | 74 | foreach ($threaded as $idx => $value) 75 | $this->assertEquals($idx, $value); 76 | } 77 | 78 | public function testThreadedSynchronized() { 79 | $threaded = new Threaded(); 80 | $threaded->synchronized(function($self, ...$args){ 81 | $self->assertEquals([1, 2, 3, 4, 5], $args); 82 | }, $this, 1, 2 ,3 ,4 , 5); 83 | } 84 | 85 | /** 86 | * @expectedException RuntimeException 87 | */ 88 | public function testThreadedImmutabilityWrite() { 89 | $threaded = new Threaded(); 90 | $threaded->test = new Threaded(); 91 | $threaded->test = new Threaded(); 92 | } 93 | 94 | /** 95 | * @expectedException RuntimeException 96 | */ 97 | public function testThreadedImmutabilityUnset() { 98 | $threaded = new Threaded(); 99 | $threaded->test = new Threaded(); 100 | unset($threaded->test); 101 | } 102 | } 103 | ?> 104 | -------------------------------------------------------------------------------- /src/Threaded.php: -------------------------------------------------------------------------------- 1 | __set($offset, $value); 13 | } 14 | 15 | public function offsetGet($offset) { 16 | return $this->__get($offset); 17 | } 18 | 19 | public function offsetUnset($offset) { 20 | $this->__unset($offset); 21 | } 22 | 23 | public function offsetExists($offset) { 24 | return $this->__isset($offset); 25 | } 26 | 27 | public function count() { 28 | return count($this->data); 29 | } 30 | 31 | public function getIterator() { 32 | return new ArrayIterator($this->data); 33 | } 34 | 35 | public function __set($offset, $value) { 36 | if ($offset === null) { 37 | $offset = count($this->data); 38 | } 39 | 40 | if (!$this instanceof Volatile) { 41 | if (isset($this->data[$offset]) && 42 | $this->data[$offset] instanceof Threaded) { 43 | throw new \RuntimeException(); 44 | } 45 | } 46 | 47 | if (is_array($value)) { 48 | $safety = 49 | new Volatile(); 50 | $safety->merge( 51 | $this->convertToVolatile($value)); 52 | $value = $safety; 53 | } 54 | 55 | return $this->data[$offset] = $value; 56 | } 57 | 58 | public function __get($offset) { 59 | return $this->data[$offset]; 60 | } 61 | 62 | public function __isset($offset) { 63 | return isset($this->data[$offset]); 64 | } 65 | 66 | public function __unset($offset) { 67 | if (!$this instanceof Volatile) { 68 | if (isset($this->data[$offset]) && $this->data[$offset] instanceof Threaded) { 69 | throw new \RuntimeException(); 70 | } 71 | } 72 | unset($this->data[$offset]); 73 | } 74 | 75 | public function shift() { 76 | return array_shift($this->data); 77 | } 78 | 79 | public function chunk($size) { 80 | $chunk = []; 81 | while (count($chunk) < $size) { 82 | $chunk[] = $this->shift(); 83 | } 84 | return $chunk; 85 | } 86 | 87 | public function pop() { 88 | return array_pop($this->data); 89 | } 90 | 91 | public function merge($merge) { 92 | foreach ($merge as $k => $v) { 93 | $this->data[$k] = $v; 94 | } 95 | } 96 | 97 | public function wait($timeout = 0) { 98 | return true; 99 | } 100 | 101 | public function notify() { 102 | return true; 103 | } 104 | 105 | public function synchronized(Closure $closure, ... $args) { 106 | return $closure(...$args); 107 | } 108 | 109 | public function isRunning() { 110 | return $this->state & THREAD::RUNNING; 111 | } 112 | 113 | public function isTerminated() { 114 | return $this->state & THREAD::ERROR; 115 | } 116 | 117 | public static function extend($class) { return true; } 118 | 119 | public function addRef() {} 120 | public function delRef() {} 121 | public function getRefCount() {} 122 | 123 | public function lock() { return true; } 124 | public function unlock() { return true; } 125 | public function isWaiting() { return false; } 126 | 127 | public function run() {} 128 | 129 | public function isGarbage() { return true; } 130 | 131 | private function convertToVolatile($value) { 132 | if (is_array($value)) { 133 | foreach ($value as $k => $v) { 134 | if (is_array($v)) { 135 | $value[$k] = 136 | new Volatile(); 137 | $value[$k]->merge( 138 | $this->convertToVolatile($v)); 139 | } 140 | } 141 | } 142 | return $value; 143 | } 144 | 145 | protected $data; 146 | protected $state; 147 | } 148 | } 149 | --------------------------------------------------------------------------------