best way to obtain a lock in php
Asked Answered
S

11

19

I'm trying to update a variable in APC, and will be many processes trying to do that.

APC doesn't provide locking functionality, so I'm considering using other mechanisms... what I've found so far is mysql's GET_LOCK(), and php's flock(). Anything else worth considering?

Update: I've found sem_acquire, but it seems to be a blocking lock.

Stere answered 28/11, 2008 at 13:50 Comment(3)
What does the variable contain, exactly; why are you worried about locking? You may be able to work around the problem.Rhenium
A (late) word of warning: MySQL GET_LOCK() has a very dangerous behaviour. A second GET_LOCK() quietly releases the former lock on the same connection. MySQL can hold only ONE lock per connection. Nested locks are impossible with stock MySQL. It should not be used for general purpose locking.Jamarjamb
In MySQL 5.7, get_lock has resolved the issues mentioned above, so you can use it like you would expect now: dev.mysql.com/doc/refman/5.7/en/locking-functions.htmlMahdi
U
19
/*
CLASS ExclusiveLock
Description
==================================================================
This is a pseudo implementation of mutex since php does not have
any thread synchronization objects
This class uses flock() as a base to provide locking functionality.
Lock will be released in following cases
1 - user calls unlock
2 - when this lock object gets deleted
3 - when request or script ends
==================================================================
Usage:

//get the lock
$lock = new ExclusiveLock( "mylock" );

//lock
if( $lock->lock( ) == FALSE )
    error("Locking failed");
//--
//Do your work here
//--

//unlock
$lock->unlock();
===================================================================
*/
class ExclusiveLock
{
    protected $key   = null;  //user given value
    protected $file  = null;  //resource to lock
    protected $own   = FALSE; //have we locked resource

    function __construct( $key ) 
    {
        $this->key = $key;
        //create a new resource or get exisitng with same key
        $this->file = fopen("$key.lockfile", 'w+');
    }


    function __destruct() 
    {
        if( $this->own == TRUE )
            $this->unlock( );
    }


    function lock( ) 
    {
        if( !flock($this->file, LOCK_EX | LOCK_NB)) 
        { //failed
            $key = $this->key;
            error_log("ExclusiveLock::acquire_lock FAILED to acquire lock [$key]");
            return FALSE;
        }
        ftruncate($this->file, 0); // truncate file
        //write something to just help debugging
        fwrite( $this->file, "Locked\n");
        fflush( $this->file );

        $this->own = TRUE;
        return TRUE; // success
    }


    function unlock( ) 
    {
        $key = $this->key;
        if( $this->own == TRUE ) 
        {
            if( !flock($this->file, LOCK_UN) )
            { //failed
                error_log("ExclusiveLock::lock FAILED to release lock [$key]");
                return FALSE;
            }
            ftruncate($this->file, 0); // truncate file
            //write something to just help debugging
            fwrite( $this->file, "Unlocked\n");
            fflush( $this->file );
            $this->own = FALSE;
        }
        else
        {
            error_log("ExclusiveLock::unlock called on [$key] but its not acquired by caller");
        }
        return TRUE; // success
    }
};
Underestimate answered 13/10, 2010 at 10:37 Comment(5)
__destruct wont get called on fatal error will it? I'm worried about situations that could cause the lock to be permanently stuck on and require manual interventionDairen
flocks are cleared when a file is closed, and files are closed when the php process exits. So shouldn't be a problem.Institutionalize
calling flock and locking a file on disk is far far more expensive than the actual APC operation, therefore a very expensive way to coordinate APC cache.Kealey
@JohnSmith, I've done some editing. It should work fine now.Saguenay
Note that the fwrite(...) of "Unlocked" should probably be done BEFORE you unlock said file.Remonaremonetize
K
10

You can use the apc_add function to achieve this without resorting to file systems or mysql. apc_add only succeeds when the variable is not already stored; thus, providing a mechanism of locking. TTL can be used to ensure that falied lockholders won't keep on holding the lock forever.

The reason apc_add is the correct solution is because it avoids the race condition that would otherwise exist between checking the lock and setting it to 'locked by you'. Since apc_add only sets the value if it's not already set ( "adds" it to the cache ), it ensures that the lock can't be aquired by two calls at once, regardless of their proximity in time. No solution that doesn't check and set the lock at the same time will inherently suffer from this race condition; one atomic operation is required to successfully lock without race condition.

Since APC locks will only exist in the context of that php execution, it's probably not the best solution for general locking, as it doesn't support locks between hosts. Memcache also provides an atomic add function and thus can also be used with this technique - which is one method of locking between hosts. Redis also supports atomic 'SETNX' functions and TTL, and is a very common method of locking and synchronization between hosts. Howerver, the OP requests a solution for APC in particular.

Kealey answered 7/10, 2012 at 3:57 Comment(2)
How does the "lock" get released if the process that created the original variable dies without removing it? I'm guessing it doesn't. Automatic release of locks in the even of failure is an important feature of locks.Bodhisattva
great question @Jason, I'll expand the answer.Kealey
A
5

If the point of the lock is to prevent multiple processes from trying to populate an empty cache key, why wouldn't you want to have a blocking lock?


  $value = apc_fetch($KEY);

  if ($value === FALSE) {
      shm_acquire($SEMAPHORE);

      $recheck_value = apc_fetch($KEY);
      if ($recheck_value !== FALSE) {
        $new_value = expensive_operation();
        apc_store($KEY, $new_value);
        $value = $new_value;
      } else {
        $value = $recheck_value;
      }

      shm_release($SEMAPHORE);
   }

If the cache is good, you just roll with it. If there's nothing in the cache, you get a lock. Once you have the lock, you'll need to double-check the cache to make sure that, while you were waiting to get the lock, the cache wasn't repopulated. If the cache was repopulated, use that value & release the lock, otherwise, you do the computation, populate the cache & then release your lock.

Alula answered 2/12, 2008 at 19:53 Comment(1)
The reason for not using a blocking lock is that since there'll be loads of those processes, it would significantly slow them down. I'd rather them not to update the variable than wait and cause a meltdown as they accumulate.Stere
C
3

If you don't mind basing your lock on the filesystem, then you could use fopen() with mode 'x'. Here is an example:

$f = fopen("lockFile.txt", 'x');
if($f) {
    $me = getmypid();
    $now = date('Y-m-d H:i:s');
    fwrite($f, "Locked by $me at $now\n");
    fclose($f);
    doStuffInLock();
    unlink("lockFile.txt"); // unlock        
}
else {
    echo "File is locked: " . file_get_contents("lockFile.txt");
    exit;
}

See www.php.net/fopen

Chuu answered 30/11, 2008 at 23:22 Comment(2)
As long as you never need NFS, this probably the easiest solution. Though there is a good chance of getting a race condition or worse a pile up if the locking script crashes before freeing the flock.Fillander
Yes, you can get a pile up if the script crashes, but there are ways to work around that, or at least detect the problem using the PID and time written inside the lock file and send an email.Chuu
F
3

Actually, check to see if this will work better then Peter's suggestion.

https://www.php.net/flock

use an exclusive lock and if your comfortable with it, put everything else that attempted to lock the file in a 2-3 second sleep. If done right your site will experience a hang regarding the locked resource but not a horde of scripts fighting to cache the samething.

Fillander answered 2/12, 2008 at 19:26 Comment(0)
T
1

I realize this is a year old, but I just stumbled upon the question while doing some research myself on locking in PHP.

It occurs to me that a solution might be possible using APC itself. Call me crazy, but this might be a workable approach:

function acquire_lock($key, $expire=60) {
    if (is_locked($key)) {
        return null;
    }
    return apc_store($key, true, $expire);
}

function release_lock($key) {
    if (!is_locked($key)) {
        return null;
    }
    return apc_delete($key);
}

function is_locked($key) {
    return apc_fetch($key);
}

// example use
if (acquire_lock("foo")) {
    do_something_that_requires_a_lock();
    release_lock("foo");
}

In practice I might throw another function in there to generate a key to use here, just to prevent collision with an existing APC key, e.g.:

function key_for_lock($str) {
    return md5($str."locked");
}

The $expire parameter is a nice feature of APC to use, since it prevents your lock from being held forever if your script dies or something like that.

Hopefully this answer is helpful for anyone else who stumbles here a year later.

Telemann answered 28/11, 2008 at 13:50 Comment(3)
Since the aquire_lock is not atomic, it's not really useful when you need a lock because of concurrent accesses to some resource.Bechance
If the script dies, will APC release it?Amicable
acquire_lock introduces a typical race condition. Use apc_add to create and check the lock in one call instead.Unprepared
J
1

APC is now considered unmaintained and dead. It's successor APCu offers locking via apcu_entry. But be aware, that it also prohibits the concurrent execution of any other APCu functions. Depending on your use case, this might be OK for you.

From the manual:

Note: When control enters apcu_entry() the lock for the cache is acquired exclusively, it is released when control leaves apcu_entry(): In effect, this turns the body of generator into a critical section, disallowing two processes from executing the same code paths concurrently. In addition, it prohibits the concurrent execution of any other APCu functions, since they will acquire the same lock.

Jailbird answered 5/1, 2017 at 9:18 Comment(0)
B
0

EAccelerator has methods for it; eaccelerator_lock and eaccelerator_unlock.

Bechance answered 2/5, 2011 at 11:14 Comment(0)
N
0

Can't say if this is the best way to handle the job, but at least it is convenient.

function WhileLocked($pathname, callable $function, $proj = ' ')
{
    // create a semaphore for a given pathname and optional project id
    $semaphore = sem_get(ftok($pathname, $proj)); // see ftok for details
    sem_acquire($semaphore);
    try {
        // capture result
        $result = call_user_func($function);
    } catch (Exception $e) {
        // release lock and pass on all errors
        sem_release($semaphore);
        throw $e;
    }

    // also release lock if all is good
    sem_release($semaphore);
    return $result;
}

Usage is as simple as this.

$result = WhileLocked(__FILE__, function () use ($that) {
    $this->doSomethingNonsimultaneously($that->getFoo());
});

Third optional argument can come handy if you use this function more than once per file.

Last but not least it isn't hard to modify this function (while keeping its signature) to use any other kind of locking mechanism at a later date, e.g. if you happen to find yourself working with multiple servers.

Newtonnext answered 27/4, 2016 at 8:14 Comment(0)
S
0

APCu has apcu_entry since 5.1.0, can implement a lock mechanism with it now:

/** get a lock, will wait until the lock is available,
 * make sure handle deadlock yourself :p
 * 
 * useage : $lock = lock('THE_LOCK_KEY', uniqid(), 50);
 * 
 * @param $lock_key : the lock you want to get it
 * @param $lock_value : the unique value to specify lock owner
 * @param $retry_millis : wait befor retry
 * @return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value]
 */
function lock($lock_key, $lock_value, $retry_millis) {
    $got_lock = false;
    while (!$got_lock) {
        $fetched_lock_value = apcu_entry($lock_key, function ($key) use ($lock_value) {
            return $lock_value;
        }, 100);
        $got_lock = ($fetched_lock_value == $lock_value);
        if (!$got_lock) usleep($retry_millis*1000);
    }
    return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value];
}

/** release a lock
 * 
 * usage : unlock($lock);
 * 
 * @param $lock : return value of function lock
 */
function unlock($lock) {
    apcu_delete($lock['lock_key']);
}
Sidewinder answered 16/7, 2021 at 5:54 Comment(0)
S
-1

What I've found, actually, is that I don't need any locking at all... given what I'm trying to create is a map of all the class => path associations for autoload, it doesn't matter if one process overwrites what the other one has found (it's highly unlikely, if coded properly), because the data will get there eventually anyway. So, the solution turned out to be "no locks".

Stere answered 3/12, 2008 at 17:29 Comment(1)
If that's the case, you should close the question.Ethbin

© 2022 - 2024 — McMap. All rights reserved.