page contents

详细解说:Swoole协程与Go协程的区别!

一、进程、线程、协程 进程是什么? 进程就是应用程序的启动实例。例如:打开一个软件,就是开启了一个进程。进程拥有代码和打开的文件资源,数据资源,独立的内存空间。 线程是什么? 线...

attachments-2021-05-nxIGEL3G6099ff1b6ff74.png

一、进程、线程、协程

进程是什么?

进程就是应用程序的启动实例。
例如:打开一个软件,就是开启了一个进程。
进程拥有代码和打开的文件资源,数据资源,独立的内存空间。

线程是什么?

线程属于进程,是程序的执行者。
一个进程至少包含一个主线程,也可以有更多的子线程。
线程有两种调度策略,一是:分时调度,二是:抢占式调度。

协程是什么?

协程是轻量级线程, 协程的创建、切换、挂起、销毁全部为内存操作,消耗是非常低的。
1 协程是属于线程,协程是在线程里执行的。
2 协程的调度是用户手动切换的,所以又叫用户空间线程。
3 协程的调度策略是:协作式调度。

为什么要用协程

目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念就是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务。

其实不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。

而且由于抢占式调度执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题(事件驱动和异步程序也有同样的优点)。

因为协程是用户自己来编写调度逻辑的,对于我们的CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。

协程相对于多线程的优点

多线程编程是比较困难的, 因为调度程序任何时候都能中断线程, 必须记住保留锁, 去保护程序中重要部分, 防止多线程在执行的过程中断。

而协程默认会做好全方位保护, 以防止中断。我们必须显示产出才能让程序的余下部分运行。对协程来说, 无需保留锁, 而在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻, 只有一个协程运行。

总结下大概下面几点:

  • 无需系统内核的上下文切换,减小开销;

  • 无需原子操作锁定及同步的开销,不用担心资源共享的问题;

  • 单线程即可实现高并发,单核 CPU 即便支持上万的协程都不是问题,

所以很适合用于高并发处理,尤其是在应用在网络爬虫中。

二、Swoole 协程

Swoole 的协程客户端必须在协程的上下文环境中使用。

// 第一种情况:Request 回调本身是协程环境
$server->on('Request'function($request, $response) {
    // 创建 Mysql 协程客户端
    $mysql = new Swoole\Coroutine\MySQL();
    $mysql->connect([]);
    $mysql->query();
});

// 第二种情况:WorkerStart 回调不是协程环境
$server->on('WorkerStart'function() {
    // 需要先声明一个协程环境,才能使用协程客户端
    go(function(){
        // 创建 Mysql 协程客户端
        $mysql = new Swoole\Coroutine\MySQL();
        $mysql->connect([]);
        $mysql->query();
    });
});

Swoole 的协程是基于单线程的, 无法利用多核CPU,同一时间只有一个在调度。

// 启动 4 个协程
$n = 4;
for ($i = 0; $i < $n; $i++) {
    go(function () use ($i) {
        // 模拟 IO 等待
        Co::sleep(1);
        echo microtime(true) . ": hello $i " . PHP_EOL;
    });
};
echo "hello main \n";

// 每次输出的结果都是一样
$ php test.php 
hello main 
1558749158.0913: hello 0 
1558749158.0915: hello 3 
1558749158.0915: hello 2 
1558749158.0915: hello 1

Swoole 协程使用示例及详解

// 创建一个 Http 服务
$server = new Swoole\Http\Server('127.0.0.1'9501, SWOOLE_BASE);

// 调用 onRequest 事件回调函数时,底层会调用 C 函数 coro_create 创建一个协程,
// 同时保存这个时间点的 CPU 寄存器状态和 ZendVM stack 信息。
$server->on('Request'function($request, $response) {
    // 创建一个 Mysql 的协程客户端
    $mysql = new Swoole\Coroutine\MySQL();

    // 调用 mysql->connect 时发生 IO 操作,底层会调用 C 函数 coro_save 保存当前协程的状态,
    // 包括 Zend VM 上下文以及协程描述的信息,并调用 coro_yield 让出程序控制权,当前的请求会挂起。
    // 当协程让出控制权之后,会继续进入 EventLoop 处理其他事件,这时 Swoole 会继续去处理其他客户端发来的 Request。
    $res = $mysql->connect([
        'host'     => '127.0.0.1',
        'user'     => 'root',
        'password' => 'root',
        'database' => 'test'
    ]);

    // IO 事件完成后,MySQL 连接成功或失败,底层调用 C 函数 coro_resume 恢复对应的协程,恢复 ZendVM 上下文,继续向下执行 PHP 代码。
    if ($res == false) {
        $response->end("MySQL connect fail");
        return;
    }

    // mysql->query 的执行过程和 mysql->connect 一致,也会进行一次协程切换调度
    $ret = $mysql->query('show tables'2);

    // 所有操作完成后,调用 end 方法返回结果,并销毁此协程。
    $response->end('swoole response is ok, result='.var_export($ret, true));
});

// 启动服务
$server->start();

三、Go 的协程 goroutine

1 goroutine 是轻量级的线程,Go 语言从语言层面就支持原生协程。

2 Go 协程与线程相比,开销非常小。
3 Go 协程的堆栈开销只用2KB,它可以根据程序的需要增大和缩小,
而线程必须指定堆栈的大小,并且堆栈的大小都是固定的。

4 goroutine 是通过 GPM 调度模型实现的。

M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。
G: 表示一个 goroutine,它有自己的栈。
P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。

Go 在 runtime、系统调用等多个方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或进行系统调用时,会主动把当前协程的 CPU 转让出去,让其他协程调度执行。

Go 语言原生层面就支持协层,不需要声明协程环境。

package main

import "fmt"

func main() {
    // 直接通过 Go 关键字,就可以启动一个协程。
    go func() {
        fmt.Println("Hello Go!")
    }()
}

Go 协程是基于多线程的,可以利用多核 CPU,同一时间可能会有多个协程在执行。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 设置这个参数,可以模拟单线程与 Swoole 的协程做比较
    // 如果这个参数设置成 1,则每次输出的结果都一样。
    // runtime.GOMAXPROCS(1)

    // 启动 4 个协程
    var i int64
    for i = 0; i < 4; i++ {
        go func(i int64) {
            // 模拟 IO 等待
            time.Sleep(1 * time.Second)
            fmt.Printf("hello %d \n", i)
        }(i)
    }

    fmt.Println("hello main")

    // 等待其他的协程执行完,如果不等待的话,
    // main 执行完退出后,其他的协程也会相继退出。
    time.Sleep(10 * time.Second)
}

// 第一次输出的结果
go run test.go
hello main
hello 2 
hello 1 
hello 0 
hello 3 

// 第二次输出的结果
go run test.go
hello main
hello 2 
hello 0 
hello 3 
hello 1 

// 依次类推,每次输出的结果都不一样

Go 协程使用示例及详解

package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    "net/http"
    "time"
)
import _ "github.com/go-sql-driver/mysql"

func main() {
    dsn := fmt.Sprintf("%v:%v@(%v:%v)/%v?charset=utf8&parseTime=True&loc=Local",
        "root",
        "root",
        "127.0.0.1",
        "3306",
        "fastadmin",
    )
    db, err := gorm.Open("mysql", dsn)
    if err != nil {
        fmt.Printf("mysql connection failure, error: (%v)", err.Error())
        return
    }
    db.DB().SetMaxIdleConns(10)  // 设置连接池
    db.DB().SetMaxOpenConns(100// 设置与数据库建立连接的最大数目
    db.DB().SetConnMaxLifetime(time.Second * 7)

    http.HandleFunc("/test"func(writer http.ResponseWriter, request *http.Request) {
        // http Request 是在协程中处理的
        // 在 Go 源码 src/net/http/server.go:2851 行处 `go c.serve(ctx)` 给每个请求启动了一个协程
        var name string
        row := db.Table("fa_auth_rule").Where("id = ?"1).Select("name").Row()
        err = row.Scan(&name)
        if err != nil {
            fmt.Printf("error: %v", err)
            return
        }
        fmt.Printf("name: %v \n", name)
    })
    http.ListenAndServe("0.0.0.0:8001"nil)
}


四、案例分析

背景:

在我们的积分策略服务系统中,使用到了 mongodb 存储,但是 swoole 没有提供 mongodb 协程客户端。

那么这种场景下,在连接及操作 Mongodb 时会发生同步阻塞,无法发生协程切换,导致整个进程都会阻塞。

在这段时间内,进程将无法再处理新的请求,这使得系统的并发性大大降低。

使用同步的 mongodb 客户端

$server->on('Request'function($request, $response) {
    // swoole 没有提供协程客户端,那么只能使用同步客户端
    // 这种情况下,进程阻塞,无法切换协程
    $m = new MongoClient();    // 连接到mongodb
    $db = $m->test;            // 选择一个数据库
    $collection = $db->runoob; // 选择集合
    // 更新文档
    $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
    $cursor = $collection->find();
    foreach ($cursor as $document) {
        echo $document["title"] . "\n";
    }
}}

通过使用 Server->taskCo 来异步化对 mongodb 的操作

$server->on('Task'function (swoole_server $serv, $task_id, $worker_id, $data) {
    $m = new MongoClient();    // 连接到mongodb
    $db = $m->test;            // 选择一个数据库
    $collection = $db->runoob; // 选择集合
    // 更新文档
    $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
    $cursor = $collection->find();
    foreach ($cursor as $document) {
        $data = $document["title"];
    }
    return $data;
});

$server->on('Request'function ($request, $response) use ($server) {
    // 通过 $server->taskCo() 把对 mongodb 的操作,投递到异步 task 中。
    // 投递到异步 task 后,将发生协程切换,可以继续处理其他的请求,提供并发能力。
    $tasks[] = "hello world";
    $result = $server->taskCo($tasks, 0.5);
    $response->end('Test End, Result: '.var_export($result, true));
});
上面两种使用方式就是 Swoole 中常用的方法了。
那么我们在 Go 中怎么处理这种同步的问题呢 ?

实际上在 Go 语言中就不用担心这个问题了,如我们之前所说到的,
Go 在语言层面就已经支持协程了,只要是发生 IO 操作,网络请求都会发生协程切换。

这也就是 Go 语言天生以来就支持高并发的原因了。

package main

import (
    "fmt"
    "gopkg.in/mgo.v2"
    "net/http"
)

func main() {
    http.HandleFunc("/test"func(writer http.ResponseWriter, request *http.Request) {
        session, err := mgo.Dial("127.0.0.1:27017")
        if err != nil {
            fmt.Printf("Error: %v \n", err)
            return
        }
        session.SetMode(mgo.Monotonic, true)
        c := session.DB("test").C("runoob")
        fmt.Printf("Connect %v \n", c)
    })
    http.ListenAndServe("0.0.0.0:8001"nil)
}

并行:同一时刻,同一个 CPU 只能执行同一个任务,要同时执行多个任务,就需要有多个 CPU。

并发:CPU 切换时间任务非常快,就会感觉到有很多任务在同时执行。

五、协程 CPU 密集场景调度

我们上面说到都是基于 IO 密集场景的调度。
那么如果是 CPU 密集型的场景,应该怎么处理呢?

在 Swoole v4.3.2 版本中,已经支持了协程 CPU 密集场景的调度。

想要支持 CPU 密集调度,需要在编译时增加编译选项 --enable-scheduler-tick 开启 tick 调度器。

其次还需要我们手动声明 declare(tick=N) 语法功能来实现协程调度。

<?php
declare(ticks=1000);

$max_msec = 10;
Swoole\Coroutine::set([
    'max_exec_msec' => $max_msec,
]);

$s = microtime(1);
echo "start\n";
$flag = 1;
go(function () use (&$flag, $max_msec){
    echo "coro 1 start to loop for $max_msec msec\n";
    $i = 0;
    while($flag) {
        $i ++;
    }
    echo "coro 1 can exit\n";
});

$t = microtime(1);
$u = $t-$s;
echo "shedule use time ".round($u * 10005)." ms\n";
go(function () use (&$flag){
    echo "coro 2 set flag = false\n";
    $flag = false;
});
echo "end\n";

// 输出结果
start
coro 1 start to loop for 10 msec
shedule use time 10.2849 ms
coro 2 set flag = false
end
coro 1 can exit

Go 在 CPU 密集运算时,有可能导致协程无法抢占 CPU 会一直挂起。这时候就需要显示的调用代码 runtime.Gosched() 挂起当前协程,让出 CPU 给其他的协程。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 如果设置单线程,则第一个协程无法让出时间片
    // 第二个协程一直得不到时间片,阻塞等待。
    // runtime.GOMAXPROCS(1)

    flag := true

    go func() {
        fmt.Printf("coroutine one start \n")
        i := 0
        for flag {
            i++
            // 如果加了这行代码,协程可以让时间片
            // 这个因为 fmt.Printf 是内联函数,这是种特殊情况
            // fmt.Printf("i: %d \n", i)
        }
        fmt.Printf("coroutine one exit \n")
    }()

    go func() {
        fmt.Printf("coroutine two start \n")
        flag = false
        fmt.Printf("coroutine two exit \n")
    }()

    time.Sleep(5 * time.Second)
    fmt.Printf("end \n")
}

// 输出结果
coroutine one start 
coroutine two start 
coroutine two exit 
coroutine one exit 
end

注:time.sleep() 模拟 IO 操作,for i++ 模拟 CPU 密集运算。

总结

  • 协程是轻量级的线程,开销很小。

  • Swoole 的协程客户端需要在协程的上下文环境中使用。

  • 在 Swoole v4.3.2 版本之后,已经支持协程 CPU 密集场景调度。

  • Go 语言层面就已经完全支持协程了。

更多相关技术内容咨询欢迎前往并持续关注六星社区了解详情。

如果你想用Python开辟副业赚钱,但不熟悉爬虫与反爬虫技术,没有接单途径,也缺乏兼职经验
关注下方微信公众号:Python编程学习圈,获取价值999元全套Python入门到进阶的学习资料以及教程,还有Python技术交流群一起交流学习哦。

attachments-2022-06-WRK3nvxu62abf76a81592.jpeg

  • 发表于 2021-05-11 11:52
  • 阅读 ( 564 )
  • 分类:swoole

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
轩辕小不懂
轩辕小不懂

2403 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1478 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章