PHP程序中的文件锁、互斥锁、读写锁使用技巧解析


Posted in PHP onMarch 21, 2016

文件锁
全名叫 advisory file lock, 书中有提及。 这类锁比较常见,例如 mysql, php-fpm 启动之后都会有一个pid文件记录了进程id,这个文件就是文件锁。

这个锁可以防止重复运行一个进程,例如在使用crontab时,限定每一分钟执行一个任务,但这个进程运行时间可能超过一分钟,如果不用进程锁解决冲突的话两个进程一起执行就会有问题。

使用PID文件锁还有一个好处,方便进程向自己发停止或者重启信号。例如重启php-fpm的命令为

kill -USR2 `cat /usr/local/php/var/run/php-fpm.pid`
发送USR2信号给pid文件记录的进程,信号属于进程通信,会另开一个篇幅。

php的接口为flock,文档比较详细。先看一下定义,bool flock ( resource $handle , int $operation [, int &$wouldblock ] ).

  • $handle是文件系统指针,是典型地由 fopen() 创建的 resource(资源)。这就意味着使用flock必须打开一个文件。
  • $operation 是操作类型。
  • &$wouldblock 如果锁是阻塞的,那么这个变量会设为1.

需要注意的是,这个函数默认是阻塞的,如果想非阻塞可以在 operation 加一个 bitmask LOCK_NB. 接下来测试一下。

$pid_file = "/tmp/process.pid";
$pid = posix_getpid();
$fp = fopen($pid_file, 'w+');
if(flock($fp, LOCK_EX | LOCK_NB)){
  echo "got the lock \n";
  ftruncate($fp, 0);   // truncate file
  fwrite($fp, $pid);
  fflush($fp);      // flush output before releasing the lock
  sleep(300); // long running process
  flock($fp, LOCK_UN);  // 释放锁定
} else {
  echo "Cannot get pid lock. The process is already up \n";
}
fclose($fp);

保存为 process.php,运行php process.php &, 此时再次运行php process.php,就可以看到错误提示。flock也有共享锁,LOCK_SH.

互斥锁和读写锁
sync模块中的Mutex:
Mutex是一个组合词,mutual exclusion。用pecl安装一下sync模块, pecl install sync。 文档中的SyncMutex只有两个方法,lock 和 unlock, 我们就直接上代码测试吧。没有用IDE写,所以cs异常丑陋,请无视。

$mutex = new SyncMutex("UniqueName");

for($i=0; $i<2; $i++){
  $pid = pcntl_fork();
  if($pid <0){
    die("fork failed");
  }elseif ($pid>0){
    echo "parent process \n";
  }else{
    echo "child process {$i} is born. \n";
    obtainLock($mutex, $i);
  }
}

while (pcntl_waitpid(0, $status) != -1) { 
  $status = pcntl_wexitstatus($status); 
  echo "Child $status completed\n"; 
}

function obtainLock ($mutex, $i){
  echo "process {$i} is getting the mutex \n";
  $res = $mutex->lock(200);
  sleep(1);
  if (!$res){
    echo "process {$i} unable to lock mutex. \n";
  }else{
    echo "process {$i} successfully got the mutex \n";
    $mutex->unlock();
  }
  exit();
}

保存为mutex.php, run php mutex.php, output is

parent process 
parent process 
child process 1 is born. 
process 1 is getting the mutex 
child process 0 is born. 
process 0 is getting the mutex 
process 1 successfully got the mutex 
Child 0 completed
process 0 unable to lock mutex. 
Child 0 completed

这里子进程0和1不一定谁在前面。但是总有一个得不到锁。这里SyncMutex::lock(int $millisecond)的参数是 millisecond, 代表阻塞的时长, -1 为无限阻塞。

sync模块中的读写锁:
SyncReaderWriter的方法类似,readlock, readunlock, writelock, writeunlock,成对出现即可,没有写测试代码,应该和Mutex的代码一致,把锁替换一下就可以。

sync模块中的Event:
感觉和golang中的Cond比较像,wait()阻塞,fire()唤醒Event阻塞的一个进程。有一篇好文介绍了Cond, 可以看出Cond就是锁的一种固定用法。SyncEvent也一样。
php文档中的例子显示,fire()方法貌似可以用在web应用中。

上测试代码

for($i=0; $i<3; $i++){
  $pid = pcntl_fork();
  if($pid <0){
    die("fork failed");
  }elseif ($pid>0){
    //echo "parent process \n";
  }else{
    echo "child process {$i} is born. \n";
    switch ($i) {
    case 0:
      wait();
      break;
    case 1:
      wait();
      break;
    case 2:
      sleep(1);
      fire();
      break;
    }
  }
}

while (pcntl_waitpid(0, $status) != -1) { 
  $status = pcntl_wexitstatus($status); 
  echo "Child $status completed\n"; 
}

function wait(){
  $event = new SyncEvent("UniqueName");
  echo "before waiting. \n";
  $event->wait();
  echo "after waiting. \n";
  exit();
}

function fire(){
  $event = new SyncEvent("UniqueName");
  $event->fire();
  exit();
}

这里故意少写一个fire(), 所以程序会阻塞,证明了 fire() 一次只唤醒一个进程。

pthreads模块
锁定和解锁互斥量:

函数:

pthread_mutex_lock (mutex) 
pthread_mutex_trylock (mutex) 
pthread_mutex_unlock (mutex)

用法:

线程用pthread_mutex_lock()函数去锁定指定的mutex变量,若该mutex已经被另外一个线程锁定了,该调用将会阻塞线程直到mutex被解锁。
pthread_mutex_trylock() will attempt to lock a mutex. However, if the mutex is already locked, the routine will return immediately with a "busy" error code. This routine may be useful in pthread_mutex_trylock().

尝试着去锁定一个互斥量,然而,若互斥量已被锁定,程序会立刻返回并返回一个忙错误值。该函数在优先级改变情况下阻止死锁是非常有用的。线程可以用pthread_mutex_unlock()解锁自己占用的互斥量。在一个线程完成对保护数据的使用,而其它线程要获得互斥量在保护数据上工作时,可以调用该函数。若有一下情形则会发生错误:

  • 互斥量已经被解锁
  • 互斥量被另一个线程占用

互斥量并没有多么“神奇”的,实际上,它们就是参与的线程的“君子约定”。写代码时要确信正确地锁定,解锁互斥量。

Q:有多个线程等待同一个锁定的互斥量,当互斥量被解锁后,那个线程会第一个锁定互斥量?
A:除非线程使用了优先级调度机制,否则,线程会被系统调度器去分配,那个线程会第一个锁定互斥量是随机的。

#include<stdlib.h> 
#include<stdio.h> 
#include<unistd.h> 
#include<pthread.h> 

typedef struct ct_sum 
{ 
  int sum; 
  pthread_mutex_t lock; 
}ct_sum; 

void * add1(void *cnt) 
{    
  pthread_mutex_lock(&(((ct_sum*)cnt)->lock)); 
  for(int i=0; i < 50; i++) 
  {
    (*(ct_sum*)cnt).sum += i;   
  } 
  pthread_mutex_unlock(&(((ct_sum*)cnt)->lock)); 
  pthread_exit(NULL); 
  return 0; 
} 
void * add2(void *cnt) 
{    
  pthread_mutex_lock(&(((ct_sum*)cnt)->lock)); 
  for(int i=50; i<101; i++) 
  {  
     (*(ct_sum*)cnt).sum += i;  
  } 
  pthread_mutex_unlock(&(((ct_sum*)cnt)->lock)); 
  pthread_exit(NULL); 
  return 0; 
} 
 
int main(void) 
{
  pthread_t ptid1, ptid2; 
  ct_sum cnt; 
  pthread_mutex_init(&(cnt.lock), NULL); 
  cnt.sum=0; 
 
  pthread_create(&ptid1, NULL, add1, &cnt); 
  pthread_create(&ptid2, NULL, add2, &cnt); 
  
  pthread_join(ptid1,NULL); 
  pthread_join(ptid2,NULL);

  printf("sum %d\n", cnt.sum);
  pthread_mutex_destroy(&(cnt.lock)); 

  return 0; 
}

信号量
sync模块中的信号量:
SyncSemaphore文档中显示,它和Mutex的不同之处,在于Semaphore一次可以被多个进程(或线程)得到,而Mutex一次只能被一个得到。所以在SyncSemaphore的构造函数中,有一个参数指定信号量可以被多少进程得到。
public SyncSemaphore::__construct ([ string $name [, integer $initialval [, bool $autounlock ]]] ) 就是这个$initialval (initial value)

$lock = new SyncSemaphore("UniqueName", 2);

for($i=0; $i<2; $i++){
  $pid = pcntl_fork();
  if($pid <0){
    die("fork failed");
  }elseif ($pid>0){
    echo "parent process \n";
  }else{
    echo "child process {$i} is born. \n";
    obtainLock($lock, $i);
  }
}

while (pcntl_waitpid(0, $status) != -1) { 
  $status = pcntl_wexitstatus($status); 
  echo "Child $status completed\n"; 
}

function obtainLock ($lock, $i){
  echo "process {$i} is getting the lock \n";
  $res = $lock->lock(200);
  sleep(1);
  if (!$res){
    echo "process {$i} unable to lock lock. \n";
  }else{
    echo "process {$i} successfully got the lock \n";
    $lock->unlock();
  }
  exit();
}

这时候两个进程都能得到锁。

  • sysvsem模块中的信号量
  • sem_get 创建信号量
  • sem_remove 删除信号量(一般不用)
  • sem_acquire 请求得到信号量
  • sem_release 释放信号量。和 sem_acquire 成对使用。
$key = ftok('/tmp', 'c');

$sem = sem_get($key);

for($i=0; $i<2; $i++){
  $pid = pcntl_fork();
  if($pid <0){
    die("fork failed");
  }elseif ($pid>0){
    //echo "parent process \n";
  }else{
    echo "child process {$i} is born. \n";
    obtainLock($sem, $i);
  }
}

while (pcntl_waitpid(0, $status) != -1) { 
  $status = pcntl_wexitstatus($status); 
  echo "Child $status completed\n"; 
}
sem_remove($sem); // finally remove the sem

function obtainLock ($sem, $i){
  echo "process {$i} is getting the sem \n";
  $res = sem_acquire($sem, true);
  sleep(1);
  if (!$res){
    echo "process {$i} unable to get sem. \n";
  }else{
    echo "process {$i} successfully got the sem \n";
    sem_release($sem);
  }
  exit();
}

这里有一个问题,sem_acquire()第二个参数$nowait默认为false,阻塞。我设为了true,如果得到锁失败,那么后面的sem_release会报警告 PHP Warning:  sem_release(): SysV semaphore 4 (key 0x63000081) is not currently acquired in /home/jason/sysvsem.php on line 33, 所以这里的release操作必须放在得到锁的情况下执行,前面的几个例子中没有这个问题,没得到锁执行release也不会报错。当然最好还是成对出现,确保得到锁的情况下再release。
此外,ftok这个方法的参数有必要说明下,第一个 必须是existing, accessable的文件, 一般使用项目中的文件,第二个是单字符字符串。返回一个int。

输出为

parent process 
parent process 
child process 1 is born. 
process 1 is getting the mutex 
child process 0 is born. 
process 0 is getting the mutex 
process 1 successfully got the mutex 
Child 0 completed
process 0 unable to lock mutex. 
Child 0 completed
PHP 相关文章推荐
phpMyAdmin 安装配置方法和问题解决
Jun 08 PHP
memcached 和 mysql 主从环境下php开发代码详解
May 16 PHP
PHP使用正则表达式清除超链接文本
Nov 12 PHP
PHP的fsockopen、pfsockopen函数被主机商禁用的解决办法
Jul 08 PHP
PHP中加密解密函数与DES加密解密实例
Oct 17 PHP
PHP清除数组中所有字符串两端空格的方法
Oct 20 PHP
PHP用户验证和标签推荐的简单使用
Oct 31 PHP
php中10个不同等级压缩优化图片操作示例
Nov 14 PHP
CI框架AR数据库操作常用函数总结
Nov 21 PHP
thinkPHP框架实现的短信接口验证码功能示例
Jun 20 PHP
PHP添加PNG图片背景透明水印操作类定义与用法示例
Mar 12 PHP
linux mint下安装phpstorm2020包括JDK部分的教程详解
Sep 17 PHP
PHP编程中尝试程序并发的几种方式总结
Mar 21 #PHP
PHP的Laravel框架中使用消息队列queue及异步队列的方法
Mar 21 #PHP
Zend Framework框架之Zend_Mail实现发送Email邮件验证功能及解决标题乱码的方法
Mar 21 #PHP
Zend Framework教程之Zend_Form组件实现表单提交并显示错误提示的方法
Mar 21 #PHP
Zend Framework实现多文件上传功能实例
Mar 21 #PHP
Zend Framework入门之环境配置及第一个Hello World示例(附demo源码下载)
Mar 21 #PHP
Zend Framework教程之连接数据库并执行增删查的方法(附demo源码下载)
Mar 21 #PHP
You might like
给海燕B411配件机起死回生配上件
2021/03/02 无线电
ThinkPHP框架整合微信支付之JSAPI模式图文详解
2019/04/09 PHP
yii框架使用分页的方法分析
2019/07/25 PHP
关于IE浏览器以及Firefox下的javascript冒泡事件的响应层级
2010/10/14 Javascript
Firefox中autocomplete=&quot;off&quot; 设置不起作用Bug的解决方法
2011/03/25 Javascript
JavaScript将DOM事件处理程序封装为event.js 出现的低级错误问题
2016/08/03 Javascript
Vue监听数组变化源码解析
2017/03/09 Javascript
Vue.js 插件开发详解
2017/03/29 Javascript
基于VUE.JS的移动端框架Mint UI的使用
2017/10/11 Javascript
深入浅出理解JavaScript闭包的功能与用法
2018/08/01 Javascript
vue+axios实现post文件下载
2019/09/25 Javascript
[55:03]LGD vs EG 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/18 DOTA
[53:29]完美世界DOTA2联赛循环赛 DM vs Matador BO2第二场 11.04
2020/11/05 DOTA
[41:17]完美世界DOTA2联赛PWL S3 access vs CPG 第二场 12.13
2020/12/17 DOTA
Python中操作文件之write()方法的使用教程
2015/05/25 Python
Python使用剪切板的方法
2017/06/06 Python
用python实现k近邻算法的示例代码
2018/09/06 Python
在Python dataframe中出生日期转化为年龄的实现方法
2018/10/20 Python
Python在Matplotlib图中显示中文字体的操作方法
2019/07/29 Python
Python socket模块方法实现详解
2019/11/05 Python
python实现字符串和数字拼接
2020/03/02 Python
Python文件名匹配与文件复制的实现
2020/12/11 Python
彻底解决pip下载pytorch慢的问题方法
2021/03/01 Python
CSS3 真的会替代 SCSS 吗
2021/03/09 HTML / CSS
html5+css3进度条倒计时动画特效代码【推荐】
2016/03/08 HTML / CSS
Bally美国官网:经典瑞士鞋履、手袋及配饰奢侈品牌
2018/05/18 全球购物
英国花园、DIY、电器和家居用品商店:Robert Dyas
2019/03/18 全球购物
北京银河万佳Java面试题
2012/03/21 面试题
化学系大学生自荐信范文
2014/03/01 职场文书
导游词开场白
2015/01/31 职场文书
2015年安置帮教工作总结
2015/05/22 职场文书
2015年大学宣传部工作总结
2015/05/26 职场文书
拯救大兵瑞恩观后感
2015/06/09 职场文书
军训心得体会范文(2016最新篇)
2016/01/11 职场文书
幼儿园音乐教学反思
2016/02/18 职场文书
Redis监控工具RedisInsight安装与使用
2022/03/21 Redis