Swoole 协程与 Go 协程的区别
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 协程的区别的更多相关文章
- 消息/事件, 同步/异步/协程, 并发/并行 协程与状态机 ——从python asyncio引发的集中学习
我比较笨,只看用await asyncio.sleep(x)实现的例子,看再多,也还是不会. 已经在unity3d里用过coroutine了,也知道是“你执行一下,主动让出权限:我执行一下,主动让出权 ...
- Python的异步编程[0] -> 协程[1] -> 使用协程建立自己的异步非阻塞模型
使用协程建立自己的异步非阻塞模型 接下来例子中,将使用纯粹的Python编码搭建一个异步模型,相当于自己构建的一个asyncio模块,这也许能对asyncio模块底层实现的理解有更大的帮助.主要参考为 ...
- python协程与异步协程
在前面几个博客中我们一一对应解决了消费者消费的速度跟不上生产者,浪费我们大量的时间去等待的问题,在这里,针对业务逻辑比较耗时间的问题,我们还有除了多进程之外更优的解决方式,那就是协程和异步协程.在引入 ...
- Python协程与Go协程的区别二
写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...
- Python协程与JavaScript协程的对比
前言 以前没怎么接触前端对JavaScript 的异步操作不了解,现在有了点了解一查,发现 python 和 JavaScript 的协程发展史简直就是一毛一样! 这里大致做下横向对比和总结,便于对这 ...
- C++20协程实例:携程化的IOCP服务端/客户端
VC支持协程已经有一段时间了,之前一直想不明白协程的意义在哪里,前几天拉屎的时候突然灵光一闪: 以下是伪代码: task server() { for (;;) { sock_context s = ...
- Unity在协程内部停止协程自身后代码执行问题
当在协程内部停止自身后,后面的代码块还会继续执行,直到遇到yield语句才会终止. 经测试:停止协程,意味着就是停止yield,所以在停止协程后,yield之后的语句也就不会执行了. 代码如下: us ...
- python 协程与go协程的区别
进程.线程和协程 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定义: 操作系统能够进行运算调度的最小单位.它被包含在进 ...
- 使用context关闭协程以及协程中的协程
package main import ( "sync" "context" "fmt" "time" ) var wg ...
随机推荐
- EasyNVR H5直播流媒体解决方案前端构建之:如何播放自动适配RTMP/HLS直播播放
之前在进行EasyNVR多屏开发的时候,由于多屏功能不需要在手机端展示出来(pc多播放为RTMP,手机端播放为HLS),因此只注意到了引用videojs来进行rtmp的播放.由于不同项目需求不同,对h ...
- pycharm注册码地址
(1)地址:http://idea.lanyus.com/ (2)注意,在破解的时候,是先修改hosts文件所在路径:“C:\Windows\System32\drivers\etc\hosts”,修 ...
- 九度OJ 1075:斐波那契数列 (数字特性)
时间限制:5 秒 内存限制:32 兆 特殊判题:否 提交:3121 解决:1806 题目描述: 编写一个求斐波那契数列的递归函数,输入n值,使用该递归函数,输出如样例输出的斐波那契数列. 输入: 一个 ...
- 流畅的python学习笔记第八章:深拷贝,浅拷贝,可变参数
首先来看赋值,浅拷贝,深拷贝. 一赋值: a=['word',2,3] b=a print id(a),id(b) print [id(x) for x in a] print [id(x) for ...
- LeetCode:矩形区域【223】
LeetCode:矩形区域[223] 题目描述 在二维平面上计算出两个由直线构成的矩形重叠后形成的总面积. 每个矩形由其左下顶点和右上顶点坐标表示,如图所示. 示例: 输入: -3, 0, 3, 4, ...
- Machine Learning No.2: Linear Regression with Multiple Variables
1. notation: n = number of features x(i) = input (features) of ith training example = value of feat ...
- GDB调试core文件(3)
列出一些常见问题: 一,如何使用core文件 使用core文件 在core文件所在目录下键入: gdb -c core 它会启动GNU的调试器,来调试core文件,并且会显示生成此core文件的程序名 ...
- css3立体旋转菜单
css3立体旋转菜单,css3,3D,立体旋转,立体菜单,菜单导航,css3立体旋转菜单是一款纯css3实现的三维立体旋转导航菜单. 源码下载页:http://www.huiyi8.com/sc/71 ...
- BZOJ 2442 [Usaco2011 Open]修剪草坪:单调队列优化dp
题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=2442 题意: 有n个数a[i]从左到右排成一排. 你可以任意选数,但是连续的数不能超过k个 ...
- 分享知识-快乐自己:Excel快速导入Oracle 数据库
需求: oracle 数据库有一个student表,现有一个excel表:student.xlsx,需导入oracle数据库student表中. student表的拥有者是c##MLQ1 密码为:x ...