扩展下载链接 :http://php.net/manual/zh/intro.sync.php

简介 [纯谷歌翻译]

“同步”扩展在php中引入了跨平台同步对象。命名和未命名的互斥体,信号量,事件,读写器和命名共享内存对象在posix(例如linux)和Windows平台上提供os级同步。
在扩展拆解过程中会自动清除已获取的同步对象。这意味着如果php过早终止一个脚本(例如脚本执行时间被超过),对象将不会处于未知状态。
唯一的例外是如果php本身崩溃(例如内部缓冲区溢出)。未命名的同步对象在多线程场景之外没有太多的用处。未命名的对象与pthreads pecl扩展一起更有用。

脚本结束的时候自动释放引用

互斥锁

SyncMutex — 互斥锁类定义
    SyncMutex::__construct ([ string $name ] ) — 构造一个新的互斥锁对象
    SyncMutex::lock — 等待获取互斥锁
    SyncMutex::unlock — 解除互斥锁

互斥锁的实例

<?php
// php互斥锁功能使用
$syncMutex = new SyncMutex("定义锁名称为空是默认锁");
//获取锁,如果没有获取到锁则处于等待状态,超时返回false
//默认加锁 和等待锁, 默认为无限等待 单位毫秒
if(!$syncMutex-> lock(3000)) {  // 3秒
  die("wait lock time out");
}
echo "do somthing ..." . PHP_EOL;
sleep(10);  //睡眠10秒
echo "end ". PHP_EOL;
die(); //这里会自动释放引用
//解互斥锁
$syncMutex->unlock();

同步信号

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

warning: 如果对象是一个autounlock为false的已命名信号量,则该对象被锁定,并且php脚本在对象被解锁之前结束,那么底层的信号量将以不一致的状态结束。

当信号量为1的时候就算是实现了互斥操作

<?php
SyncSemaphore — 同步信号类定义
    public SyncSemaphore::__construct ([ string $name /*信号名称*/ [, integer $initialval = 1 /*信号量的初始值。这是可以获得的锁的数量。*/[, bool $autounlock = true /*指定是否在PHP脚本结束时自动解锁信号量。*/]]] ) 构造一个同步信号的对象
    SyncSemaphore::lock — 减少信号量或等待的次数
    SyncSemaphore::unlock — 增加了信号量的计数

实例1

<?php
$semaphore = new SyncSemaphore("LimitedResource_2clients", 2);
//同时启动多个终端运行
if (!$semaphore->lock())
{
    echo "无法获取信号量." . PHP_EOL;
    exit();
}

echo  "do somthing..." . PHP_EOL;
sleep(10);
echo "end" . PHP_EOL;
/* ... */
//die();  测试期间如果将自动释放锁设置为false,当资源被用完的时候永远处于锁定状态,不知道是不是这么回事
$semaphore->unlock();

实例2

<?php
$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();
}

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

互斥读写锁

一个跨平台的本地实现的命名和未命名的读写器对象。
读写器对象允许许多读者或作者访问资源。
这是一个高效的资源管理解决方案,其中访问主要是只读的,但偶尔需要独占写入访问。

如果实例对象没有正确解锁,系统将处于锁死状态 $autounlock = false

<?php
SyncReaderWriter {
/* 方法 */
    public __construct ([ string $name [, bool $autounlock = true ]] )
    public bool readlock ([ integer $wait = -1 ] ) # 读加锁
    public bool readunlock ( void ) # 释放锁
    public bool writelock ([ integer $wait = -1 ] ) # 写加锁
    public bool writeunlock ( void ) # 释放锁
}

实例

<?php
$readwrite = new SyncReaderWriter("FileCacheLock",false /*不自动释放锁,不要随意设置,你并不能控制什么时候会出异常*/);
$readwrite->readlock();
/* ... */
// $readwrite->readunlock(); 注释会永久锁死

$readwrite->writelock();
/* ... */
//$readwrite->writeunlock(); 注释会永久锁死

SyncEvent 同步事件

一个跨平台的本地实现的命名和未命名的事件对象。支持自动和手动事件对象。
一个事件对象不用轮询就等待该对象被触发/设置。
一个实例等待事件对象,而另一个实例触发/设置事件。
事件对象在长时间运行的进程将以其他方式轮询资源(例如检查是否需要处理上载的数据)的情况下是有用的。

<?php
SyncEvent {
  /* 方法 */
  public __construct ([ string $name /*事件名称*/ [, bool $manual = false /*指定是否必须手动重置对象*/ [, bool $prefire = false  /*是否预先发出信号对象 只有在调用进程/线程是第一个创建对象时才会有影响。*/]]] )
  public bool fire ( void )
  public bool reset ( void )
  public bool wait ([ integer $wait = -1 ] )
}

感觉和golang中的Cond比较像,wait()阻塞,fire()唤醒Event阻塞的一个进程。有一篇好文介绍了Cond, 可以看出Cond就是锁的一种固定用法。SyncEvent也一样。

案例1

# wait.php  等待触发事件的进程文件
<?php
while (true) {
  $event = new SyncEvent("GetAppReport");
  echo "waiting ..." . PHP_EOL;
  $event->wait();// 设置事件等待: 等待事件被触发或重置 , 如果时间没有触发 则处于阻塞状态
  echo "waited ..." . PHP_EOL;
}

# fire.php 触发事件文件
$event = new SyncEvent("GetAppReport" );
if($event->fire()){
  echo "fired" . PHP_EOL;
}

案例二

<?php
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() 一次只唤醒一个进程。

同步共享内存

<?php
SyncSharedMemory {
    /* 方法 */
    public __construct ( string $name , integer $size ) 创建一个共享内存对象
    public bool first ( void ) 检查该对象是否是命名共享内存的系统范围的第一个实例
    public read ([ integer $start = 0 [, integer $length ]] ) # 从命名内存中赋值数据
    public bool size ( void ) # 返回命名内存的大小
    public write ([ string $string [, integer $start = 0 ]] ) # 向共享内存中写入数据
}

实例

<?php
//可能需要与其他同步对象保护共享内存。当最后一个引用消失时,共享内存就消失了。
$mem = new SyncSharedMemory("UnqName", 1024);
echo 'is first:' . var_export($mem->first(), true) . PHP_EOL;
$curlen = 0;
if ($mem->first()){
    while (true) {
      sleep(5);
      $curlen += $mem->write(json_encode(array("name" => "my_report.txt")) , $curlen);
      echo "写入数据成功" . var_export($result,true). " SIZE:" . $mem->size() ." WRITED SITE: $curlen". PHP_EOL;
    }
} else {
    echo  "读取数据: " . $mem->read(). PHP_EOL;
}
// 测试体现: 必须将开辟内存阻塞在进程内,才能在其他进程访问