Alzi la mano chi, lavorando con PHP, non ha mai avuto la necessità di limitare l'accesso simultaneo a una funzione o a un metodo di una classe. Si tratta di un'esigenza tipica delle cosiddette maintenance task, ovvero di quelle procedure eseguite a intervalli regolari senza l'ausilio di un cron job vero e proprio ma al primo accesso di un qualsiasi utente a partire da una determinata ora. E' probabile che, se vi siete imbattuti in questo articolo, siate alla ricerca di un modo per impedire che l'accesso simultaneo di più utenti possa provocare l'esecuzione simultanea della medesima task prima del completamento della stessa.
In altri linguaggi di programmazione (ASP.NET/C#, Phyton et. al.) questa esigenza si risolve facilmente utilizzando procedure di thread synchronization come i ben noti semafori o lock, in modo simile al seguente esempio (C#):
1 2 3 |
lock (object) { ExecuteLockedFunction() } |
In PHP, contraddistinto sotto Windows da una implementazione thread-unsafe e, sotto Linux, da una esecuzione isolata per ciascun processo, non esiste una funzionalità nativa per ottenere un lock di questo tipo. Un tipico workaround utilizzato dagli sviluppatori è quello di ricorrere al flock(), una tecnica di lock basata sulla creazione di semafori creati direttamente sul filesystem: in poche parole, un lock ottenuto sfruttando la creazione/presenza/eliminazione di un file che fa le funzioni di semaforo e ha il compito di tenere sincronizzati i thread/processi. Pur trattandosi di un approccio assolutamente percorribile, questa tecnica ha almeno due difetti: le performance non eccezionali - dovute alla necessità di effettuare frequenti operazioni IO - e la scomodità di dover gestire i file/semafori con tutto quello che ne consegue - path, permessi e via dicendo. Fortunatamente esiste un metodo decisamente più pratico, a patto che l'installazione PHP preveda l'utilizzo di una delle numerose librerie di caching disponibili. Le più note e utilizzate sono:
- WinCache, di cui abbiamo già avuto modo di parlare in un paio di occasioni, realizzata da Microsoft e disponibile in tutte le distribuzioni PHP per Windows.
- APC, scaricabile dall'apposita pagina PECL.
Trattandosi di librerie dedicate alla gestione di un'area di cache condivisa tra i vari processi/thread di esecuzione PHP, ciascuna di esse prevede una funzione specifica per acquisire il lock: vediamo quali sono e come implementarli all'interno del nostro codice.
Nella lista non è presente OPCache, l'ottima libreria sviluppata da Zend e disponibile in tutte le distribuzioni di PHP recenti per Windows e Linux, perché - almeno allo stato attuale - non mette a disposizione dello sviluppatore una user cache su cui poter intervenire in modo arbitrario.
WinCache
WinCache mette a disposizione un metodo chiamato wincache_lock che, come suggerisce il nome, svolge esattamente il lavoro che ci serve. Per ottenere l'effetto desiderato è consigliabile implementarlo nel seguente modo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
try { // WinCache-based lock // ref.: http://php.net/manual/en/function.wincache-lock.php $canLock = function_exists('wincache_lock'); $lockKey = 'lock_key'; if (!$canLock || wincache_lock($lockKey)) { // todo: do your locked stuff if ($canLock) wincache_unlock($lockKey); } } catch (Exception $e) { if ($canLock) wincache_unlock($lockKey); throw $e; } |
L'implementazione potrebbe essere molto più snella, ma in questo modo saremo certi che la nostra funzione venga eseguita anche presso sistemi che non prevedono la presenza di WinCache: il locking è infatti subordinato alla presenza della funzione wincache_lock, in mancanza della quale procederemo a una esecuzione di tipo convenzionale. La presenza del blocco try/catch mette inoltre il codice al riparo da deadlock, ovvero da quelle situazioni in cui il lock non viene rilasciato - magari a causa di una eccezione non gestita - provocando il blocco dell'applicazione e/o l'impossibilità di eseguire il codice da quel momento in poi. Inutile dire che, per poter disporre di questa funzionalità, è necessario che l'estensione WinCache sia stata installata con la funzionalità user cache abilitata.
Per maggiori informazioni su wincache_lock consigliamo di consultare la pagina ufficiale relativa sul sito php.net, che contiene anche un interessante paragrafo che spiega quando è il caso di utilizzare questa tecnica e quando invece è meglio astenersi dal farlo.
APC
Se fate uso di APC potete utilizzare la funzione apc_add, sfruttando il fatto che - a differenza della funzione gemella apc_store - inserisce una chiave/valore nella cache in modalità cache-unique, ovvero senza sovrascrivere una chiave già esistente ma restituendo in quel caso FALSE. Una possibile implementazione è quindi la seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
try { // APC-based lock // ref.: http://php.net/manual/en/function.wincache-lock.php $canLock = function_exists('apc_add'); $lockKey = 'lock_key'; if (!$canLock || apc_add($lockKey)) { // todo: do your locked stuff if ($canLock) apc_remove($lockKey); } } catch (Exception $e) { if ($canLock) apc_remove($lockKey); throw $e; } |
Anche in questo caso abbiamo adottato le opportune strategie che consentono al nostro script di funzionare anche in mancanza di APC e le contromisure anti-deadlock del caso.
Per maggiori informazioni su APC e sulla funzione apc_add rimandiamo come sempre alla pagina ufficiale su php.net.