Advanced discussion: Shared Memory

After looking for the best way to handle some “multi threading”, writing some of my own code, I actually happened upon a nice little class, one I’ll be sharing in full once I have fixed it’s obvious mistakes.

The guy who wrote this took excellent care with his multi threading portion, its absolutely beautiful, except he could have gone farther with it. He even wrote in some sh_mop classes to help out, which unfortunately are lacking, here’s where I’m at with the alterations so far on his shared memory class:


    private $shmId;
    private $readCache = NULL;

    public function __construct() {
        if (! function_exists('shmop_open')) throw new Exception('shmop functions not available on this PHP installation');
        srand();
        $memoryKey = ftok(__FILE__, 't').getmypid().rand();
        $this->shmId = shmop_open($memoryKey, "c", 0644, 5000);
    }
    
    public function set($key,$value) {
        $data = $this->readFromSharedMemory();
        $data[$key] = $value;
        if(!(boolean) shmop_write($this->shmId, serialize($data),0)) {
            die("Failed to write");
        }
    }
    
    public function get($key) {
        $data = $this->readFromSharedMemory();
        if (!isset($data[$key])) {
            return false;
        }
        return $data[$key];
    }

    public function getAll() {
        return $this->readFromSharedMemory();
    }

    private function readFromSharedMemory() {
        $dataSer = shmop_read($this->shmId , 0, shmop_size($this->shmId));
        $data = @unserialize($dataSer);
        if (!$data) {
            return array();
        }
        return $data;
    }

    public function close() {
        @shmop_delete($this->shmId);
        @shmop_close($this->shmId);
    }

I added in the getAll() function, as well as removed an exception that would break the script if a key wasn’t created yet, it now returns false and the developer can handle how we would like.

What I’m running into with this approach, is that because he reads the whole section of shared memory, makes the change to the whole array and then places the whole thing back in, we have some clashing between processes hitting at the same time. The more threads the more you notice. I wouldn’t expect a perfect system except when it comes to the end of the scripts, I have a benchmark class which returns each threads current status, but some seam to be “incomplete” because their data was overwritten at the same time.

Another issue here is that line 7 has a static size for the shared memory,initially he had it set to 100 bytes, which isnt room for much of anything. The bigger it gets though, the higher the overhead. I’m thinking of adding in some sort of function to destroy the shared memory and then recreate with a larger one when needed.

I think it might be worth it to create separate sectors for each thread to write to to avoid clashing. What are your thoughts on both of these situations?

So for your first issue, it sounds like you need to implement a locking technique, which is typical for any shared system. Just like you need to implement locking when dealing with files, locking with shared memory would be a requirement to stop the clashing. I’d likely follow the pattern used by ‘apt’ on debian based systems. Create a file/indicator when the shared memory is being accessed, and remove it when done. You would check the existence of said indicator/file, if it doesn’t exist, create it, access your memory, remove the indicator/file. If the indicator/file exists, wait, check again, wait (and so on) until the existence isn’t found, create the indicator/file, access the memory, remove the indicator/file.

As for your static size, I think you have the right idea there, to dynamically increase it as necessary. Be sure to increase it more than its immediate necessity is (to help performance). Example: Let’s say you have 100 bytes to start with and you are now at 98 bytes used. The next 4 bytes come in, meaning you need 102 bytes, but only have 100 currently reserved. Setup a new 130 bytes, transfer over the existing 98 and add the new 4 bytes so you have 102 out of 130 bytes used (I’ve used a 30% rule in the past, increasing the bytes by 30 percent on each growth, with the thought, as the data gets larger and larger the expectation is the data being sent is increasing larger and larger too). From what I recall from my C++ dynamic memory allocation courses, grabbing and allocating memory can be expensive if done often, it is best to give room for growth (but not too much where you are taking more than what you will actually need – making you look resource intensive).

Lastly, I’d allow yourself the ability to pass in the initial size. There may be times you know your original dataset is going to be 300 bytes, why reserve 100 just to increase it immediately to 300 or 400 when you write to it?

Not being funny here, but aren’t there tools out there that are better than php for multithreading? Doesn’t ruby handle multithreading properly?

Yes there are better languages for multi threading.

Over the last few weeks though, after trying other languages with the mongodriver (my main tool as of late) PHP crushes everyone. Java and Python were both under half the ops per second PHP was at when using the most basic mongo operations.

PHP has a lot of issues, no doubt, but I’m realizing that if PHP had true multi threading capability, it would be used ALOT more. I think you’d start seeing more project like nanoweb, heavily integrated with your own site. When messing with pcntl fork, I realized there was actually some potential if the right libraries were given to us.

EDIT: If PHP had multithreading capability as well as some other aspects of its language cleaned up… it would be used a lot more. My point is that the single thread that it does run on, performs suprisingly well, and I want to start expanding on that because that seems to be the gateway language for a lot of people these days.

I was thinking of using a static buffer amount, but I like the idea of a percentage when increasing allocated memory. I’ll set an optional argument to pre allocate a larger amount of memory than default.

I had kicked around was of creating a lock on memory, but I’m not sure that’s what is needed in my case, as all of my processes are writing to the same area as fast as they can. Perhaps the ability to set a mode:

Mode 1) Space is shared, use memory locking (default)
Mode 2) Space is shared, no memory locking
Mode 3) Separate memory space per each thread, each thread writes to its own space without locking, microtime(true) is applied to each var. When reading, all threads spaces are read and then the most recently timestamped var is returned.

Now I wonder for mode 1, how should I handle locked memory exceptions? Immediately retry? I feel like that might lead to problems.

FYI: Original authors blog post and git project: http://www.typo3-media.com/blog/multithreating-in-php.html | https://github.com/danielpoe/Threadi

But I plan on putting this thing on steroids :slight_smile:

Before getting into requirements for each mode, can you explain the bold emphasis more? If each process is using the same shared memory, then each process writing as fast as they can will indeed need locking so they don’t step on each others toes (process 1 already requested bytes 4-6, but process 3 also grabbed them and now whoever finishes last wins. They become race conditions and that isn’t good. Now if they have separate memory locations, this entire point is moot, as they shouldn’t be crossing over unless each process can spin its own threads within its memory allocation too (then you have race conditions again).

Okay, now that, I have that out of the way, locking and waiting for a lock to clear are acceptable approaches that other languages use. There are two things you need to keep in mind when implementing locking however, 1) if an error (exception) occurs, clear the LOCK!, don’t let it hold up everybody else because it failed. 2) Be consistent, lock immediately when placed inside a setter operation, and unlock as SOON as you can. Also consider if dirty reads are acceptable or not (if not, then you may need to lock getters too to ensure you get the latest data).