协程coroutine的PHP与Golang实现

1、概念

  • 传统单进程OS(单核):
    是按顺序执行进程A->B->C,
    缺点是单一执行流程,进程阻塞会浪费CPU时间资源。
单进程OS.png
  • 多进程OS:
    进程ABC由CPU进行轮询调度,同时规定时间片执行时间,时间到时CPU会强行切换进程;
    优点是并发执行,解决了阻塞问题;
    缺点是:
    1、进程切换需要保存进程的当前状态,CPU有切换成本(浪费),而且进程/线程数量越多,切换成本越大;
    2、多线程存在同步竞争问题(锁、资源争抢),造成开发设计愈发复杂;
    3、每个线程承载一个任务时,CPU调度高,会造成高内存占用;
多进程OS.png
  • 协程(Coroutine):
    把一个线程分为用户空间(user space)和内核空间(kernel space)并绑定在一起,CPU只关注内核空间;
    此时用户空间用来执行用户线程,这个用户线程就叫做协程,即co-routine,内核空间用来执行内核线程,即thread;
    除此之外,我们还需要一个调度器用来绑定、关联并调度thread和coroutine;
将单线程拆分为thread和coroutine
协程的调度模型
  • Golang的GMP模型:
    G:Goroutine - 协程
    M:Machine - 内核线程
    P:Processer - 调度器
go的GMP模型,源自 https://www.bilibili.com/video/BV1gf4y1r79E?p=26

2、实现

2.1、 原生PHP实现简单的协程

  • 核心在于实现一个调度器,并在协程执行逻辑中使用yield出让协程的CPU使用权;

一个调度器demo(源自 https://segmentfault.com/a/1190000012457145

/**
 * Class Scheduler
 */
Class Scheduler
{
    /**
     * @var SplQueue
     */
    protected $taskQueue;
    /**
     * @var int
     */
    protected $tid = 0;

    /**
     * Scheduler constructor.
     */
    public function __construct()
    {
        /* 原理就是维护了一个队列,
         * 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制
         * */
        $this->taskQueue = new SplQueue();
    }

    /**
     * 增加一个任务
     *
     * @param Generator $task
     * @return int
     */
    public function addTask(Generator $task)
    {
        $tid = $this->tid;
        $task = new Task($tid, $task);
        $this->taskQueue->enqueue($task);
        $this->tid++;
        return $tid;
    }

    /**
     * 把任务进入队列
     *
     * @param Task $task
     */
    public function schedule(Task $task)
    {
        $this->taskQueue->enqueue($task);
    }

    /**
     * 运行调度器
     */
    public function run()
    {
        while (!$this->taskQueue->isEmpty()) {
            // 任务出队
            $task = $this->taskQueue->dequeue();
            $res = $task->run(); // 运行任务直到 yield

            if (!$task->isFinished()) {
                $this->schedule($task); // 任务如果还没完全执行完毕,入队等下次执行
            }
        }
    }
}

调度器与协程的使用

<?php
function task1() {
    for ($i = 1; $i <= 10; ++$i) {
        echo "This is task 1 iteration $i.\n";
        yield; // 主动让出CPU的执行权
    }
}
 
function task2() {
    for ($i = 1; $i <= 5; ++$i) {
        echo "This is task 2 iteration $i.\n";
        yield; // 主动让出CPU的执行权
    }
}
 
$scheduler = new Scheduler; // 实例化一个调度器
$scheduler->addTask(task1()); // 添加不同的闭包函数作为任务
$scheduler->addTask(task2());
$scheduler->run();

  • 个人认为,这种方式实现的coroutine,功能非常局限,且设计复杂,没有太多的实战价值。

2.2 PHP-Swoole

  • 环境要求:

PHP版本要求:>= 7.0
基于Server、Http\Server、WebSocket\Server进行开发,底层在onRequet, onReceive, onConnect等事件回调之前自动创建一个协程,在回调函数中使用协程API
使用Coroutine::create或go方法创建协程,在创建的协程中使用协程API

function Swoole\Coroutine::create(callable $function, ...$args) : int|false;
或
function go(callable $function, ...$args) : int|false; // 短名API

创建失败返回false
创建成功返回协程的ID
由于底层会优先执行子协程的代码,因此只有子协程挂起时,Coroutine::create才会返回,继续执行当前协程的代码。

  • 执行顺序:

在一个协程中使用go嵌套创建新的协程。因为Swoole的协程是单线程模型,因此:
使用go创建的子协程会优先执行,子协程执行完毕或挂起时,将重新回到父协程向下执行代码
如果子协程挂起后,父协程退出,不影响子协程的执行

go(function() {
    go(function () {
        co::sleep(3.0);
        go(function () {
            co::sleep(2.0);
            echo "co[3] end\n";
        });
        echo "co[2] end\n";
    });

    co::sleep(1.0);
    echo "co[1] end\n";
});
  • 协程开销:

协程需要创建单独的内存栈,在PHP-7.2版本中底层会分配8K的stack来存储协程的变量,zval的尺寸为16字节,因此8K的stack最大可以保存512个变量。协程栈内存占用超过8K后ZendVM会自动扩容。
协程退出时会释放申请的stack内存。
PHP-7.1、PHP-7.0默认会分配256K栈内存
可调用Co::set(['stack_size' => 4096])修改默认的栈内存尺寸

2.3 Golang

  • 关键字go 定义一个golang的协程goroutine;

package main

import (
    "fmt"
    "time"
)

//子goroutine
func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new Goroutine : i = %d\n", i)
        time.Sleep(1 * time.Second)
    }
}

//主goroutine
func main() {
    //创建一个go程 去执行newTask() 流程
    go newTask()

    fmt.Println("main goroutine exit")

    /*
        i := 0
        for {
            i++
            fmt.Printf("main goroutine: i = %d\n", i)
            time.Sleep(1 * time.Second)
        }
    */
}


3、对比

  • Swoole4与Go协程在设计上是完全一致的,均是stackful的,每个协程拥有独立的运行栈。协程调度器使用汇编代码,切换协程上下文。

  • Swoole4的协程调度器是单线程的,因此不存在数据同步问题,同一时间只会有一个协程在运行
    Go协程调度器是多线程的,同一时间可能会有多个协程同时执行
    因此在Swoole4协程中操作全局变量是不需要加锁的。而Go的程序由于依然是类似Java的多线程模式,因此务必要对临界资源加锁,避免出现数据同步问题。或者使用官方sync包提供的各种并发容器。

  • 实际上Go的chan和并发容器,底层仍然使用了Mutex进行锁操作,锁的争抢是普遍存在的。

  • Swoole4由于是单线程多进程的,底层没有使用任何Mutex锁,不存在锁的争抢。 同样带来的问题是,没有超全局变量。只有进程级全局变量,读写PHP全局变量只在当前进程内有效。如果希望多进程共享数据,有3种解决方案:
    1、使用Table和Atomic对象,或者其他共享内存数据结构
    2、使用IPC进程间通信
    3、借助存储实现数据的共享和中转,如Redis、MySQL或文件操作

Go
func test() {
    db := new(database)
    close := db.connect()

    go func(db) {
        db.query(sql);
    } (db);

    go func(db) {
        db.query(sql);
    } (db);
}

Go是允许这样操作的,实际上这个可能会存在严重问题。socket读写操作产生并发,可能产生数据包错乱。

Swoole中禁止了这种行为。不允许多个协程同时读取同一个Socket。否则会产生致命错误。

PHP
function test() {
    $db = new Database;
    $db->connect('127.0.0.1', 6379);

    go(function () use ($db) {
        $db->query($sql);
    });

    go(function () use ($db) {
        $db->query($sql);
    });
}

以上代码中有2个协程同时操作$db对象,可能会产生严重错误。底层会直接抛出致命错误,错误信息为:

"%s has already been bound to another coroutine#%ld,
reading or writing of the same socket in multiple coroutines at the same time is not allowed."
错误码:SW_ERROR_CO_HAS_BEEN_BOUND

使用Channel或SplQueue实现连接池,管理资源对象,就可以很好地解决此问题。


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容