ConcurrencyPHPverifiedVerified
Mutex / Lock Pattern in PHP
Guarantee that only one thread at a time can access a shared resource by requiring threads to acquire an exclusive lock before proceeding.
How to Implement the Mutex / Lock Pattern in PHP
1Step 1: Define a simple mutex using file locking
class Mutex
{
private mixed $lockFile = null;
private bool $locked = false;
public function __construct(
private readonly string $name,
) {}2Step 2: Acquire the lock
public function lock(): void
{
$path = sys_get_temp_dir() . "/{$this->name}.lock";
$this->lockFile = fopen($path, 'w');
if ($this->lockFile === false) {
throw new \RuntimeException("Cannot create lock file");
}
if (!flock($this->lockFile, LOCK_EX)) {
throw new \RuntimeException("Cannot acquire lock: {$this->name}");
}
$this->locked = true;
}3Step 3: Release the lock
public function unlock(): void
{
if ($this->lockFile !== null && $this->locked) {
flock($this->lockFile, LOCK_UN);
fclose($this->lockFile);
$this->locked = false;
}
}4Step 4: Execute a callback while holding the lock
public function synchronized(callable $callback): mixed
{
$this->lock();
try {
return $callback();
} finally {
$this->unlock();
}
}
}
// Usage
$mutex = new Mutex('counter');
$counter = 0;
$mutex->synchronized(function () use (&$counter): void {
$counter++;
echo "Counter: {$counter}\n";
});<?php
declare(strict_types=1);
// [step] Define mutex interface and result types
interface MutexInterface
{
public function acquire(float $timeoutMs = 0): bool;
public function release(): void;
public function isLocked(): bool;
/** @template T
* @param callable(): T $callback
* @return T
*/
public function synchronized(callable $callback): mixed;
}
final readonly class LockInfo
{
public function __construct(
public string $name,
public string $owner,
public float $acquiredAt,
public ?float $expiresAt,
) {}
}
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function warning(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
}
// [step] Implement a file-based mutex with timeout and ownership tracking
final class FileMutex implements MutexInterface
{
private mixed $lockFile = null;
private bool $locked = false;
private ?LockInfo $lockInfo = null;
private readonly string $lockPath;
public function __construct(
private readonly string $name,
private readonly LoggerInterface $logger,
private readonly ?float $ttlSeconds = null,
?string $lockDir = null,
) {
$dir = $lockDir ?? sys_get_temp_dir() . '/php_mutex';
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$this->lockPath = $dir . '/' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $name) . '.lock';
}
public function acquire(float $timeoutMs = 0): bool
{
if ($this->locked) {
throw new \LogicException("Mutex '{$this->name}' is already acquired by this process");
}
$this->lockFile = fopen($this->lockPath, 'w');
if ($this->lockFile === false) {
throw new \RuntimeException("Cannot create lock file: {$this->lockPath}");
}
$deadline = $timeoutMs > 0 ? microtime(true) + ($timeoutMs / 1000) : 0;
do {
if (flock($this->lockFile, LOCK_EX | LOCK_NB)) {
$this->locked = true;
$now = microtime(true);
$this->lockInfo = new LockInfo(
name: $this->name,
owner: (string) getmypid(),
acquiredAt: $now,
expiresAt: $this->ttlSeconds !== null ? $now + $this->ttlSeconds : null,
);
// Write lock metadata
fwrite($this->lockFile, json_encode([
'pid' => getmypid(),
'acquiredAt' => $now,
'expiresAt' => $this->lockInfo->expiresAt,
], JSON_THROW_ON_ERROR));
fflush($this->lockFile);
$this->logger->info("Mutex acquired", ['name' => $this->name]);
return true;
}
if ($deadline > 0 && microtime(true) >= $deadline) {
fclose($this->lockFile);
$this->lockFile = null;
$this->logger->warning("Mutex acquire timeout", ['name' => $this->name]);
return false;
}
usleep(1000); // 1ms spin wait
} while ($deadline > 0);
fclose($this->lockFile);
$this->lockFile = null;
return false;
}
public function release(): void
{
if (!$this->locked || $this->lockFile === null) {
throw new \LogicException("Mutex '{$this->name}' is not acquired");
}
ftruncate($this->lockFile, 0);
flock($this->lockFile, LOCK_UN);
fclose($this->lockFile);
$this->lockFile = null;
$this->locked = false;
$this->lockInfo = null;
$this->logger->info("Mutex released", ['name' => $this->name]);
}
public function isLocked(): bool { return $this->locked; }
public function synchronized(callable $callback): mixed
{
if (!$this->acquire(timeoutMs: 5000)) {
throw new \RuntimeException("Failed to acquire mutex '{$this->name}'");
}
try {
return $callback();
} finally {
$this->release();
}
}
public function getLockInfo(): ?LockInfo { return $this->lockInfo; }
public function __destruct()
{
if ($this->locked) {
$this->release();
}
}
}Mutex / Lock Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Mutex / Lock Pattern in the Real World
“A single-occupancy public restroom with a door latch is a perfect mutex. The latch (lock) ensures only one person (thread) occupies the restroom (critical section) at a time. Anyone who finds the door locked must wait outside; when the occupant leaves and unlatches the door, one waiting person may enter.”