天天看點

c++ 協程_Swoole 實作協程基本概念和底層原理

協程是什麼

協程可以了解為純使用者态的線程,其通過協作而不是搶占來進行切換,相對于程序或者線程,協程所有的操作都可以在使用者态完成,建立和切換的消耗更低,Swoole 可以為每一個請求建立對應的協程,根據 IO 的狀态來合理的排程協程。

在 Swoole 4.x 中,協程(Coroutine)取代了異步回調,成為 Swoole推薦的程式設計方式。Swoole 協程解決了異步回調程式設計困難的問題,使用協程可以以傳統同步程式設計的方法編寫代碼,底層自動切換為異步 IO,既保證了程式設計的簡單性,又可借助異步 IO,提升系統的并發能力。

注:Swoole 4.x 之前的版本也支援協程,不過 4.x 版本對協程核心進行了重構,功能更加強大,提供了完整的協程+通道特性,帶來全新的 CSP 程式設計模型。

基本使用示例

  • PHP 版本要求:>= 7.0;
  • 基于

    Server

    HttpServer

    WebSocketServer

    進行開發的時候,Swoole 底層會在

    onRequest

    onReceive

    onConnect

    等事件回調之前自動建立一個協程,在回調函數中即可使用協程 API;
  • 你也可以使用

    Coroutine::create

    go

    方法建立協程,在建立的協程中使用協程 API 進行程式設計。

以 Swoole 自帶的 TCP 伺服器

SwooleServer

實作為例,我們可以定義伺服器端實作如下:

$server = new SwooleServer("127.0.0.1", 9501);
​
// 調用 onReceive 事件回調函數時底層會自動建立一個協程
$server->on('receive', function ($serv, $fd, $from_id, $data) {
    // 向用戶端發送資料後關閉連接配接(在這裡面可以調用 Swoole 協程 API)
    $serv->send($fd, 'Swoole: ' . $data);
    $serv->close($fd);
});
$server->start();
           

然後我們以協程方式實作 TCP 用戶端如下:

// 通過 go 函數建立一個協程

go(function () {

    $client = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);

    // 嘗試與指定 TCP 服務端建立連接配接,這裡會觸發 IO 事件切換協程,交出控制權讓 CPU 去處理其他事情
    if ($client->connect("127.0.0.1", 9501, 0.5)) {

        // 建立連接配接後發送内容

        $client->send("hello worldn");

        // 列印接收到的消息(調用 recv 函數會恢複協程繼續處理後續代碼,比如列印消息、關閉連接配接)

        echo $client->recv();

        // 關閉連接配接

        $client->close();

    } else {

        echo "connect failed.";
    }
});
           

底層實作原理

我們以 MySQL 連接配接查詢為例,對 Swoole 協程底層實作做一個簡單的介紹:

$server = new SwooleHttpServer('127.0.0.1', 9501, SWOOLE_BASE);

#1
$server->on('Request', function($request, $response) {
    $mysql = new SwooleCoroutineMySQL();
    #2
    $res = $mysql->connect([
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    ]);
    #3
    if ($res == false) {
        $response->end("MySQL connect fail!");
        return;
    }
    $ret = $mysql->query('show tables', 2);
    $response->end("swoole response is ok, result=".var_export($ret, true));
});

$server->start();




           

在這段代碼中,我們啟動一個基于 Swoole 實作的 HTTP 伺服器監聽用戶端請求,如果有

onRequest

事件發生,則通過基于 Swoole 協程實作的異步 MySQL 用戶端元件對 MySQL 伺服器發起連接配接請求,并執行查詢操作,然後将結果以響應方式傳回給 HTTP 用戶端,下面我們來看一下協程在這段代碼中的應用:

  • 調用

    SwooleHttpServer

    onRequest

    事件回調函數時,底層會調用 C 函數

    coro_create

    建立一個協程(

    #1

    位置),同時儲存這個時間點的 CPU 寄存器狀态和 ZendVM 堆棧資訊;
  • 調用

    mysql->connect

    時會發生 IO 操作,底層會調用 C 函數

    coro_save

    儲存目前協程的狀态,包括 ZendVM 上下文以及協程描述資訊,并調用

    coro_yield

    讓出程式控制權,目前的請求會挂起(

    #2

    位置);
  • 協程讓出程式控制權後,會繼續進入 HTTP 伺服器的事件循環處理其他事件,這時 Swoole 可以繼續去處理其他用戶端發來的請求;
  • 當資料庫 IO 事件完成後,MySQL 連接配接成功或失敗,底層調用 C 函數

    coro_resume

    恢複對應的協程,恢複 ZendVM 上下文,繼續向下執行 PHP 代碼(

    #3

    位置);
  • mysql->query

    的執行過程與

    mysql->connect

    一樣,也會觸發 IO 事件并進行一次協程切換排程;
  • 所有操作完成後,調用

    end

    方法傳回結果,并銷毀此協程。
注:更深層次的協程底層實作可以參考 Swoole 官方文檔的介紹。

上面這段代碼我們借助了 Swoole 實作的協程 MySQL 用戶端(Swoole 還提供了很多其他協程用戶端,如 Redis、HTTP等,後面我們會詳細介紹),所有的編碼和之前編寫同步代碼時并沒有任何不同,但是 Swoole 底層會在 IO 事件發生時,儲存目前狀态,将程式控制權交出,以便 CPU 處理其它事件,當 IO 事件完成時恢複并繼續執行後續邏輯,進而實作異步 IO 的功能,這正是協程的強大之處,它可以讓伺服器同時可以處理更多請求,而不會阻塞在這裡等待 IO 事件處理完成,進而極大提高系統的并發性。

協程的适用場景

通過上面這個簡單的示例,我們得出協程非常适合并發程式設計,常見的并發程式設計場景如下:

  • 高并發服務,如秒殺系統、高性能 API 接口、RPC 伺服器,使用協程模式,服務的容錯率會大大增加,某些接口出現故障時,不會導緻整個服務崩潰;
  • 爬蟲,可實作非常強大的并發能力,即使是非常慢速的網絡環境,也可以高效地利用帶寬;
  • 即時通信服務,如 IM 聊天、遊戲伺服器、物聯網、消息伺服器等等,可以確定消息通信完全無阻塞,每個消息包均可即時地被處理。

協程引入的問題

協程再為我們帶來便利的同時,也引入了一些新的問題:

  • 協程需要為每個并發儲存棧記憶體并維護對應的虛拟機狀态,如果程式并發很大可能會占用大量記憶體;
  • 協程排程會增加額外的一些 CPU 開銷。

盡管如此,在處理高并發應用時,使用協程帶來的優勢還是遠遠高于 PHP 預設的同步阻塞機制。

協程 vs 線程

Swoole 的協程在底層實作上是單線程的,是以同一時間隻有一個協程在工作,協程的執行是串行的,這與線程不同,多個線程會被作業系統排程到多個 CPU 并行執行。

一個協程正在運作時,其他協程會停止工作。目前協程執行阻塞 IO 操作時會挂起,底層排程器會進入事件循環。當有 IO 完成事件時,底層排程器恢複事件對應的協程的執行。

在 Swoole 中對 CPU 多核的利用,仍然依賴于 Swoole 引擎的多程序機制。

協程 vs 生成器

一些架構中會使用 PHP 的生成器來實作半自動化的協程,但在實際使用中,開發者需要在涉及協程邏輯的函數調用前增加

yield

關鍵字,這帶來了額外的學習和維護成本,非常容易犯錯,此外 Yield/Generator 代碼風格與傳統的同步風格代碼存在沖突,無法複用已有代碼。

Swoole 協程是全自動化的協程,開發者無需添加任何關鍵字,底層自動實作協程的切換和排程,此外,Swoole 協程風格與傳統的同步風格代碼是一緻的,是以可以複用已有代碼。

使用時的注意事項

程式設計範式
  • 協程之間通訊不要使用全局變量或者引用外部變量到目前作用域,而要使用

    Channel

    (後面會介紹具體使用)
  • 項目中如果有擴充 hook 了

    zend_execute_ex

    或者

    zend_execute_internal

    這兩個函數,需要特别注意一下 C 棧,可以使用

    co::set

    重新設定 C 棧大小
擴充沖突

由于某些跟蹤調試的 PHP 擴充大量使用了全局變量,可能會導緻 Swoole 協程發生崩潰,請關閉這些相關擴充:

  • xdebug
  • phptrace
  • aop
  • molten
  • xhprof
  • phalcon(Swoole協程無法運作在 phalcon 架構中)
嚴重錯誤

由于多個協程是并發執行的,是以以下行為可能會導緻協程出現嚴重錯誤:

  • 不能使用類靜态變量/全局變量儲存協程上下文内容,否則可能導緻變量被污染,要使用 Context 管理上下文
  • 同一時間可能會有很多個請求在并行處理,多個協程共用一個用戶端連接配接的話,就會導緻不同協程之間發生資料錯亂

錯誤和異常處理

在協程程式設計中可直接使用

try/catch

處理異常,但必須在協程内捕獲,不得跨協程捕獲異常。

此外,如果在協程内使用

exit

終止程式執行退出目前協程的話,會抛出

SwooleExitException

異常,你可以在需要的位置捕獲該異常并實作與原生 PHP 一樣的退出邏輯:

go(function () {
    try {
        SwooleCoroutine::sleep(1);  // 模拟 IO 事件讓出控制權
        exit(SWOOLE_EXIT_IN_COROUTINE);
    } catch (SwooleExitException $exception) {
        assert($exception->getStatus() === 1);
        assert($exception->getFlags() === SWOOLE_EXIT_IN_COROUTINE);
        return;
    }
});
           
注:不能将 go 函數放到 try 語句塊中,這樣就是跨協程捕獲異常了。 以上内容希望幫助到大家, 很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裡入手去提升,對此我整理了一些資料,包括但不限于:分布式架構、高可擴充、高性能、高并發、伺服器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點進階進階幹貨需要的可以免費分享給大家 ,需要請戳這裡連結 或者 知乎專欄

PHP7進階架構師​zhuanlan.zhihu.com

c++ 協程_Swoole 實作協程基本概念和底層原理