4

I'm running a CGI program in non-threaded PHP, a little exercise in artificial life and evolution. Organisms have a genome and an interpreter, which causes them to perform certain operations, including moving about and interacting with one another in a shared World map. Currently I'm maintaining a poor semblance of threading by using multiple PHP processes interacting via a MySQL database, but I want to rewrite the code so that it uses pthreads to run continuously in a single thread, not necessarily using a database (although I'd probably want to keep it for reporting).

I've been browsing through asked and answered questions and code examples on github, but haven't managed to find anything that - as far as I can tell - addresses the thing I want. As I am not exactly a genius OOP coder and am entirely new to writing threaded code, especially in PHP, my questions will be fairly broad.

I've tried to narrow down the scope of my questions a bit by writing code showing what I'm trying to do, but it may still be too broad. I'd appreciate any advice on how to narrow it down further.

My questions about the code below:

  • How do I get an Organism to act on a shared World object so that the changes in the World object are communicated to all threads, avoiding conflicts and maintaining coherence?
  • Given that the population size is ultimately variable, is there a way to make the references to the Organisms part of the World object (ie. $world->organisms), and have World be able to create new Organisms, as shown in below (faulty) code?
  • Given that I ultimately want to create a population of hundreds of Organisms, do you have any pointers on limiting the number of active threads (ie. limiting memory/cpu usage) while still maintaining consistency?

Below code (doesn't run, of course, but) illustrates what I am seeking to achieve:

/*
 * Shared object containing a map of the world and 
 * methods for getting/setting coordinates
 */
class World extends Thread
{
    public $map;
    public $sx;
    public $sy;
    public $organisms;

    // set all map coords to 0
    public function __construct($sx, $sy, $p)
    {
        $map = array();
        for( $i = 0; $i < $sx; $i++ )
        {
            $map[$i] = array();
            for( $j = 0; $j < $sy; $j++ )
            {
                $map[$i][$j] = 0;
            }
        }
        $this->map = $map;
        $this->sx = $sx;
        $this->sy = $sy;

        // create and start organism threads
        $this->organisms = array();
        for( $i = 0; $i < $p; $i++ )
        {
            // this won't work because threaded objects created
            // within a thread scope that have no external references
            // are destroyed in the constructor
            $this->makeOrganism($i+1, $i+1, $i+1);
        }
    }

    // produces a new organism, adds to world population
    public function makeOrganism($x, $y, $value)
    {
        if( $x < 1 || $x > $this->sx ) return false;
        if( $y < 1 || $y > $this->sy ) return false;
        if( $this->getMap($x, $y) != 0 ) return false;

        echo "creating new organism $value\n";
        $organisms = $this->organisms;
        // doesn't work because the world data is decoupled in the new thread
        $organisms[] = new Organism($this, $x, $y, $value);
        $this->organisms = $organisms;

        return true;
    }

    // assumes valid coords
    public function setMap($x, $y, $value)
    {    
        return $this->map[$x-1][$y-1] = $value;
    }

    // assumes valid coords
    public function getMap($x, $y)
    {
        return $this->map[$x-1][$y-1];
    }

    public function getSX()
    {
        return $this->sx;
    }

    public function getSY()
    {
        return $this->sy;
    }

    public function run() 
    {
        for( $i = 0; $i < count($this->organisms); $i++ )
        {
            echo "starting organism ", $this->value, "\n";
            $this->organisms[$i]->start();
        }
    }
}

/*
 * Autonomously running agent accessing shared World map
 */
class Organism extends Thread
{
    public $value;
    public $world;
    public $x;
    public $y;

    public function __construct(World $world, $x, $y, $value)
    {
        $this->world = $world;
        $this->value = $value;
        $this->x = $x;
        $this->y = $y;
        // assume coordinates are empty
        $this->world->setMap($x, $y, $value);
    }

    // try to move organism by $dx, $dy
    public function move($dx, $dy)
    {
        $x = $this->x + $dx;
        $y = $this->y + $dy;
        if( $x < 1 || $x > $this->world->getSX() ) return false;
        if( $y < 1 || $y > $this->world->getSY() ) return false;
        if( $this->world->getMap($x, $y) != 0 ) return false;

        $this->world->setMap($x, $y, $this->value);
        $this->world->setMap($this->x, $this->y, 0);
        $this->x = $x;
        $this->y = $y;
        return true;
    }

    public function getValue()
    {
        return $this->value;
    }

    public function run()
    {
        // infinite loop; organisms move randomly about until they die
        while( true )
        {
            echo "running organism ", $this->getValue(), "\n";
            // this should operate on the shared object World, 
            // maintaining consistency and avoiding conflicts between threads
            $dx = rand(-1, 1);
            $dy = rand(-1, 1);
            $this->move($dx, $dy);

            // occasionally add an organism to the population by cloning this one
            if( rand(0, 100) > 95 )
            {
                $this->world->makeOrganism($this->x+1, $this->y+1, $this->value+100);
            }

            // wait random interval, organisms are
            // not expected to move all at the same time
            $this->wait(1000 + rand(500, 1500));
        }
    }
}

// initialize shared object
$world = new World(50, 50, 50);
$world->start();
$world->join();
Gralgrathor
  • 216
  • 1
  • 8

1 Answers1

2

I'm going to answer for pthreads v3, PHP7.

Please don't use pthreads v2 for new projects, v3 is far superior.

How do I get an Organism to act on a shared World object so that the changes in the World object are communicated to all threads, avoiding conflicts and maintaining coherence?

The following code creates a bunch of threads that manipulate a shared object:

<?php
class Test extends Thread {

    public function __construct(Threaded $shared) {
        $this->shared = $shared;
    }

    public function run() {
        $this->shared[] = $this->getThreadId();
    }
}

$shared = new Threaded();
$tests = [];
for ($i = 0; $i<20; $i++) {
    $tests[$i] = 
        new Test($shared);
    $tests[$i]->start();
}

foreach ($tests as $test)
    $test->join();

var_dump($shared);
?>

Will yield something similar to:

object(Threaded)#1 (20) {
  [0]=>
  int(140322714146560)
  [1]=>
  int(140322703144704)
  [2]=>
  int(140322621355776)
  [3]=>
  int(140322612963072)
  [4]=>
  int(140322604570368)
  [5]=>
  int(140322596177664)
  [6]=>
  int(140322587784960)
  [7]=>
  int(140322579392256)
  [8]=>
  int(140322570999552)
  [9]=>
  int(140322487138048)
  [10]=>
  int(140322478745344)
  [11]=>
  int(140322470352640)
  [12]=>
  int(140322461959936)
  [13]=>
  int(140322453567232)
  [14]=>
  int(140322445174528)
  [15]=>
  int(140322436781824)
  [16]=>
  int(140322428389120)
  [17]=>
  int(140322419996416)
  [18]=>
  int(140322411603712)
  [19]=>
  int(140322403211008)
}

It is not an accident that the number of elements is consistent:

$this->shared[] = $this->getThreadId();

When this is executed, safety is provided for you by pthreads.

Any Thread with a reference to the Threaded $shared object can manipulate it.

Consistency and safety are two different things, consider the following code:

<?php
class Test extends Thread {

    public function __construct(Threaded $shared) {
        $this->shared = $shared;
    }

    public function run() {
        if (!isset($this->shared[0])) {
            $this->shared[0] = $this->getThreadId();
        }
    }

    private $shared;
}

$shared = new Threaded();
$tests  = [];

for ($i = 0; $i < 16; $i++) {
    $tests[$i] = 
        new Test($shared);
    $tests[$i]->start();
}

foreach ($tests as $test)
    $test->join();
?>

You might expect only one Thread to travel this path:

$this->shared[0] = $this->getThreadId();

But, there is no guarantee of that. Since no lock is held between the call to isset and the statement above, many threads are free to travel the path concurrently.

Replacing the run method of Test with the following code will ensure consistency:

    public function run() {
        $this->shared->synchronized(function(){
            if (!isset($this->shared[0])) {
                $this->shared[0] = $this->getThreadId();
            }
        });
    }

Given that the population size is ultimately variable, is there a way to make the references to the Organisms part of the World object (ie. $world->organisms), and have World be able to create new Organisms, as shown in below (faulty) code?

This sounds like it violates basic principles of SOLID. Regardless, this is not a good thing to chase after.

It sounds like Organisms should belong to the main process, and so need to be constructed there and passed into the Thread or Worker or whatever.

They need to belong to the main process because they might end up in more than one Thread.

Given that I ultimately want to create a population of hundreds of Organisms, do you have any pointers on limiting the number of active threads (ie. limiting memory/cpu usage) while still maintaining consistency?

Using the Pool implementation provided by pthreads.

Follows is some code:

<?php
class Organisms extends Volatile {}

class World extends Threaded {

    public function __construct(Organisms $organisms, Volatile $grid) {
        $this->organisms = $organisms;
        $this->grid = $grid;
    }

    public function populate($organism, int $x, int $y) : bool {
        $reference = $this->getGridReference($x, $y);

        return $this->grid->synchronized(function() use($organism, $reference) {
            if (isset($this->grid[$reference]))
                return false;
            return (bool) $this->grid[$reference] = $organism;
        });
    }

    private function getGridReference(int $x, int $y) {
        return sprintf("%dx%d", $x, $y);
    }

    public function getOrganisms() { return $this->organisms; }

    private $organisms;
}

class Organism extends Threaded {

    public function __construct(World $world) {
        $this->world = $world;
    }

    public function setPosition(int $x, int $y) {
        $this->x = $x;
        $this->y = $y;
    }

    public function getWorld() { return $this->world; }

    private $world;
    private $x = -1;
    private $y = -1;
}

class OrganismPopulateTask extends Threaded {

    public function __construct(World $world, Organism $organism, int $x, int $y) {
        $this->world = $world;
        $this->organism = $organism;
        $this->x = $x;
        $this->y = $y;
    }

    public function run() {
        if ($this->world->populate(
            (object) $this->organism, $this->x, $this->y)) {
                $this->organism->setPosition($this->x, $this->y);
        }
    }

    private $world;
    private $organism;
    private $x;
    private $y;
}

$organisms = new Organisms();
$grid = new Volatile();
$world = new World($organisms, $grid);
$pool = new Pool(16);

$organisms[] = new Organism($world);
$organisms[] = new Organism($world);

$pool
    ->submit(new OrganismPopulateTask($world, $organisms[0], 10, 10));
$pool   
    ->submit(new OrganismPopulateTask($world, $organisms[1], 10, 10));

$pool->shutdown();

var_dump($world);
?>

Will yield:

object(World)#3 (2) {
  ["organisms"]=>
  object(Organisms)#1 (2) {
    [0]=>
    object(Organism)#5 (3) {
      ["world"]=>
      *RECURSION*
      ["x"]=>
      int(10)
      ["y"]=>
      int(10)
    }
    [1]=>
    object(Organism)#6 (3) {
      ["world"]=>
      *RECURSION*
      ["x"]=>
      int(-1)
      ["y"]=>
      int(-1)
    }
  }
  ["grid"]=>
  object(Volatile)#2 (1) {
    ["10x10"]=>
    object(Organism)#5 (3) {
      ["world"]=>
      *RECURSION*
      ["x"]=>
      int(10)
      ["y"]=>
      int(10)
    }
  }
}

Note: This uses features included in v3.1.2

Most of it should be self explanatory, the explicit casts are to avoid exceptions caused by trying to connect to objects that have gone away.

The main thing to notice is that each "action" is treated as a "task" and submitted to the Pool.

Joe Watkins
  • 17,032
  • 5
  • 41
  • 62
  • Thanks for answering, despite the vagueness of the question, this is a lot of help. It does require me to figure out how to build PHP7+pthreads, but that shouldn't be too much of a hassle. All I need to do now is to figure out how to make any member of $organisms be able to instruct the main thread to pool new queue items, such as _OrganismSpawnTask($world, $organism)_ or anything else that means interaction between Organisms and their environment. I'll go read up on SOLID, try some code and come back with more specific questions when I get stuck again. Thanks! – Gralgrathor Nov 22 '15 at 19:19
  • You can't share Pools. Each Worker could have a Pool of it's own ... a small one would be advisable ... – Joe Watkins Nov 22 '15 at 20:43
  • I'm thinking (not very clearly, mind you; I'm only halfway through Uncle Bob's Craftsman series by now) that perhaps a queuing mechanism using a database or simple array could be used for instructing the main thread to run a particular task on a particular set of organisms. That way there's no direct violation of directionality. The problem is that I'm trying to simulate the messiness of the real world with its chaotic dependencies in an environment more amenable to strict hierarchies. I'm not quite sure how to adjust my way of thinking accordingly. – Gralgrathor Nov 22 '15 at 21:00