淺談SWOOLE協程篇

閱讀本文需要以下知識點

  • 瞭解進程、線程相關基礎
  • 熟練php的hello world輸出
  • 會swoole單詞拼寫

協程的介紹

協程是什麼?

A coroutine is a function that can suspend its execution (yield) until the given given YieldInstruction finishes.

簡單的說協程是寄宿在線程下程序員實現的一種跟更輕量的併發的協作輕量線程

隨着程序員人羣的增大,大佬也不斷的爆發式增長,當然就開始有人覺得線程不好用了,那怎麼辦呢?當然是基於線程的理念上再去實現一套更加輕量、更好騙star的一套輕量線程(事實上協程不能完全被認爲線程,因爲一個線程可以有多個協程)

協程和線程的區別

本質

線程 內核態
協程 用戶態

調度方式

線程的調度方式爲系統調度,常用的調度策略有分時調度搶佔調度。說白就是線程的調度完全不受自己控制

協程的調度方式爲協作式調度 不受內核控制由自由策略調度切換

等等

協作式調度?

上述說了協程是用戶態的,所以所謂的協作式調度直接可以理解爲是程序員寫的調度方式,也就是我想怎麼調度就怎麼調度,而不用通過系統內核被調度。

~~深。。。。~~淺入理解swoole的協程

既然打算淺入理解的swoole的協程,我們必須要知道swoole的協程模型。
swoole的協程是基於單線程。可以理解爲協程的切換是串行的,再同一個時間點只運行一個協程.

說到這裏,肯定就有人問了。go呢,go的協程的是基於多線程。當然各有各的好處,具體可以自行使用搜索引擎瞭解

我們可以直接copy & paste 下面代碼,再本地的環境進行的 demo run

<?php

$func = function ($index, $isCorotunine = true) {
    $isCorotunine && \Swoole\Coroutine::sleep(2);
    echo "index:" . $index . ", value:" . (++$count) . PHP_EOL;
    echo "is corotunine:" . intval($isCorotunine) . PHP_EOL;
};

$func(1, false);
go($func, 2, true);
go($func, 3, true);
go($func, 4, true);
go($func, 5, true);
go($func, 6, true);
$func(7, false);    

會得到以下結果

index:1, value:1
is corotunine:0
index:7, value:2
is corotunine:0
index:2, value:3
is corotunine:1
index:6, value:4
is corotunine:1
index:5, value:5
is corotunine:1
index:4, value:6
is corotunine:1
index:3, value:7
is corotunine:1

肯定有人會想,哇塞,盡然1秒都執行完了,一點都不堵塞啊!!

好了,事實上關於1秒執行完的事情可以回過頭再去看下協程的概念。
我們可以關注的是執行順序,1和7是非協程的執行能立馬返回結果符合預期。
關於協程的調度順序
爲什麼是26543不是65432或者23456有序的返回呢

爲了找到我們的答案,我們只能通過源碼進行知曉一些東西

分析源碼

image

圖來自https://segmentfault.com/a/1190000019089997?utm_source=tag-newest

如果沒有較強的基礎還有啃爛的apcu的前提下(當然我也沒有!T_T)
我們需要關心的是以下兩個
yield 一個是協程的讓出CPU
resume 恢復協程

我們可以自己思考下,如果讓你設計協程,需要考慮哪些方面?

協程結構圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-iLgbTXO9-1593482951922)(/img/bVbIP6V)]

協程的創建

<?php
go (function(){
echo "swoole 太棒了";
});

調用的swoole封裝給PHPgo函數爲創建一個協程

我們根據拓展源碼中的

大部分的PHP擴展函數以及擴展方法的參數聲明放在swoole_*.ccswoole.cc裏面。

PHP_FALIAS(go, swoole_coroutine_create, arginfo_swoole_coroutine_create)

可以知道 go->swoole_coroutine_create

在swoole_coroutine.cc文件裏找到

PHP_FUNCTION(swoole_coroutine_create)
{
    ....
    // 劃重點 要考
    long cid = PHPCoroutine::create(&fci_cache, fci.param_count, fci.params);
    ....
}

long PHPCoroutine::create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv)
{
    if (sw_unlikely(Coroutine::count() >= config.max_num))
    {
        php_swoole_fatal_error(E_WARNING, "exceed max number of coroutine %zu", (uintmax_t) Coroutine::count());
        return SW_CORO_ERR_LIMIT;
    }

    if (sw_unlikely(!active))
    {
        // 劃重點 要考
        activate();
    }

    // 保存回調函數
    php_coro_args php_coro_args;
    //函數信息
    php_coro_args.fci_cache = fci_cache;
    //參數
    php_coro_args.argv = argv;
    php_coro_args.argc = argc;
    // 劃重點 要考
    save_task(get_task());

    // 劃重點 要考
    return Coroutine::create(main_func, (void*) &php_coro_args);
}

// 保存棧 (入棧)
void PHPCoroutine::save_task(php_coro_task *task)
{
    save_vm_stack(task);
    save_og(task);
}
// 初始化reactor的事件
inline void PHPCoroutine::activate()
{
    if (sw_unlikely(active))
    {
        return;
    }

    /* init reactor and register event wait */
    php_swoole_check_reactor();

    /* replace interrupt function */
    orig_interrupt_function = zend_interrupt_function;
    zend_interrupt_function = coro_interrupt_function;
    
    /* replace the error function to save execute_data */
    orig_error_function = zend_error_cb;
    zend_error_cb = error;

    if (config.hook_flags)
    {
        enable_hook(config.hook_flags);
    }

    if (SWOOLE_G(enable_preemptive_scheduler) || config.enable_preemptive_scheduler)
    {
        /* create a thread to interrupt the coroutine that takes up too much time */
        interrupt_thread_start();
    }

    if (!coro_global_active)
    {
        if (zend_hash_str_find_ptr(&module_registry, ZEND_STRL("xdebug")))
        {
            php_swoole_fatal_error(E_WARNING, "Using Xdebug in coroutines is extremely dangerous, please notice that it may lead to coredump!");
        }

        /* replace functions that can not work correctly in coroutine */
        inject_function();

        coro_global_active = true;
    }
    /**
     * deactivate when reactor free.
     */
    swReactor_add_destroy_callback(SwooleG.main_reactor, deactivate, nullptr);
    active = true;
}

根據Coroutine::create繼續往下跳轉

    static inline long create(coroutine_func_t fn, void* args = nullptr)
    {
        return (new Coroutine(fn, args))->run();
    }

在創建完協程後立馬執行
我們觀察下構造方法

    Coroutine(coroutine_func_t fn, void *private_data) :
            ctx(stack_size, fn, private_data)
    {
        cid = ++last_cid;
        coroutines[cid] = this;
        if (sw_unlikely(count() > peak_num))
        {
            peak_num = count();
        }
    }

上述代碼我可以發現還有一個Context的類 這個構造函數我們可以猜到做了3件事情

  1. 分配對應協程id (每個協程都有自己的id)
  2. 保存上下文
  3. 更新當前的協程的數量

swoole使用的協程庫爲 boost.context 可自行搜索
主要暴露的函數接口爲jump_fcontextmake_fcontext
具體的作用保存當前執行狀態的上下文暫停當前的執行狀態夠跳轉到其他位置繼續執行

執協程

inline long run()
    {
        long cid = this->cid;
        origin = current;
        current = this;
        // 依賴boost.context 切棧
        ctx.swap_in();
        // 判斷是否執行結束
        check_end();
        return cid;
    }

判斷是否結束

inline void check_end()
    {
        if (ctx.is_end())
        {
            close();
        }
        else if (sw_unlikely(on_bailout))
        {
            SW_ASSERT(current == nullptr);
            on_bailout();
            // expect that never here
            exit(1);
        }
    }

根據ctx.is_end()的函數找到

    inline bool is_end()
    {
        return end_;
    }
bool Context::swap_in()
{
    jump_fcontext(&swap_ctx_, ctx_, (intptr_t) this, true);
    return true;
}

我們可以總結下swoole在創建協程的時候主要做了哪些事情

  1. 檢測環境
  2. 解析參數
  3. 保存上下文
  4. 切換C棧
  5. 執行協程

協程的yield

上述的demo我們使用\Swoole\Coroutine::sleep(2)
根據上述說函數申明的我們在swoole_corotunine_system.h發現對應的文件爲swoole_coroutine_systemsleep的函數

PHP_METHOD(swoole_coroutine_system, sleep)
{
    double seconds;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_DOUBLE(seconds)
    ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);

    if (UNEXPECTED(seconds < SW_TIMER_MIN_SEC))
    {
        php_swoole_fatal_error(E_WARNING, "Timer must be greater than or equal to " ZEND_TOSTR(SW_TIMER_MIN_SEC));
        RETURN_FALSE;
    }
    System::sleep(seconds);
    RETURN_TRUE;
}

調用了sleep函數之後對當前的協程做了三件事
1.增加了timer定時器
2.註冊回掉函數再延遲之後resume協程
3.通過yield讓出調度

int System::sleep(double sec)
{
// 獲取當前的協程
    Coroutine* co = Coroutine::get_current_safe();
   //swTimer_add 註冊定時器 sleep_timeout回調的函數
   if (swTimer_add(&SwooleG.timer, (long) (sec * 1000), 0, co, sleep_timeout) == NULL)
    {
        return -1;
    }
    // 讓出當前cpu
    co->yield();
    return 0;
}

// 回調函數
static void sleep_timeout(swTimer *timer, swTimer_node *tnode)
{
   // 恢復調度
    ((Coroutine *) tnode->data)->resume();
}

swTimer_node* swTimer_add(swTimer *timer, long _msec, int interval, void *data, swTimerCallback callback)
{
    ....
    // 保存當前上下文和對應過期時間
    tnode->data = data;
    tnode->type = SW_TIMER_TYPE_KERNEL;
    tnode->exec_msec = now_msec + _msec;
    tnode->interval = interval ? _msec : 0;
    tnode->removed = 0;
    tnode->callback = callback;
    tnode->round = timer->round;
    tnode->dtor = NULL;

    // _next_msec保存最快過期的事件
    if (timer->_next_msec < 0 || timer->_next_msec > _msec)
    {
        timer->set(timer, _msec);
        timer->_next_msec = _msec;
    }

    tnode->id = timer->_next_id++;
    if (sw_unlikely(tnode->id < 0))
    {
        tnode->id = 1;
        timer->_next_id = 2;
    }

    tnode->heap_node = swHeap_push(timer->heap, tnode->exec_msec, tnode);
    ....
    timer->num++;
    return tnode;
}

協程的切換

我們

void Coroutine::resume()
{
    SW_ASSERT(current != this);
    if (sw_unlikely(on_bailout))
    {
        return;
    }
    state = SW_CORO_RUNNING;
    if (sw_likely(on_resume))
    {
        on_resume(task);
    }
    // 將當前的協程保存爲origin -> 理解程previous
    origin = current;
    // 需要執行的協程 變成 current
    current = this;
    // 入棧執行
    ctx.swap_in();
    check_end();
}

到這裏時候 關於協程調用順序的答案已經出來了

在創建協程的時候(new Coroutine(fn, args))->run();sleep觸發yield都在不斷變更的Corotuninecurrentorigin 再執切換的時候和php代碼創建協程的時間發生穿插,而不是我們想象中的隊列有序執行
比如當創建協程只有2個的時候

<?php

$func = function ($index, $isCorotunine = true) {
    $isCorotunine && \Swoole\Coroutine::sleep(2);
    echo "index:" . $index . PHP_EOL;
    echo "is corotunine:" . intval($isCorotunine) . PHP_EOL;
};

$func(1, false);

go($func, 2, true);
go($func, 3, true);

返回輸出 因爲連續創建協程的執行時間小沒有被打亂

php swoole_go_demo1.php
index:1
is corotunine:0
index:2
is corotunine:1
index:3
is corotunine:1

當連續創建的時候200個協程的時候
返回就變得打亂的index 符合預計猜想

index:1,index:202,index:1,index:2,index:4,index:8,index:16,index:32,index:64,index:128,index:129,index:65,index:130,index:131,index:33,index:66,index:132,index:133,index:67,index:134,index:135,index:17,index:34,index:68,index:136,index:137,index:69,index:138,index:139,index:35,index:70,index:140,index:141,index:71,index:142,index:143,index:9,index:18,index:36,index:72,index:144,index:145,index:73,index:146,index:147,index:158,index:157,index:156,index:155,index:154,index:153,index:152,index:151,index:37,index:74,index:148,index:149,index:75,index:150,index:19,index:38,index:76,index:77,index:39,index:78,index:79,index:5,index:10,index:20,index:40,index:80,index:81,index:41,index:82,index:83,index:21,index:127,index:126,index:125,index:124,index:123,index:122,index:121,index:120,index:119,index:118,index:117,index:116,index:115,index:114,index:113,index:112,index:111,index:110,index:109,index:108,index:107,index:106,index:105,index:104,index:103,index:102,index:101,index:100,index:99,index:98,index:97,index:96,index:95,index:94,index:93,index:92,index:91,index:90,index:89,index:88,index:87,index:42,index:84,index:85,index:43,index:86,index:11,index:22,index:44,index:45,index:23,index:46,index:47,index:3,index:6,index:12,index:24,index:48,index:49,index:25,index:50,index:51,index:13,index:26,index:63,index:62,index:61,index:60,index:59,index:58,index:57,index:56,index:55,index:52,index:53,index:27,index:54,index:7,index:14,index:28,index:29,index:15,index:30,index:31,index:200,index:199,index:192,index:185,index:175,index:168,index:161,index:163,index:172,index:179,index:187,index:194,index:174,index:160,index:173,index:176,index:198,index:195,index:180,index:167,index:169,index:184,index:197,index:193,index:177,index:162,index:171,index:186,index:182,index:164,index:191,index:183,index:166,index:196,index:178,index:170,index:189,index:188,index:165,index:181,index:190,index:159

最後彩蛋

我們使用GO的協程的來實現上述的demo

package main

import (
	"fmt"
	"time"
)

var count int = 0

func main() {
	output(false, 1)

	go output(true, 2)
	go output(true, 3)
	go output(true, 4)
	go output(true, 5)
	go output(true, 6)

	output(false, 7)

	time.Sleep(time.Second)
}

func output(isCorotunine bool, index int) {
	time.Sleep(time.Second)
	count = count + 1
	fmt.Println(count, isCorotunine, index)
}

猜猜返回結果是如何的 可以根據go的協程基於多線程的方式再去研究下
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-p0GMooWU-1593482951925)(/img/bVbISI7)]

寫給最後,文章純屬自己根據代碼和資料理解,如果有錯誤麻煩提出來,倍感萬分,如果因爲一些錯誤的觀點被誤導我只能說

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章