Errors like “Race condition” are rarely seen on lightly loaded projects, and as the load increases, the situation slowly but surely changes. And once normal data caching in a file, for example, like this:
function getFlagFromFile($filename) { if (file_exists($filename)) { if (!$this->validate()) { // ? unlink($filename); return false; } else { return file_get_contents($filename); } } return false; }
gives an error in the line unlink (): the file $ filename does not exist!
The most interesting and incomprehensible is that the error occurs at random times, and when you try to debag - it does not play!
Race condition error occurs when the system is in such a state in which the same code is executed simultaneously (several parallel threads). In the above example, if the code is executed in several threads, file_exist ($ filename) and! $ This-> validate () checks can be performed with a positive result on both threads simultaneously, but unlink ($ filename) can be done by one thread earlier than another - and then the second thread will cause an error.
')
Let's look at two ways to deal with this error within a single server that do not use the flock () file lock (with multiple requests from a critical section of code, unnecessary calls to the file system are not the best way out).
It is possible to interfere with a race condition using APC and semaphores, and there and there there are corresponding atomic operations. But let's do it in order.
APC solution
The alternative cache in PHP (Alternative PHP Cache - APC) caches the bytecode of the executed scripts, thereby preventing the expenditure of resources for analyzing the source code, they know this if not all, almost everything. But not everyone knows that APC has its own key-value storage, a feature of which is the preservation of values between requests. The value specified once will be stored in APC until the web server is restarted, or until the value is forced to be deleted (or the value will expire if it has been set).
For an exclusive lock, use the apc_add function (key, value, lifetime) —it will return false if the value has already been assigned before (apc_store () exists to re-assign the value to the key). The full condition for using the solution is (changes in getFlagFromFile () are marked with comments ***):
function canUseApc() { return extension_loaded('apc') && ini_get('apc.enabled') && php_sapi_name() !== 'cli'; } function getFlagFromFile($filename) { if (file_exists($filename)) { if (!$this->validate()) { if ($this->canUseApc() && apc_add('some_key', 1)) { //*** unlink($filename); apc_delete('some_key'); //*** } return false; } else { return file_get_contents($filename); } } return false; }
File deletion here will be performed only if the thread was able to set a value in APC, which means there will be no parallel deletions, as well as errors. But if you forget to delete the value from APC using apc_delete (), then only restarting the web server will help to delete it.
This is the easiest solution to implement. However, the main disadvantage of the solution is that APC does not work for CLI scripts. For them, the solution is suitable for semaphores.
Semaphore Solution
The semaphore is easier to imagine as a variable (flag) that can be increased or decreased. When a semaphore is captured by one process, its value is reduced by one, and when released, it is increased by one. However, if the current value of the semaphore is zero, the process will not succeed in capturing it and it will wait for the semaphore to be released.
To obtain the semaphore resource, the sem_get function is used (integer identifier, semaphore value = 1). By function, you can get a semaphore with a value different from one, and then several threads can capture the semaphore. Actually, the sem_acquire function (semaphore resource) is used for the capture, which returns true if the capture was successful, and false otherwise.
Our example using semaphores will look like this:
function getFlagFromFile($filename) { if (file_exists($filename)) { if (!$this->validate()) { $sem = sem_get(1); //*** if (sem_acquire($sem) && file_exists($filename)) { //*** unlink($filename); } sem_remove($sem); //*** return false; } else { return file_get_contents($filename); } } return false; }
UPD: FrenzyKryger correctly noted that sem_remove ($ sem) should be out of conditionThe attentive reader will notice that we have added a re-check for the existence of the file. When the first thread captures the semaphore, deletes the file and releases the semaphore, the second thread can continue execution and it should no longer delete the file that is not there, which is why we add the check again.
Here, the sem_remove function (semaphore resource) plays an important role, which releases a busy semaphore. If the semaphore is not released, the parallel thread will remain in the idle state until the end of the current one.
This is the disadvantage of this decision: the situation in which a parallel flow can wait is not always acceptable. Often, the server must respond as quickly as possible, rather than waiting for exclusive access, even though it was not possible to perform the required action. Plus, in comparison with the previous solution, semaphores work in cli-scripts.
Let's sum up
Each of the considered methods has its own minuses and advantages. It is most convenient to combine both solutions into one class, hiding the immediate implementation. Then we get a good and simple tool to prevent the race condition:
function getFlagFromFile($filename) { if (file_exists($filename)) { if (!$this->validate()) { if ($race = RaceCondition::prevent('FLAG_'.$filename)) { //*** unlink($filename); $race->release(); //*** } return false; } else { return file_get_contents($filename); } } return false; }
I do not post a complete solution, leaving it as a homework =) And simple googling will quickly give more detailed answers about semaphores, streams and APC.
Comments and edits are welcome in PM!