PHP面試遇到面試官的swoole協程三連問,快哭了!

weixin_49163826發表於2020-09-29

什麼是程式?

程式就是應用程式的啟動例項。獨立的檔案資源,資料資源,記憶體空間。

什麼是執行緒?

執行緒屬於程式,是程式的執行者。一個程式至少包含一個主執行緒,也可以有更多的子執行緒。執行緒有兩種排程策略,一是:分時排程,二是:搶佔式排程。

我的官方企鵝群

什麼是協程?

協程是輕量級執行緒,協程也是屬於執行緒,協程是線上程裡執行的。協程的排程是使用者手動切換的,所以又叫使用者空間執行緒。協程的建立、切換、掛起、銷燬全部為記憶體操作,消耗是非常低的。協程的排程策略是:協作式排程。

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 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年時的論文就已經有了, 做理論研究的人, 如果沒有能提前幾年, 十幾年甚至幾十年的大膽假設, 可能很難提高了.

點關注,不迷路

好了各位,以上就是這篇文章的全部內容了,能看到這裡的人呀,都是人才。之前說過,PHP方面的技術點很多,也是因為太多了,實在是寫不過來,寫過來了大家也不會看的太多,所以我這裡把它整理成了PDF和文件,如果有需要的可以

點選進入暗號: PHP+「平臺」

在這裡插入圖片描述

在這裡插入圖片描述


更多學習內容可以訪問【對標大廠】精品PHP架構師教程目錄大全,只要你能看完保證薪資上升一個臺階(持續更新)

以上內容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務程式碼寫多了沒有方向感,不知道該從那裡入手去提升,對此我整理了一些資料,包括但不限於:分散式架構、高可擴充套件、高效能、高併發、伺服器效能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell指令碼、Docker、微服務、Nginx等多個知識點高階進階乾貨需要的可以免費分享給大家,需要的可以加入我的 PHP技術交流群

相關文章