什么是進程?
進程就是應(yīng)用程序的啟動實例。獨立的文件資源,數(shù)據(jù)資源,內(nèi)存空間。
什么是線程?
線程屬于進程,是程序的執(zhí)行者。一個進程至少包含一個主線程,也可以有更多的子線程。線程有兩種調(diào)度策略,一是:分時調(diào)度,二是:搶占式調(diào)度。
什么是協(xié)程?
協(xié)程是輕量級線程,協(xié)程也是屬于線程,協(xié)程是在線程里執(zhí)行的。協(xié)程的調(diào)度是用戶手動切換的,所以又叫用戶空間線程。協(xié)程的創(chuàng)建、切換、掛起、銷毀全部為內(nèi)存操作,消耗是非常低的。協(xié)程的調(diào)度策略是:協(xié)作式調(diào)度。
Swoole 協(xié)程的原理
Swoole4 由于是單線程多進程的,同一時間同一個進程只會有一個協(xié)程在運行。
Swoole server 接收數(shù)據(jù)在 worker 進程觸發(fā) onReceive 回調(diào),產(chǎn)生一個攜程。Swoole 為每個請求創(chuàng)建對應(yīng)攜程。協(xié)程中也能創(chuàng)建子協(xié)程。
協(xié)程在底層實現(xiàn)上是單線程的,因此同一時間只有一個協(xié)程在工作,協(xié)程的執(zhí)行是串行的。
因此多任務(wù)多協(xié)程執(zhí)行時,一個協(xié)程正在運行時,其他協(xié)程會停止工作。當(dāng)前協(xié)程執(zhí)行阻塞 IO 操作時會掛起,底層調(diào)度器會進入事件循環(huán)。當(dāng)有 IO 完成事件時,底層調(diào)度器恢復(fù)事件對應(yīng)的協(xié)程的執(zhí)行。。所以協(xié)程不存在 IO 耗時,非常適合高并發(fā) IO 場景。(如下圖)

Swoole 的協(xié)程執(zhí)行流程
協(xié)程沒有 IO 等待 正常執(zhí)行 PHP 代碼,不會產(chǎn)生執(zhí)行流程切換
協(xié)程遇到 IO 等待 立即將控制權(quán)切,待 IO 完成后,重新將執(zhí)行流切回原來協(xié)程切出的點
協(xié)程并行協(xié)程依次執(zhí)行,同上一個邏輯
協(xié)程嵌套執(zhí)行流程由外向內(nèi)逐層進入,直到發(fā)生 IO,然后切到外層協(xié)程,父協(xié)程不會等待子協(xié)程結(jié)束
協(xié)程的執(zhí)行順序
先來看看基礎(chǔ)的例子:
go(function () {
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
echo "hello go2 \n";
});
go() 是 \Co::create() 的縮寫, 用來創(chuàng)建一個協(xié)程, 接受 callback 作為參數(shù), callback 中的代碼, 會在這個新建的協(xié)程中執(zhí)行.
備注: \Swoole\Coroutine 可以簡寫為 \Co
上面的代碼執(zhí)行結(jié)果:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello go1
hello main
hello go2
執(zhí)行結(jié)果和我們平時寫代碼的順序, 好像沒啥區(qū)別. 實際執(zhí)行過程:
運行此段代碼, 系統(tǒng)啟動一個新進程
遇到
go(), 當(dāng)前進程中生成一個協(xié)程, 協(xié)程中輸出heelo go1, 協(xié)程退出進程繼續(xù)向下執(zhí)行代碼, 輸出
hello main再生成一個協(xié)程, 協(xié)程中輸出
heelo go2, 協(xié)程退出
運行此段代碼, 系統(tǒng)啟動一個新進程. 如果不理解這句話, 你可以使用如下代碼:
// co.php
<?php
sleep(100);
執(zhí)行并使用 ps aux 查看系統(tǒng)中的進程:
root@b98940b00a9b /v/w/c/p/swoole# php co.php &
?
root@b98940b00a9b /v/w/c/p/swoole# ps aux
PID USER TIME COMMAND
1 root 0:00 php -a
10 root 0:00 sh
19 root 0:01 fish
749 root 0:00 php co.php
760 root 0:00 ps aux
?
我們來稍微改一改, 體驗協(xié)程的調(diào)度:
use Co;
go(function () {
Co::sleep(1); // 只新增了一行代碼
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
echo "hello go2 \n";
});
\Co::sleep() 函數(shù)功能和 sleep() 差不多, 但是它模擬的是 IO等待(IO后面會細講). 執(zhí)行的結(jié)果如下:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello main
hello go2
hello go1
怎么不是順序執(zhí)行的呢? 實際執(zhí)行過程:
- 運行此段代碼, 系統(tǒng)啟動一個新進程
- 遇到
go(), 當(dāng)前進程中生成一個協(xié)程 - 協(xié)程中遇到 IO阻塞 (這里是
Co::sleep()模擬出的 IO等待), 協(xié)程讓出控制, 進入?yún)f(xié)程調(diào)度隊列 - 進程繼續(xù)向下執(zhí)行, 輸出
hello main - 執(zhí)行下一個協(xié)程, 輸出
hello go2 - 之前的協(xié)程準(zhǔn)備就緒, 繼續(xù)執(zhí)行, 輸出
hello go1
到這里, 已經(jīng)可以看到 swoole 中 協(xié)程與進程的關(guān)系, 以及 協(xié)程的調(diào)度, 我們再改一改剛才的程序:
go(function () {
Co::sleep(1);
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
Co::sleep(1);
echo "hello go2 \n";
});
我想你已經(jīng)知道輸出是什么樣子了:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello main
hello go1
hello go2
?
協(xié)程快在哪? 減少IO阻塞導(dǎo)致的性能損失
大家可能聽到使用協(xié)程的最多的理由, 可能就是 協(xié)程快. 那看起來和平時寫得差不多的代碼, 為什么就要快一些呢? 一個常見的理由是, 可以創(chuàng)建很多個協(xié)程來執(zhí)行任務(wù), 所以快. 這種說法是對的, 不過還停留在表面.
首先, 一般的計算機任務(wù)分為 2 種:
- CPU密集型, 比如加減乘除等科學(xué)計算
- IO 密集型, 比如網(wǎng)絡(luò)請求, 文件讀寫等
其次, 高性能相關(guān)的 2 個概念:
- 并行: 同一個時刻, 同一個 CPU 只能執(zhí)行同一個任務(wù), 要同時執(zhí)行多個任務(wù), 就需要有多個 CPU 才行
- 并發(fā): 由于 CPU 切換任務(wù)非??? 快到人類可以感知的極限, 就會有很多任務(wù) 同時執(zhí)行 的錯覺
了解了這些, 我們再來看協(xié)程, 協(xié)程適合的是 IO 密集型 應(yīng)用, 因為協(xié)程在 IO阻塞 時會自動調(diào)度, 減少IO阻塞導(dǎo)致的時間損失.
我們可以對比下面三段代碼:
- 普通版: 執(zhí)行 4 個任務(wù)
$n = 4;
for ($i = 0; $i < $n; $i++) {
sleep(1);
echo microtime(true) . ": hello $i \n";
};
echo "hello main \n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.php
1528965075.4608: hello 0
1528965076.461: hello 1
1528965077.4613: hello 2
1528965078.4616: hello 3
hello main
real 0m 4.02s
user 0m 0.01s
sys 0m 0.00s
?
- 單個協(xié)程版:
$n = 4;
go(function () use ($n) {
for ($i = 0; $i < $n; $i++) {
Co::sleep(1);
echo microtime(true) . ": hello $i \n";
};
});
echo "hello main \n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.php
hello main
1528965150.4834: hello 0
1528965151.4846: hello 1
1528965152.4859: hello 2
1528965153.4872: hello 3
real 0m 4.03s
user 0m 0.00s
sys 0m 0.02s
?
- 多協(xié)程版: 見證奇跡的時刻
$n = 4;
for ($i = 0; $i < $n; $i++) {
go(function () use ($i) {
Co::sleep(1);
echo microtime(true) . ": hello $i \n";
});
};
echo "hello main \n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.php
hello main
1528965245.5491: hello 0
1528965245.5498: hello 3
1528965245.5502: hello 2
1528965245.5506: hello 1
real 0m 1.02s
user 0m 0.01s
sys 0m 0.00s
?
為什么時間有這么大的差異呢:
普通寫法, 會遇到 IO阻塞 導(dǎo)致的性能損失
單協(xié)程: 盡管 IO阻塞 引發(fā)了協(xié)程調(diào)度, 但當(dāng)前只有一個協(xié)程, 調(diào)度之后還是執(zhí)行當(dāng)前協(xié)程
多協(xié)程: 真正發(fā)揮出了協(xié)程的優(yōu)勢, 遇到 IO阻塞 時發(fā)生調(diào)度, IO就緒時恢復(fù)運行
我們將多協(xié)程版稍微修改一下:
- 多協(xié)程版2: CPU密集型
$n = 4;
for ($i = 0; $i < $n; $i++) {
go(function () use ($i) {
// Co::sleep(1);
sleep(1);
echo microtime(true) . ": hello $i \n";
});
};
echo "hello main \n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.php
1528965743.4327: hello 0
1528965744.4331: hello 1
1528965745.4337: hello 2
1528965746.4342: hello 3
hello main
real 0m 4.02s
user 0m 0.01s
sys 0m 0.00s
?
只是將 Co::sleep() 改成了 sleep(), 時間又和普通版差不多了. 因為:
sleep()可以看做是 CPU密集型任務(wù), 不會引起協(xié)程的調(diào)度Co::sleep()模擬的是 IO密集型任務(wù), 會引發(fā)協(xié)程的調(diào)度
這也是為什么, 協(xié)程適合 IO密集型 的應(yīng)用.
再來一組對比的例子: 使用 redis
// 同步版, redis使用時會有 IO 阻塞
$cnt = 2000;
for ($i = 0; $i < $cnt; $i++) {
$redis = new \Redis();
$redis->connect('redis');
$redis->auth('123');
$key = $redis->get('key');
}
// 單協(xié)程版: 只有一個協(xié)程, 并沒有使用到協(xié)程調(diào)度減少 IO 阻塞
go(function () use ($cnt) {
for ($i = 0; $i < $cnt; $i++) {
$redis = new Co\Redis();
$redis->connect('redis', 6379);
$redis->auth('123');
$redis->get('key');
}
});
// 多協(xié)程版, 真正使用到協(xié)程調(diào)度帶來的 IO 阻塞時的調(diào)度
for ($i = 0; $i < $cnt; $i++) {
go(function () {
$redis = new Co\Redis();
$redis->connect('redis', 6379);
$redis->auth('123');
$redis->get('key');
});
}
性能對比:
# 多協(xié)程版
root@0124f915c976 /v/w/c/p/swoole# time php co.php
real 0m 0.54s
user 0m 0.04s
sys 0m 0.23s
?
# 同步版
root@0124f915c976 /v/w/c/p/swoole# time php co.php
real 0m 1.48s
user 0m 0.17s
sys 0m 0.57s
?
swoole 協(xié)程和 go 協(xié)程對比: 單進程 vs 多線程
接觸過 go 協(xié)程的 coder, 初始接觸 swoole 的協(xié)程會有點 懵, 比如對比下面的代碼:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("hello go")
}()
fmt.Println("hello main")
time.Sleep(time.Second)
}
> 14:11 src $ go run test.go
hello main
hello go
剛寫 go 協(xié)程的 coder, 在寫這個代碼的時候會被告知不要忘了 time.Sleep(time.Second), 否則看不到輸出 hello go, 其次, hello go與 hello main 的順序也和 swoole 中的協(xié)程不一樣.
原因就在于 swoole 和 go 中, 實現(xiàn)協(xié)程調(diào)度的模型不同.
上面 go 代碼的執(zhí)行過程:
- 運行 go 代碼, 系統(tǒng)啟動一個新進程
- 查找
package main, 然后執(zhí)行其中的func mian() - 遇到協(xié)程, 交給協(xié)程調(diào)度器執(zhí)行
- 繼續(xù)向下執(zhí)行, 輸出
hello main - 如果不添加
time.Sleep(time.Second), main 函數(shù)執(zhí)行完, 程序結(jié)束, 進程退出, 導(dǎo)致調(diào)度中的協(xié)程也終止
go 中的協(xié)程, 使用的 MPG 模型:
- M 指的是 Machine, 一個M直接關(guān)聯(lián)了一個內(nèi)核線程
- P 指的是 processor, 代表了M所需的上下文環(huán)境, 也是處理用戶級代碼邏輯的處理器
- G 指的是 Goroutine, 其實本質(zhì)上也是一種輕量級的線程

而 swoole 中的協(xié)程調(diào)度使用 單進程模型, 所有協(xié)程都是在當(dāng)前進程中進行調(diào)度, 單進程的好處也很明顯 -- 簡單 / 不用加鎖 / 性能也高.
無論是 go 的 MPG模型, 還是 swoole 的 單進程模型, 都是對 CSP理論 的實現(xiàn).
CSP通信方式, 在1985年時的論文就已經(jīng)有了, 做理論研究的人, 如果沒有能提前幾年, 十幾年甚至幾十年的大膽假設(shè), 可能很難提高了.
點關(guān)注,不迷路
好了各位,以上就是這篇文章的全部內(nèi)容了,能看到這里的人呀,都是人才。之前說過,PHP方面的技術(shù)點很多,也是因為太多了,實在是寫不過來,寫過來了大家也不會看的太多,所以我這里把它整理成了PDF和文檔,如果有需要的可以


更多學(xué)習(xí)內(nèi)容可以訪問【對標(biāo)大廠】精品PHP架構(gòu)師教程目錄大全,只要你能看完保證薪資上升一個臺階(持續(xù)更新)
以上內(nèi)容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業(yè)務(wù)代碼寫多了沒有方向感,不知道該從那里入手去提升,對此我整理了一些資料,包括但不限于:分布式架構(gòu)、高可擴展、高性能、高并發(fā)、服務(wù)器性能調(diào)優(yōu)、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優(yōu)化、shell腳本、Docker、微服務(wù)、Nginx等多個知識點高級進階干貨需要的可以免費分享給大家,需要的可以加入我的 PHP技術(shù)交流群