PHP面試遇到面試官的swoole協程三連問,快哭了!
什麼是程式?
程式就是應用程式的啟動例項。獨立的檔案資源,資料資源,記憶體空間。
什麼是執行緒?
執行緒屬於程式,是程式的執行者。一個程式至少包含一個主執行緒,也可以有更多的子執行緒。執行緒有兩種排程策略,一是:分時排程,二是:搶佔式排程。
什麼是協程?
協程是輕量級執行緒,協程也是屬於執行緒,協程是線上程裡執行的。協程的排程是使用者手動切換的,所以又叫使用者空間執行緒。協程的建立、切換、掛起、銷燬全部為記憶體操作,消耗是非常低的。協程的排程策略是:協作式排程。
Swoole 協程的原理
-
Swoole4 由於是單執行緒多程式的,同一時間同一個程式只會有一個協程在執行。
-
Swoole server 接收資料在 worker 程式觸發 onReceive 回撥,產生一個攜程。Swoole 為每個請求建立對應攜程。協程中也能建立子協程。
-
協程在底層實現上是單執行緒的,因此同一時間只有一個協程在工作,協程的執行是序列的。
-
因此多工多協程執行時,一個協程正在執行時,其他協程會停止工作。當前協程執行阻塞 IO 操作時會掛起,底層排程器會進入事件迴圈。當有 IO 完成事件時,底層排程器恢復事件對應的協程的執行。。所以協程不存在 IO 耗時,非常適合高併發 IO 場景。(如下圖)
Swoole 的協程執行流程
-
協程沒有 IO 等待 正常執行 PHP 程式碼,不會產生執行流程切換
-
協程遇到 IO 等待 立即將控制權切,待 IO 完成後,重新將執行流切回原來協程切出的點
-
協程並行協程依次執行,同上一個邏輯
-
協程巢狀執行流程由外向內逐層進入,直到發生 IO,然後切到外層協程,父協程不會等待子協程結束
協程的執行順序
先來看看基礎的例子:
go(function () {
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
echo "hello go2 \n";
});
go()
是 \Co::create()
的縮寫, 用來建立一個協程, 接受 callback 作為引數, callback 中的程式碼, 會在這個新建的協程中執行.
備註: \Swoole\Coroutine
可以簡寫為 \Co
上面的程式碼執行結果:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello go1
hello main
hello go2
執行結果和我們平時寫程式碼的順序, 好像沒啥區別. 實際執行過程:
-
執行此段程式碼, 系統啟動一個新程式
-
遇到
go()
, 當前程式中生成一個協程, 協程中輸出heelo go1
, 協程退出 -
程式繼續向下執行程式碼, 輸出
hello main
-
再生成一個協程, 協程中輸出
heelo go2
, 協程退出
執行此段程式碼, 系統啟動一個新程式. 如果不理解這句話, 你可以使用如下程式碼:
// co.php
<?php
sleep(100);
執行並使用 ps aux
檢視系統中的程式:
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
⏎
我們來稍微改一改, 體驗協程的排程:
use Co;
go(function () {
Co::sleep(1); // 只新增了一行程式碼
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
echo "hello go2 \n";
});
\Co::sleep()
函式功能和 sleep()
差不多, 但是它模擬的是 IO等待(IO後面會細講). 執行的結果如下:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello main
hello go2
hello go1
怎麼不是順序執行的呢? 實際執行過程:
- 執行此段程式碼, 系統啟動一個新程式
- 遇到
go()
, 當前程式中生成一個協程 - 協程中遇到 IO阻塞 (這裡是
Co::sleep()
模擬出的 IO等待), 協程讓出控制, 進入協程排程佇列 - 程式繼續向下執行, 輸出
hello main
- 執行下一個協程, 輸出
hello go2
- 之前的協程準備就緒, 繼續執行, 輸出
hello go1
到這裡, 已經可以看到 swoole 中 協程與程式的關係, 以及 協程的排程, 我們再改一改剛才的程式:
go(function () {
Co::sleep(1);
echo "hello go1 \n";
});
echo "hello main \n";
go(function () {
Co::sleep(1);
echo "hello go2 \n";
});
我想你已經知道輸出是什麼樣子了:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello main
hello go1
hello go2
⏎
協程快在哪? 減少IO阻塞導致的效能損失
大家可能聽到使用協程的最多的理由, 可能就是 協程快. 那看起來和平時寫得差不多的程式碼, 為什麼就要快一些呢? 一個常見的理由是, 可以建立很多個協程來執行任務, 所以快. 這種說法是對的, 不過還停留在表面.
首先, 一般的計算機任務分為 2 種:
- CPU密集型, 比如加減乘除等科學計算
- IO 密集型, 比如網路請求, 檔案讀寫等
其次, 高效能相關的 2 個概念:
- 並行: 同一個時刻, 同一個 CPU 只能執行同一個任務, 要同時執行多個任務, 就需要有多個 CPU 才行
- 併發: 由於 CPU 切換任務非常快, 快到人類可以感知的極限, 就會有很多工 同時執行 的錯覺
瞭解了這些, 我們再來看協程, 協程適合的是 IO 密集型 應用, 因為協程在 IO阻塞 時會自動排程, 減少IO阻塞導致的時間損失.
我們可以對比下面三段程式碼:
- 普通版: 執行 4 個任務
$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
⏎
- 單個協程版:
$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
⏎
- 多協程版: 見證奇蹟的時刻
$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阻塞 導致的效能損失
-
單協程: 儘管 IO阻塞 引發了協程排程, 但當前只有一個協程, 排程之後還是執行當前協程
-
多協程: 真正發揮出了協程的優勢, 遇到 IO阻塞 時發生排程, IO就緒時恢復執行
我們將多協程版稍微修改一下:
- 多協程版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密集型任務, 不會引起協程的排程 -
Co::sleep()
模擬的是 IO密集型任務, 會引發協程的排程
這也是為什麼, 協程適合 IO密集型 的應用.
再來一組對比的例子: 使用 redis
// 同步版, redis使用時會有 IO 阻塞
$cnt = 2000;
for ($i = 0; $i < $cnt; $i++) {
$redis = new \Redis();
$redis->connect('redis');
$redis->auth('123');
$key = $redis->get('key');
}
// 單協程版: 只有一個協程, 並沒有使用到協程排程減少 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');
}
});
// 多協程版, 真正使用到協程排程帶來的 IO 阻塞時的排程
for ($i = 0; $i < $cnt; $i++) {
go(function () {
$redis = new Co\Redis();
$redis->connect('redis', 6379);
$redis->auth('123');
$redis->get('key');
});
}
效能對比:
# 多協程版
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 協程和 go 協程對比: 單程式 vs 多執行緒
接觸過 go 協程的 coder, 初始接觸 swoole 的協程會有點 懵, 比如對比下面的程式碼:
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 協程的 coder, 在寫這個程式碼的時候會被告知不要忘了 time.Sleep(time.Second)
, 否則看不到輸出 hello go
, 其次, hello go
與 hello main
的順序也和 swoole 中的協程不一樣.
原因就在於 swoole 和 go 中, 實現協程排程的模型不同.
上面 go 程式碼的執行過程:
- 執行 go 程式碼, 系統啟動一個新程式
- 查詢
package main
, 然後執行其中的func mian()
- 遇到協程, 交給協程排程器執行
- 繼續向下執行, 輸出
hello main
- 如果不新增
time.Sleep(time.Second)
, main 函式執行完, 程式結束, 程式退出, 導致排程中的協程也終止
go 中的協程, 使用的 MPG 模型:
- M 指的是 Machine, 一個M直接關聯了一個核心執行緒
- P 指的是 processor, 代表了M所需的上下文環境, 也是處理使用者級程式碼邏輯的處理器
- G 指的是 Goroutine, 其實本質上也是一種輕量級的執行緒
而 swoole 中的協程排程使用 單程式模型, 所有協程都是在當前程式中進行排程, 單程式的好處也很明顯 – 簡單 / 不用加鎖 / 效能也高.
無論是 go 的 MPG模型, 還是 swoole 的 單程式模型, 都是對 CSP理論 的實現.
CSP通訊方式, 在1985年時的論文就已經有了, 做理論研究的人, 如果沒有能提前幾年, 十幾年甚至幾十年的大膽假設, 可能很難提高了.
點關注,不迷路
好了各位,以上就是這篇文章的全部內容了,能看到這裡的人呀,都是人才。之前說過,PHP方面的技術點很多,也是因為太多了,實在是寫不過來,寫過來了大家也不會看的太多,所以我這裡把它整理成了PDF和文件,如果有需要的可以
更多學習內容可以訪問【對標大廠】精品PHP架構師教程目錄大全,只要你能看完保證薪資上升一個臺階(持續更新)
以上內容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務程式碼寫多了沒有方向感,不知道該從那裡入手去提升,對此我整理了一些資料,包括但不限於:分散式架構、高可擴充套件、高效能、高併發、伺服器效能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell指令碼、Docker、微服務、Nginx等多個知識點高階進階乾貨需要的可以免費分享給大家,需要的可以加入我的 PHP技術交流群
相關文章
- 詢問面試官的面試問題面試
- 面試官,您要的快排面試
- 面試官問:JS的this指向面試JS
- 面試 HTTP ,99% 的面試官都愛問這些問題面試HTTP
- Nginx 面試 40 連問,快頂不住了~~Nginx面試
- 女生在求職面試過程中,如何巧妙回答面試官的尷尬問題求職面試
- 問問那些變態的面試官面試
- 面試官問:JS的繼承面試JS繼承
- 手撕面試官系列:BAT面試常問85題面試BAT
- 面試官,你再問我 Bit Operation 試試?面試
- 面試小冊:面試官經常問的十個棘手的 JavaScript 問題面試JavaScript
- 面試遇到的redis相關問題面試Redis
- 操蛋的面試官面試
- 面試官:ZAB協議是什麼?面試協議
- 月薪不同的三人去面試,面試官問道:各自談談對 binder 的理解?面試
- 2019年面試官最喜歡問的28道ZooKeeper面試題面試題
- SQL面試題,快問快答!SQL面試題
- 專案中遇到的RediS快取問題及面試問題總結Redis快取面試
- 面試官問我MySQL索引,我面試MySql索引
- 當面試遇到 Redis,我作為一個面試官是這麼“刁難”你的!面試Redis
- PHP面試問題總結PHP面試
- Laravel 使用 swoole 協程遇到的坑Laravel
- 面試中遇到的一些問題面試
- 橫趟!面試中遇到的 ZooKeeper 問題面試
- js面試必考三問JS面試
- 面試官:為什麼 Promise 比setTimeout() 快?面試Promise
- 分享一個作為面試官的面試思路面試
- 面試官常問的Nginx的幾個問題面試Nginx
- 【搞定Jvm面試】 面試官:談談 JVM 類載入過程是怎樣的?JVM面試
- 面試官眼中的Promise面試Promise
- 三面阿里,面試官:講講分散式的CAP定理阿里面試分散式
- 3年PHP經驗遇到的高頻面試題PHP面試題
- 面試官:小夥子知道synchronized的最佳化過程嗎?我:嘚吧嘚吧嘚,面試官:出去!面試synchronized
- Java三種面試者是面試官最討厭的,見之即斃!Java面試
- 征服面試官:OkHttp 原理篇 掌握這篇面試題彙總,吊打面試官!HTTP面試題
- Laravel 裡面用swoole的協程go報錯 ?LaravelGo
- 面試問爛的 Spring MVC 過程面試SpringMVC
- 《金三銀四面試系列》—面試必問Spring面試Spring