美文网首页
协程coroutine的PHP与Golang实现

协程coroutine的PHP与Golang实现

作者: Separes | 来源:发表于2021-05-23 13:21 被阅读0次

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实现连接池,管理资源对象,就可以很好地解决此问题。


相关文章

  • 协程coroutine的PHP与Golang实现

    1、概念 传统单进程OS(单核):是按顺序执行进程A->B->C,缺点是单一执行流程,进程阻塞会浪费CPU时间资源...

  • 4-7

    协程 协程,又称微线程,纤程。英文名Coroutine。 协程是啥 协程是python个中另外一种实现多任务的方式...

  • 协程

    协程 协程,又称微线程,纤程。英文名Coroutine。 协程是啥 协程是python个中另外一种实现多任务的方式...

  • kotlin Coroutine原理

    Coroutine协程是kotlin实现的一种异步执行逻辑的方式,相对与传统的线程,协程更加简洁,高效,占用资源少...

  • 协程介绍

    协程 协程,又称微线程,纤程。英文名Coroutine。 1.协程是什么? 协程是python个中另外一种实现多任...

  • 协程——yield

    协程: 协程,又称微线程,纤程。英文名Coroutine。 协程是python个中另外一种实现多任务的方式,只不过...

  • golang-协程 Coroutine

    轻量级 “线程”!非抢占式多任务处理,由协程主动交出控制权! 抢占式:是有操作系统主动切换线程执行非抢占式:是由我...

  • 协程

    协程 协程,又称微线程,纤程。英文名Coroutine。 协程是python个中另外一种实现多任务的方式,只不过比...

  • Python多任务-协程

    协程 协程,又称微线程,纤程。英文名Coroutine。 协程是python个中另外一种实现多任务的方式,只不过比...

  • 利用高阶函数来实现协程(Racordon 1812.08278)

    论文链接 协程的概念与实现 协程(coroutine)这一概念最早在1963年由Convay提出,虽然在上世纪80...

网友评论

      本文标题:协程coroutine的PHP与Golang实现

      本文链接:https://www.haomeiwen.com/subject/nuqojltx.html