Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a mutex method in PHP per variable value

I need to have a mutex method in PHP so that it keeps exclusivity by a variable value. This is that threads with same value should enter that method one at a time while threads with different values may access that method arbitrarily.

For example, given that method:

/**
 * @param integer $value
 */
function mutexMethod($value)
{
    // Lock for value $value
    echo 'processing';
    sleep(2);
    echo 'this is so heavy';
    // Unlock for value $value
}

For example (I need this to be run through apache):

time |
0    | php > mutexMethod(1); | php > mutexMethod(2); | php > mutexMethod(1);
1    | processing            | processing            |
2    |                       |                       |
3    | this is so heavy      | this is so heavy      | processing
4    |                       |                       |
5    |                       |                       | this is so heavy

As a first solution, I've tried using semaphores but since $value may get any value, I've run out of semaphores space very quickly (I've tried removing the semaphores after using them but this breaks other threads waiting for it and since I cannot know if there are any threads waiting for them, I cannot remove them arbitrarily.

As a second solution, I've tried creating a file with the value $value as name and using flock to lock any other thread. Despite this worked in the CLI, I couldn't manage to make it work through apache. It certainly locked the file but it never released that lock so any other request got stuck until the first one timed out (after 30 seconds).

Finally I though about using MySQL locks but I would like to avoid them as much as we would like to not use the MySQL instance for such things. Ideally we would like a pure PHP solution.

Do you have an idea on how can I solve that problem? I would like to avoid single-semaphore solutions (like having a single semaphore to control access to a file where to keep track of the locks) as this would create a massive bottleneck (specially for those threads with different values).

Thank you very much.

like image 741
carlosV2 Avatar asked Oct 31 '22 05:10

carlosV2


2 Answers

https://github.com/arvenil/ninja-mutex

flock/mysql/redis/memcache adapter

You can try them all and pick the one that works for you

In your case example could look like this

<?php
require 'vendor/autoload.php';

use NinjaMutex\Lock\MemcacheLock;
use NinjaMutex\MutexFabric;

$memcache = new Memcache();
$memcache->connect('127.0.0.1', 11211);
$lock = new MemcacheLock($memcache);
$mutexFabric = new MutexFabric('memcache', $lock);
if ($mutexFabric->get($value)->acquireLock(1000)) {
    // Do some very critical stuff

    // and release lock after you finish
    $mutexFabric->get($value)->releaseLock();
} else {
    throw new Exception('Unable to gain lock for very critical stuff!');
}
like image 151
Kamil Dziedzic Avatar answered Nov 15 '22 06:11

Kamil Dziedzic


From what I understand you want to make sure only a single process at a time is running a certain piece of code. I myself use lockfiles to have a solution that works on many platforms and doesn't rely on a specific library only available on Linux etc.

For that, I have written a small Lock class. Do note that it uses some non-standard functions from my library, for instance, to get where to store temporary files etc. But you could easily change that.

<?php   
    class Lock
    {
        private $_owned             = false;

        private $_name              = null;
        private $_lockFile          = null;
        private $_lockFilePointer   = null;

        public function __construct($name)
        {
            $this->_name = $name;
            $this->_lockFile = PluginManager::getInstance()->getCorePlugin()->getTempDir('locks') . $name . '-' . sha1($name . PluginManager::getInstance()->getCorePlugin()->getPreference('EncryptionKey')->getValue()).'.lock';
        }

        public function __destruct()
        {
            $this->release();
        }

        /**
         * Acquires a lock
         *
         * Returns true on success and false on failure.
         * Could be told to wait (block) and if so for a max amount of seconds or return false right away.
         *
         * @param bool $wait
         * @param null $maxWaitTime
         * @return bool
         * @throws \Exception
         */
        public function acquire($wait = false, $maxWaitTime = null) {
            $this->_lockFilePointer = fopen($this->_lockFile, 'c');
            if(!$this->_lockFilePointer) {
                throw new \RuntimeException(__('Unable to create lock file', 'dliCore'));
            }

            if($wait && $maxWaitTime === null) {
                $flags = LOCK_EX;
            }
            else {
                $flags = LOCK_EX | LOCK_NB;
            }

            $startTime = time();

            while(1) {
                if (flock($this->_lockFilePointer, $flags)) {
                    $this->_owned = true;
                    return true;
                } else {
                    if($maxWaitTime === null || time() - $startTime > $maxWaitTime) {
                        fclose($this->_lockFilePointer);
                        return false;
                    }
                    sleep(1);
                }
            }
        }

        /**
         * Releases the lock
         */
        public function release()
        {
            if($this->_owned) {
                @flock($this->_lockFilePointer, LOCK_UN);
                @fclose($this->_lockFilePointer);
                @unlink($this->_lockFile);
                $this->_owned = false;
            }
        }
    }

Usage

Now you can have two process that run at the same time and execute the same script

Process 1

$lock = new Lock('runExpensiveFunction');

if($lock->acquire()) {
  // Some expensive function that should only run one at a time
  runExpensiveFunction();
  $lock->release();
}

Process 2

$lock = new Lock('runExpensiveFunction');

// Check will be false since the lock will already be held by someone else so the function is skipped
if($lock->acquire()) {
  // Some expensive function that should only run one at a time
  runExpensiveFunction();
  $lock->release();
}

Another alternative would be to have the second process wait for the first one to finish instead of skipping the code.

$lock = new Lock('runExpensiveFunction');

// Process will now wait for the lock to become available. A max wait time can be set if needed.
if($lock->acquire(true)) {
  // Some expensive function that should only run one at a time
  runExpensiveFunction();
  $lock->release();
}

RAM disk

To limit the number of writes to your HDD/SSD with the lockfiles you could crate a RAM disk to store them in.

On Linux you could add something like the following to /etc/fstab

tmpfs       /mnt/ramdisk tmpfs   nodev,nosuid,noexec,nodiratime,size=1024M   0 0

On Windows you can download something like ImDisk Toolkit and create a ramdisk with that.

ImDisk RamDisk Configuration Tool

like image 45
inquam Avatar answered Nov 15 '22 06:11

inquam