1、概念
- 傳統(tǒng)單進(jìn)程OS(單核):
是按順序執(zhí)行進(jìn)程A->B->C,
缺點(diǎn)是單一執(zhí)行流程,進(jìn)程阻塞會(huì)浪費(fèi)CPU時(shí)間資源。

- 多進(jìn)程OS:
進(jìn)程ABC由CPU進(jìn)行輪詢調(diào)度,同時(shí)規(guī)定時(shí)間片執(zhí)行時(shí)間,時(shí)間到時(shí)CPU會(huì)強(qiáng)行切換進(jìn)程;
優(yōu)點(diǎn)是并發(fā)執(zhí)行,解決了阻塞問題;
缺點(diǎn)是:
1、進(jìn)程切換需要保存進(jìn)程的當(dāng)前狀態(tài),CPU有切換成本(浪費(fèi)),而且進(jìn)程/線程數(shù)量越多,切換成本越大;
2、多線程存在同步競爭問題(鎖、資源爭搶),造成開發(fā)設(shè)計(jì)愈發(fā)復(fù)雜;
3、每個(gè)線程承載一個(gè)任務(wù)時(shí),CPU調(diào)度高,會(huì)造成高內(nèi)存占用;

- 協(xié)程(Coroutine):
把一個(gè)線程分為用戶空間(user space)和內(nèi)核空間(kernel space)并綁定在一起,CPU只關(guān)注內(nèi)核空間;
此時(shí)用戶空間用來執(zhí)行用戶線程,這個(gè)用戶線程就叫做協(xié)程,即co-routine,內(nèi)核空間用來執(zhí)行內(nèi)核線程,即thread;
除此之外,我們還需要一個(gè)調(diào)度器用來綁定、關(guān)聯(lián)并調(diào)度thread和coroutine;


- Golang的GMP模型:
G:Goroutine - 協(xié)程
M:Machine - 內(nèi)核線程
P:Processer - 調(diào)度器

2、實(shí)現(xiàn)
2.1、 原生PHP實(shí)現(xiàn)簡單的協(xié)程
- 核心在于實(shí)現(xiàn)一個(gè)調(diào)度器,并在協(xié)程執(zhí)行邏輯中使用yield出讓協(xié)程的CPU使用權(quán);
一個(gè)調(diào)度器demo(源自 https://segmentfault.com/a/1190000012457145)
/**
* Class Scheduler
*/
Class Scheduler
{
/**
* @var SplQueue
*/
protected $taskQueue;
/**
* @var int
*/
protected $tid = 0;
/**
* Scheduler constructor.
*/
public function __construct()
{
/* 原理就是維護(hù)了一個(gè)隊(duì)列,
* 前面說過,從編程角度上看,協(xié)程的思想本質(zhì)上就是控制流的主動(dòng)讓出(yield)和恢復(fù)(resume)機(jī)制
* */
$this->taskQueue = new SplQueue();
}
/**
* 增加一個(gè)任務(wù)
*
* @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;
}
/**
* 把任務(wù)進(jìn)入隊(duì)列
*
* @param Task $task
*/
public function schedule(Task $task)
{
$this->taskQueue->enqueue($task);
}
/**
* 運(yùn)行調(diào)度器
*/
public function run()
{
while (!$this->taskQueue->isEmpty()) {
// 任務(wù)出隊(duì)
$task = $this->taskQueue->dequeue();
$res = $task->run(); // 運(yùn)行任務(wù)直到 yield
if (!$task->isFinished()) {
$this->schedule($task); // 任務(wù)如果還沒完全執(zhí)行完畢,入隊(duì)等下次執(zhí)行
}
}
}
}
調(diào)度器與協(xié)程的使用
<?php
function task1() {
for ($i = 1; $i <= 10; ++$i) {
echo "This is task 1 iteration $i.\n";
yield; // 主動(dòng)讓出CPU的執(zhí)行權(quán)
}
}
function task2() {
for ($i = 1; $i <= 5; ++$i) {
echo "This is task 2 iteration $i.\n";
yield; // 主動(dòng)讓出CPU的執(zhí)行權(quán)
}
}
$scheduler = new Scheduler; // 實(shí)例化一個(gè)調(diào)度器
$scheduler->addTask(task1()); // 添加不同的閉包函數(shù)作為任務(wù)
$scheduler->addTask(task2());
$scheduler->run();
- 個(gè)人認(rèn)為,這種方式實(shí)現(xiàn)的coroutine,功能非常局限,且設(shè)計(jì)復(fù)雜,沒有太多的實(shí)戰(zhàn)價(jià)值。
2.2 PHP-Swoole
- 環(huán)境要求:
PHP版本要求:>= 7.0
基于Server、Http\Server、WebSocket\Server進(jìn)行開發(fā),底層在onRequet, onReceive, onConnect等事件回調(diào)之前自動(dòng)創(chuàng)建一個(gè)協(xié)程,在回調(diào)函數(shù)中使用協(xié)程API
使用Coroutine::create或go方法創(chuàng)建協(xié)程,在創(chuàng)建的協(xié)程中使用協(xié)程API
function Swoole\Coroutine::create(callable $function, ...$args) : int|false;
或
function go(callable $function, ...$args) : int|false; // 短名API
創(chuàng)建失敗返回false
創(chuàng)建成功返回協(xié)程的ID
由于底層會(huì)優(yōu)先執(zhí)行子協(xié)程的代碼,因此只有子協(xié)程掛起時(shí),Coroutine::create才會(huì)返回,繼續(xù)執(zhí)行當(dāng)前協(xié)程的代碼。
- 執(zhí)行順序:
在一個(gè)協(xié)程中使用go嵌套創(chuàng)建新的協(xié)程。因?yàn)镾woole的協(xié)程是單線程模型,因此:
使用go創(chuàng)建的子協(xié)程會(huì)優(yōu)先執(zhí)行,子協(xié)程執(zhí)行完畢或掛起時(shí),將重新回到父協(xié)程向下執(zhí)行代碼
如果子協(xié)程掛起后,父協(xié)程退出,不影響子協(xié)程的執(zhí)行
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";
});
- 協(xié)程開銷:
協(xié)程需要?jiǎng)?chuàng)建單獨(dú)的內(nèi)存棧,在PHP-7.2版本中底層會(huì)分配8K的stack來存儲(chǔ)協(xié)程的變量,zval的尺寸為16字節(jié),因此8K的stack最大可以保存512個(gè)變量。協(xié)程棧內(nèi)存占用超過8K后ZendVM會(huì)自動(dòng)擴(kuò)容。
協(xié)程退出時(shí)會(huì)釋放申請的stack內(nèi)存。
PHP-7.1、PHP-7.0默認(rèn)會(huì)分配256K棧內(nèi)存
可調(diào)用Co::set(['stack_size' => 4096])修改默認(rèn)的棧內(nèi)存尺寸
2.3 Golang
- 關(guān)鍵字
go定義一個(gè)golang的協(xié)程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() {
//創(chuàng)建一個(gè)go程 去執(zhí)行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、對(duì)比
Swoole4與Go協(xié)程在設(shè)計(jì)上是完全一致的,均是stackful的,每個(gè)協(xié)程擁有獨(dú)立的運(yùn)行棧。協(xié)程調(diào)度器使用匯編代碼,切換協(xié)程上下文。
Swoole4的協(xié)程調(diào)度器是單線程的,因此不存在數(shù)據(jù)同步問題,同一時(shí)間只會(huì)有一個(gè)協(xié)程在運(yùn)行
Go協(xié)程調(diào)度器是多線程的,同一時(shí)間可能會(huì)有多個(gè)協(xié)程同時(shí)執(zhí)行
因此在Swoole4協(xié)程中操作全局變量是不需要加鎖的。而Go的程序由于依然是類似Java的多線程模式,因此務(wù)必要對(duì)臨界資源加鎖,避免出現(xiàn)數(shù)據(jù)同步問題。或者使用官方sync包提供的各種并發(fā)容器。實(shí)際上Go的chan和并發(fā)容器,底層仍然使用了Mutex進(jìn)行鎖操作,鎖的爭搶是普遍存在的。
Swoole4由于是單線程多進(jìn)程的,底層沒有使用任何Mutex鎖,不存在鎖的爭搶。 同樣帶來的問題是,沒有超全局變量。只有進(jìn)程級(jí)全局變量,讀寫PHP全局變量只在當(dāng)前進(jìn)程內(nèi)有效。如果希望多進(jìn)程共享數(shù)據(jù),有3種解決方案:
1、使用Table和Atomic對(duì)象,或者其他共享內(nèi)存數(shù)據(jù)結(jié)構(gòu)
2、使用IPC進(jìn)程間通信
3、借助存儲(chǔ)實(shí)現(xiàn)數(shù)據(jù)的共享和中轉(zhuǎn),如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是允許這樣操作的,實(shí)際上這個(gè)可能會(huì)存在嚴(yán)重問題。socket讀寫操作產(chǎn)生并發(fā),可能產(chǎn)生數(shù)據(jù)包錯(cuò)亂。
Swoole中禁止了這種行為。不允許多個(gè)協(xié)程同時(shí)讀取同一個(gè)Socket。否則會(huì)產(chǎn)生致命錯(cuò)誤。
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個(gè)協(xié)程同時(shí)操作$db對(duì)象,可能會(huì)產(chǎn)生嚴(yán)重錯(cuò)誤。底層會(huì)直接拋出致命錯(cuò)誤,錯(cuò)誤信息為:
"%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."
錯(cuò)誤碼:SW_ERROR_CO_HAS_BEEN_BOUND
使用Channel或SplQueue實(shí)現(xiàn)連接池,管理資源對(duì)象,就可以很好地解決此問題。