Swoole 协程与 Go 协程的区别

进程、线程、协程的概念

  • 进程是什么?

进程就是应用程序的启动实例。

例如:打开一个软件,就是开启了一个进程。

进程拥有代码和打开的文件资源,数据资源,独立的内存空间。

  • 线程是什么?

线程属于进程,是程序的执行者。

一个进程至少包含一个主线程,也可以有更多的子线程。

线程有两种调度策略,一是:分时调度,二是:抢占式调度。

  • 协程是什么?

协程是轻量级线程, 协程的创建、切换、挂起、销毁全部为内存操作,消耗是非常低的。

协程是属于线程,协程是在线程里执行的。

协程的调度是用户手动切换的,所以又叫用户空间线程。

协程的调度策略是:协作式调度。

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

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

Go 协程与线程相比,开销非常小。

Go 协程的堆栈开销只用2KB,它可以根据程序的需要增大和缩小,

而线程必须指定堆栈的大小,并且堆栈的大小都是固定的。

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 * 1000, 5)." 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 语言层面就已经完全支持协程了。

Swoole 协程与 Go 协程的区别的更多相关文章

  1. 消息/事件, 同步/异步/协程, 并发/并行 协程与状态机 ——从python asyncio引发的集中学习

    我比较笨,只看用await asyncio.sleep(x)实现的例子,看再多,也还是不会. 已经在unity3d里用过coroutine了,也知道是“你执行一下,主动让出权限:我执行一下,主动让出权 ...

  2. Python的异步编程[0] -> 协程[1] -> 使用协程建立自己的异步非阻塞模型

    使用协程建立自己的异步非阻塞模型 接下来例子中,将使用纯粹的Python编码搭建一个异步模型,相当于自己构建的一个asyncio模块,这也许能对asyncio模块底层实现的理解有更大的帮助.主要参考为 ...

  3. python协程与异步协程

    在前面几个博客中我们一一对应解决了消费者消费的速度跟不上生产者,浪费我们大量的时间去等待的问题,在这里,针对业务逻辑比较耗时间的问题,我们还有除了多进程之外更优的解决方式,那就是协程和异步协程.在引入 ...

  4. Python协程与Go协程的区别二

    写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...

  5. Python协程与JavaScript协程的对比

    前言 以前没怎么接触前端对JavaScript 的异步操作不了解,现在有了点了解一查,发现 python 和 JavaScript 的协程发展史简直就是一毛一样! 这里大致做下横向对比和总结,便于对这 ...

  6. C++20协程实例:携程化的IOCP服务端/客户端

    VC支持协程已经有一段时间了,之前一直想不明白协程的意义在哪里,前几天拉屎的时候突然灵光一闪: 以下是伪代码: task server() { for (;;) { sock_context s = ...

  7. Unity在协程内部停止协程自身后代码执行问题

    当在协程内部停止自身后,后面的代码块还会继续执行,直到遇到yield语句才会终止. 经测试:停止协程,意味着就是停止yield,所以在停止协程后,yield之后的语句也就不会执行了. 代码如下: us ...

  8. python 协程与go协程的区别

    进程.线程和协程 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定义: 操作系统能够进行运算调度的最小单位.它被包含在进 ...

  9. 使用context关闭协程以及协程中的协程

    package main import ( "sync" "context" "fmt" "time" ) var wg ...

随机推荐

  1. Kubernetes TensorFlow 默认 特定 集群管理器

    Our goal is to foster an ecosystem of components and tools that relieve the burden of running applic ...

  2. 我的Android进阶之旅------>Android使用正则表达式匹配扫描指定目录下的所有媒体文件(音乐、图像、视频文件)

    今天使用正则表达式匹配指定目录下的所有媒体文件,下面将这份代码简化了,可以收藏下来,当作工具类. package match; import java.io.File; import java.uti ...

  3. Android数据格式化

    1.文件大小格式化: Log.d(TAG, Formatter.formatFileSize(this, 100)); //100 B Log.d(TAG, Formatter.formatFileS ...

  4. python 安装coreml

    2.安装pip,  下载get-pip.py, https://bootstrap.pypa.io/get-pip.py,然后Python 这个文件,如果没有权限就加sudo 3.安装coreml:这 ...

  5. HTML5/CSS3超酷环形动画菜单

    在线演示 本地下载

  6. git设置只允许特定类型的文件

    git设置只允许特定类型的文件 # 忽略所有文件 * # 不忽略目录 !*/ # 不忽略文件.gitignore和*.foo !.gitignore !*.foo

  7. SDUT OJ 河床

    河床 Time Limit: 3000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述 地理学家们经常要对一段河流进行测量分析.他们从上游开始向下游方向等距离地选择 ...

  8. POJ3693 Maximum repetition substring —— 后缀数组 重复次数最多的连续重复子串

    题目链接:https://vjudge.net/problem/POJ-3693 Maximum repetition substring Time Limit: 1000MS   Memory Li ...

  9. LightOJ - 1079 Just another Robbery —— 概率、背包

    题目链接:https://vjudge.net/problem/LightOJ-1079 1079 - Just another Robbery    PDF (English) Statistics ...

  10. php设计模式课程---5、责任链模式是什么

    php设计模式课程---5.责任链模式是什么 一.总结 一句话总结: 自己权限不够,就交给上级处理 1.选择结构怎么做到面向对象开闭原则? 也就是说if,都可以用接口的实现来实现,这样就避免了更新的时 ...