PHP\'s documentation page for flock() indicates that it\'s not safe to use under IIS. If I can\'t rely on flock under all circumstances, is there another way I
My proposal is to use mkdir() instead of flock(). This is a real-world example for reading/writing caches showing the differences:
$data = false;
$cache_file = 'cache/first_last123.inc';
$lock_dir = 'cache/first_last123_lock';
// read data from cache if no writing process is running
if (!file_exists($lock_dir)) {
// we suppress error messages as the cache file exists in 99,999% of all requests
$data = @include $cache_file;
}
// cache file not found
if ($data === false) {
// get data from database
$data = mysqli_fetch_assoc(mysqli_query($link, "SELECT first, last FROM users WHERE id = 123"));
// write data to cache if no writing process is running (race condition safe)
// we suppress E_WARNING of mkdir() because it is possible in 0,001% of all requests that the dir already exists after calling file_exists()
if (!file_exists($lock_dir) && @mkdir($lock_dir)) {
file_put_contents($cache_file, '')) {
// remove lock
rmdir($lock_dir);
}
}
Now, we try to achieve the same with flock():
$data = false;
$cache_file = 'cache/first_last123.inc';
// we suppress error messages as the cache file exists in 99,999% of all requests
$fp = @fopen($cache_file, "r");
// read data from cache if no writing process is running
if ($fp !== false && flock($fp, LOCK_EX | LOCK_NB)) {
// we suppress error messages as the cache file exists in 99,999% of all requests
$data = @include $cache_file;
flock($fp, LOCK_UN);
}
// cache file not found
if (!is_array($data)) {
// get data from database
$data = mysqli_fetch_assoc(mysqli_query($link, "SELECT first, last FROM users WHERE id = 123"));
// write data to cache if no writing process is running (race condition safe)
$fp = fopen($cache_file, "c");
if (flock($fp, LOCK_EX | LOCK_NB)) {
ftruncate($fp, 0);
fwrite($fp, '');
flock($fp, LOCK_UN);
}
}
The important part is LOCK_NB to avoid blocking all consecutive requests:
It is also possible to add LOCK_NB as a bitmask to one of the above operations if you don't want flock() to block while locking.
Without it, the code would produce a huge bottleneck!
An additional important part is if (!is_array($data)) {. This is because $data could contain:
array() as a result of the db queryfalse of the failing includeThe race condition happens if the first visitor executes this line:
$fp = fopen($cache_file, "c");
and another visitor executes this line one millisecond later:
if ($fp !== false && flock($fp, LOCK_EX | LOCK_NB)) {
This means the first visitor creates the empty file, but the second visitor creates the lock and so include returns an empty string.
So you saw many pitfalls that can be avoided through using mkdir() and its 7x faster, too:
$filename = 'index.html';
$loops = 10000;
$start = microtime(true);
for ($i = 0; $i < $loops; $i++) {
file_exists($filename);
}
echo __LINE__ . ': ' . round(microtime(true) - $start, 5) . PHP_EOL;
$start = microtime(true);
for ($i = 0; $i < $loops; $i++) {
$fp = @fopen($filename, "r");
flock($fp, LOCK_EX | LOCK_NB);
}
echo __LINE__ . ': ' . round(microtime(true) - $start, 5) . PHP_EOL;
result:
file_exists: 0.00949
fopen/flock: 0.06401
P.S. as you can see I use file_exists() in front of mkdir(). This is because my tests (German) resulted bottlenecks using mkdir() alone.