• 协程
    • 协程执行顺序
    • 协程的实现
    • 协程与进程
    • 协程的作用域
    • 协程中的I/O连接

    协程

    协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
    一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
    协程

    协程执行顺序

    首先,我们来看一个原生php代码:

    1. <?php
    2. function task1(){
    3. for ($i=0;$i<=300;$i++){
    4. //写入文件,大概要3000微秒
    5. usleep(3000);
    6. echo "写入文件{$i}\n";
    7. }
    8. }
    9. function task2(){
    10. for ($i=0;$i<=500;$i++){
    11. //发送邮件给500名会员,大概3000微秒
    12. usleep(3000);
    13. echo "发送邮件{$i}\n";
    14. }
    15. }
    16. function task3(){
    17. for ($i=0;$i<=100;$i++){
    18. //模拟插入100条数据,大概3000微秒
    19. usleep(3000);
    20. echo "插入数据{$i}\n";
    21. }
    22. }
    23. task1();
    24. task2();
    25. task3();

    在这个代码中,我们主要做了3件事:写入文件,发送邮件,以及插入数据.
    再看下面这段代码:

    1. <?php
    2. function task1($i)
    3. {
    4. //使用$i标识 写入文件,大概要3000微秒
    5. if ($i > 300) {
    6. return false;//超过300不用写了
    7. }
    8. echo "写入文件{$i}\n";
    9. usleep(3000);
    10. return true;
    11. }
    12. function task2($i)
    13. {
    14. //使用$i标识 发送邮件,大概要3000微秒
    15. if ($i > 500) {
    16. return false;//超过500不用发送了
    17. }
    18. echo "发送邮件{$i}\n";
    19. usleep(3000);
    20. return true;
    21. }
    22. function task3($i)
    23. {
    24. //使用$i标识 插入数据,大概要3000微秒
    25. if ($i > 100) {
    26. return false;//超过100不用插入
    27. }
    28. echo "插入数据{$i}\n";
    29. usleep(3000);
    30. return true;
    31. }
    32. $i = 0;
    33. $task1Result = true;
    34. $task2Result = true;
    35. $task3Result = true;
    36. while (true) {
    37. $task1Result && $task1Result = task1($i);
    38. $task2Result && $task2Result = task2($i);
    39. $task3Result && $task3Result = task3($i);
    40. if($task1Result===false&&$task2Result===false&&$task3Result===false){
    41. break;//全部任务完成,退出循环
    42. }
    43. $i++;
    44. }

    这段代码也是做了3件事,写入文件,发送邮件,以及插入数据,但是和上面的不同的是,这段代码将这3件事交叉执行,每个任务执行完一次之后,切换到另一个任务,如此循环.
    类似于这样的执行顺序,就是协程.

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

    协程的实现

    在php中,实现协程主要使用2种方式:

    • yield生成器实现 (详细原理可查看http://www.php20.cn/article/148)
    • swoole扩展实现

    swoole实现协程代码:

    1. <?php
    2. function task1(){
    3. for ($i=0;$i<=300;$i++){
    4. //写入文件,大概要3000微秒
    5. usleep(3000);
    6. echo "写入文件{$i}\n";
    7. Co::sleep(0.001);//挂起当前协程,0.001秒后恢复//相当于切换协程
    8. }
    9. }
    10. function task2(){
    11. for ($i=0;$i<=500;$i++){
    12. //发送邮件给500名会员,大概3000微秒
    13. usleep(3000);
    14. echo "发送邮件{$i}\n";
    15. Co::sleep(0.001);//挂起当前协程,0.001秒后恢复//相当于切换协程
    16. }
    17. }
    18. function task3(){
    19. for ($i=0;$i<=100;$i++){
    20. //模拟插入100条数据,大概3000微秒
    21. usleep(3000);
    22. echo "插入数据{$i}\n";
    23. Co::sleep(0.001);//挂起当前协程,0.001秒后恢复//相当于切换协程
    24. }
    25. }
    26. $pid1 = go('task1');//go函数是swoole的开启协程函数,用于开启一个协程
    27. $pid2 = go('task2');
    28. $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的连接数有限,以及连接的开启关闭需要消耗大量资源,所以我们可以使用连接池方案实现共用连接(只要保证每个连接每次只有一个协程在使用即可)