协程 COOPERATION


协程不是 进程或线程 其执行过程更类似于子例程 或者说 不带返回值的函数调用
一个程序可以包含多个协程 可以对比与一个进程包含多个线程

比较协程和线程
多个线程相对独立 有自己的上下文 切换受系统控制
而协程也相对独立 有自己的上下文 但是其切换由自己控制 由当前协程切换到其他协程由当前协程来控制  协程
协程执行顺序

原生php代码
<?php
function task1(){
  for ($i=0;$i<=300;$i++){  //写入文件 大概要3000微秒
      usleep(3000);
      echo "写入文件{$i}\n";
  }
}
function task2(){
  for ($i=0;$i<=500;$i++){ //发送邮件给500名会员 大概3000微秒
      usleep(3000);
      echo "发送邮件{$i}\n";
  }
}
function task3(){
  for ($i=0;$i<=100;$i++){//模拟插入100条数据 大概3000微秒
      usleep(3000);
      echo "插入数据{$i}\n";
  }
}
task1();
task2();
task3();

这个代码 主要做了3件事 写入文件 发送邮件 以及插入数据

<?php
function task1($i){ //使用$i标识 写入文件,大概要3000微秒
  if ($i > 300) {
      return false;//超过300不用写了
  }
  echo "写入文件{$i}\n";
  usleep(3000);
  return true;
}

function task2($i){ //使用$i标识 发送邮件,大概要3000微秒
  if ($i > 500) {
      return false;//超过500不用发送了
  }
  echo "发送邮件{$i}\n";
  usleep(3000);
  return true;
}

function task3($i){ //使用$i标识 插入数据,大概要3000微秒
  if ($i > 100) {
      return false;//超过100不用插入
  }
  echo "插入数据{$i}\n";
  usleep(3000);
  return true;
}
$i           = 0;
$task1Result = true;
$task2Result = true;
$task3Result = true;
while (true) {
  $task1Result && $task1Result = task1($i);
  $task2Result && $task2Result = task2($i);
  $task3Result && $task3Result = task3($i);
  if($task1Result===false&&$task2Result===false&&$task3Result===false){
      break;//全部任务完成,退出循环
  }
  $i++;
}
这段代码也 做了3件事 写入文件 发送邮件 以及插入数据

但是和上面的不同的是 这段代码将这3件事交叉执行 每个任务执行完一次之后 切换到另一个任务 如此循环
类似于这样的执行顺序 就是协程

协程 指 一种用代码实现 任务交叉执行 的逻辑 协程可以使得代码1中的3个函数交叉运行
在实现了协程的框架中 不需要通过代码2的方法实现任务交叉执行 直接可让代码1中的while(1) 执行一次后切换

协程的实现
在php中 实现协程主要使用2种方式
yield生成器实现 //www.php20.cn/article/148

swoole扩展实现

swoole实现协程
<?php
function task1(){
  for ($i=0;$i<=300;$i++){ //写入文件,大概要3000微秒
      usleep(3000);
      echo "写入文件{$i}\n";
      Co::sleep(0.001);//挂起当前协程,0.001秒后恢复//相当于切换协程
  }
}
function task2(){
  for ($i=0;$i<=500;$i++){ //发送邮件给500名会员,大概3000微秒
      usleep(3000);
      echo "发送邮件{$i}\n";
      Co::sleep(0.001);//挂起当前协程,0.001秒后恢复//相当于切换协程
  }
}
function task3(){
  for ($i=0;$i<=100;$i++){ //模拟插入100条数据,大概3000微秒
      usleep(3000);
      echo "插入数据{$i}\n";
      Co::sleep(0.001);//挂起当前协程,0.001秒后恢复//相当于切换协程
  }
}
$pid1 = go('task1');//go函数是swoole的开启协程函数 用于开启一个协程
$pid2 = go('task2');
$pid3 = go('task3');

实现切换函数
为什么要用sleep挂起协程实现切换呢
因为swoole的协程是自动的 当协程内遇上I/O操作(mysql redis)等时
swoole的协程会自动切换 运行到下一个协程任务中(切换后 I/O继续执行)
直到下一个协程任务完成或者被切换(遇上I/O) 如此反复 直到所有协程任务完成 则任务完成

协程与进程
由上面的协程执行顺序中的代码2
发现 协程其实只是运行在一个进程中的函数 只是这个函数会被切换到下一个执行

协程只是一串运行在进程中的任务代码 只是这些任务代码可以交叉运行
注意 协程并不是多任务并行 属于多任务串行 每个进程在一个时间只执行了一个任务

协程的作用域
由于协程就是进程中一串任务代码 所以它的全局变量 静态变量等变量都是共享的 包括了php的全局缓冲区
所以 在开发之中 需要特别注意协程中的全局变量 静态变量 只要某一个协程内修改了 那将会影响全部的协程 在使用ob缓冲区函数拦截的时候 也得考虑是否会被其他协程的输出给污染
用协程执行顺序中的代码2解释 当task1给$_GET['name']赋值为1时 task2读取$_GET['name']也会是1 task2将$_GET['name']赋值为2时 task3读取$_GET['name']也会是2

协程中的I/O连接
在协程中 不能共用一个I/O连接 否则会造成数据异常
用协程执行顺序中的代码2 解释 当task1 task2函数共用mysql连接 并都进行查询时 由于协程是交叉运行的 可能会造成task1获取到task1+task2查询出来的数据
也可能会丢失部分数据 被task2获取

由于协程的交叉运行机制 各个协程的I/O连接都必须是独立的 所以 需要在每个协程都创建一个连接
但由于mysql redis的连接数有限
以及连接的开启关闭需要消耗大量资源
所以 可以使用连接池方案实现共用连接(只要保证每个连接每次只有一个协程在使用即可)