swoole| swoole 協程初體驗

date: 2018-5-30 14:31:38
title: swoole| swoole 協程初體驗
description: 通過協程的執行初窺 swoole 中協程的調度; 理解協程為什么快; swoole 協程和 go 協程對比

折騰 swoole 協程有一段時間了, 總結一篇入門貼, 希望對新手有幫助.

內容概覽:

  • 協程的執行順序: 初窺 swoole 中協程的調度
  • 協程為什么快: 減少IO阻塞帶來的性能損耗
  • swoole 協程和 go 協程對比: 單進程 vs 多線程

協程的執行順序

先來看看基礎的例子:

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 gohello 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, 其實本質上也是一種輕量級的線程
MPG 模型

而 swoole 中的協程調度使用 單進程模型, 所有協程都是在當前進程中進行調度, 單進程的好處也很明顯 -- 簡單 / 不用加鎖 / 性能也高.

無論是 go 的 MPG模型, 還是 swoole 的 單進程模型, 都是對 CSP理論 的實現.

CSP通信方式, 在1985年時的論文就已經有了, 做理論研究的人, 如果沒有能提前幾年, 十幾年甚至幾十年的大膽假設, 可能很難提高了.

寫在最后

今天從 go() 出發, 得以一瞥協程世界, 協程的世界里還有很多很有意思的東西, 需要我們去發現. 比如:

  • 我們普通版的代碼是當前進程里執行的, 只是單個進程, 可我們現在可能有了很多協程, 會不會有什么奇遇呢?

還有一個細節: swoole 中有 Co::sleep()sleep() 2個方法的, 而 go 中只有 time.Sleep() 一個方法?

這是 swoole 協程需要經歷的一個階段(畢竟 go 快 10 年了), 還不夠 智能的判斷 IO阻塞, 所以上面也使用了相應的協程版 redis co\Redis() -- 你得使用配套協程版, 才能達到協程調度的效果.

如果對協程的發展階段感興趣, 可以閱讀下面這篇文章:

想解鎖 swoole 協程的更多姿勢:

最后, 本期示例代碼可以從我的開源項目中獲取, 請享用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容