Goroutine 隨筆

很早之前在lua中實現(xiàn)過一版協(xié)程恰画,lua的棧是虛擬的宾茂,當(dāng)要切換協(xié)程時虛擬棧不需要退棧,只需要從C的棧(物理棧)退出拴还】缜纾恢復(fù)也簡單,直接在lua的虛擬棧壓入返回值片林,lua就可以繼續(xù)運行了端盆。這版協(xié)程比較簡單,雖然不支持lua的棧與C棧交替存在费封,但當(dāng)時也沒這種需求焕妙,運行的也算問題,沒什么特別的Bug弓摘。

最近在看Go焚鹊,開始以為Go的routine也是虛擬棧,很快發(fā)現(xiàn)不對韧献,Go是編譯語言末患,不是解釋語言,Go的棧是物理棧锤窑。當(dāng)時就想物理棧退棧了怎么再回到現(xiàn)場的璧针,搜了一下資料才發(fā)現(xiàn)自己秀逗了。物理棧不需要退棧渊啰,切換時保存好指令寄存器和棧指針寄存器探橱,恢復(fù)的時候再把3個寄存器一恢復(fù)就行了。反而比虛擬棧還要簡單些绘证,順著原理自己簡單的實現(xiàn)了一把隧膏,雖然過程有點曲折,效果還算滿意的嚷那。


首先是P私植,只包含了一個G的隊列

#include <queue>

class G;
class P
{
public:
    void addG(G* g)
    {
        gs.push(g);
    }

    G* popG()
    {
        if (gs.size() == 0)
        {
            return nullptr;
        }

        G* g = gs.front();
        gs.pop();
        return g;
    }

private:
    std::queue<G*> gs;
};

G 用ID來模擬線程存儲數(shù)據(jù),esp_變量保存棧頂指針

//G.H
typedef void(__stdcall *routine)(int);

namespace runtime
{
    void yield();
}

class M;
class G
{
public:
    G(routine fun, int arg)
    {
        id = _id++;

        //16MB的棧车酣,開始申請了4KB結(jié)果溢出了 各種Bug 以為棧指針操作出了問題
        const int stackSize = 16 * 1024 * 1024;

        //構(gòu)建堆棧
        BYTE* mem = (BYTE*)malloc(stackSize + 1024); //多申請1KB
        DWORD* p = (DWORD*)(mem + stackSize);

        *p-- = (DWORD)mem;
        *p-- = (DWORD)this;
        *p-- = arg;
        *p-- = (DWORD)fun;
        *p-- = (DWORD)(mem + stackSize); //EBP
        *p = 0; //標(biāo)識位 0表示routine還沒有運行棧曲稼,棧中都是數(shù)據(jù)
        esp_ = (DWORD)p;

        dead_ = false;
    }

private:
    G() //g0的構(gòu)造函數(shù)
    {
        id = _id++;
        dead_ = false;

        //g0不需要棧,用系統(tǒng)的棧
    }

private:
    void Attach(M* m)
    {
        m_ = m;
        if (m != nullptr)
        {
            current_ = this;
        }
        else
        {
            current_ = nullptr;
        }
    }

public:
    static G* GetCurrent()
    {
        return current_;
    }

    int getId()
    {
        return id;
    }

private:
    int id;
    bool dead_;
    M* m_;

    DWORD esp_;
    friend class M;

private:
    static thread_local G* current_;
    static int _id;
};

//G.cpp
thread_local G* G::current_;
int G::_id;

void runtime::yield()
{
    M::GetCurrent()->yieldCurrentG();
}

最后是M湖员,G的切換贫悄,沒實現(xiàn)調(diào)度

//M.h
class M
{
public:
    M(P* p);

    ~M();

public:
    void switchG();
    void yieldCurrentG();
    void addG(G* g);

    static M* GetCurrent()
    {
        return current_;
    }

private:
    void freeG(G* g, PVOID pStackMem);

private:
    G* currentG_;
    G* g0;
    P* p_;


    static thread_local M* current_;
};

//M.cpp
thread_local M* M::current_;

M::M(P* p)
{
    p_ = p;

    g0 = new G();
    currentG_ = g0;
    g0->Attach(this);

    current_ = this;
}

M::~M()
{
    delete g0;
    current_ = nullptr;
}

void M::addG(G * g)
{
    p_->addG(g);
}

__declspec(naked) void resumeG()
{
    __asm
    {
        pop edi //return address
        pop ecx //this M的指針
        pop eax //參數(shù)
        push edi
        push ecx

        mov esp, eax
        sub esp, 8  //上上行 push了一個EDI,一個ECX
        add eax, 36
        mov ebp, eax //比pushad大點
        mov edx, ecx
        pop ecx //this
        pop edi //return address
        pop eax  //flag

        cmp eax, 0
        jne RESUME

        //新routine 初始化調(diào)用棧
        pop ebp
        pop eax
        pop esi
        push edx //保留this指針
        push esi
        call eax
        pop ecx //this指針
        call M::freeG  //freeG不會返回了

    RESUME:
        pop eax
        mov esp, eax
        add eax, 36
        mov ebp, eax //比pushad大點
        pop eax
        mov ebp, eax
        push edi
        ret
    }
}

void M::freeG(G* g, PVOID pStackMem)
{
    if (g == currentG_)
    {
        g->esp_ = (DWORD)pStackMem;
        g->dead_ = true;

        //切換到g0釋放內(nèi)存
        DWORD esp_ = g0->esp_;
        __asm
        {
            mov eax, esp_
            push eax
            mov ecx, this
            push ecx
            call resumeG
        }
    }
}

//g0堆棧
void M::switchG()
{
    DWORD esp_ = 0;

    while (true)
    {
        currentG_ = p_->popG();
        if (currentG_ == nullptr)
        {
            //都運行完了
            break;
        }

        __asm
        {
            mov eax, ebp
            push eax
            mov eax, esp
            push eax
            mov eax, 1
            push eax
            mov esp_, esp
        }
        g0->esp_ = esp_;
        g0->Attach(nullptr);
        currentG_->Attach(this);

        esp_ = currentG_->esp_;
        __asm
        {
            mov eax, esp_
            push eax
            mov eax, this
            push eax
            call resumeG
        }

        if (currentG_->dead_)
        {
            //此時在g0娘摔,但currentG不是g0
            free((void*)currentG_->esp_);
            delete currentG_;
            currentG_ = nullptr;
        }
    }
}

void M::yieldCurrentG()
{
    //保存現(xiàn)場
    DWORD esp_ = 0;
    G* g = currentG_;

    __asm
    {
        mov eax, dword ptr[ebp - 4]
        mov eax, ebp
        push eax
        mov eax, esp
        push eax
        mov eax, 1
        push eax
        mov esp_, esp
    }
    g->esp_ = esp_;
    g->Attach(nullptr);
    p_->addG(g);

    //切換到g0
    g0->Attach(this);
    esp_ = g0->esp_;

    __asm
    {
        mov eax, esp_
        push eax
        mov eax, this
        push eax
        call resumeG
    }

    //從其他routine回來了
    currentG_ = g;
    g->Attach(this);
}

所有的G都會從 resumeG 開始窄坦,resumeG 切換堆棧,進(jìn)入G的routine函數(shù)執(zhí)行,由于沒實現(xiàn)調(diào)度鸭津,所以需要代碼中調(diào)用runtime::yield來切換routine彤侍。runtime::yield會調(diào)用M::yieldCurrentGM::yieldCurrentG保存現(xiàn)場后切換到g0逆趋,g0選擇下一個routine盏阶,再通過resumeG切換堆棧和指令寄存器,yieldCurrentG得以繼續(xù)執(zhí)行闻书。routine函數(shù)執(zhí)行完成后會回到最初的resumeG名斟, resumeG再調(diào)用M::freeG來銷毀G,至此routine完結(jié)銷毀魄眉。


//測試代碼
void __stdcall routine1(int arg)
{
    G* g = G::GetCurrent();
    printf("fun=routine1, id=%d, arg=%d\n", g->getId(), arg);
    runtime::yield();
    printf("fun=routine1, id=%d, arg=%d\n", g->getId(), arg);
}

void __stdcall routine2(int arg)
{
    G* g = G::GetCurrent();
    printf("fun=routine2, id=%d, arg=%d\n", g->getId(), arg);
    runtime::yield();
    printf("fun=routine2, id=%d, arg=%d\n", g->getId(), arg);
}

int main()
{
    P p;
    M m(&p);

    m.addG(new G(&routine1, 1));
    m.addG(new G(&routine2, 1));
    m.addG(new G(&routine1, 2));
    m.addG(new G(&routine2, 2));

    m.switchG();

    return 0;
}
交替運行效果圖

最關(guān)鍵就是M默認(rèn)的g0持有的是原生的系統(tǒng)棧砰盐,對其他routine的棧的操作都要在g0中進(jìn)行。避免內(nèi)存溢出坑律,堆棧破壞岩梳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市晃择,隨后出現(xiàn)的幾起案子冀值,更是在濱河造成了極大的恐慌,老刑警劉巖藕各,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異焦除,居然都是意外死亡激况,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門膘魄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乌逐,“玉大人,你說我怎么就攤上這事创葡≌闾撸” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵灿渴,是天一觀的道長洛波。 經(jīng)常有香客問我,道長骚露,這世上最難降的妖魔是什么蹬挤? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮棘幸,結(jié)果婚禮上焰扳,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好吨悍,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布扫茅。 她就那樣靜靜地躺著,像睡著了一般育瓜。 火紅的嫁衣襯著肌膚如雪葫隙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天爆雹,我揣著相機(jī)與錄音停蕉,去河邊找鬼。 笑死钙态,一個胖子當(dāng)著我的面吹牛慧起,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播册倒,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚓挤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驻子?” 一聲冷哼從身側(cè)響起灿意,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎崇呵,沒想到半個月后缤剧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡域慷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年荒辕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片犹褒。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抵窒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叠骑,到底是詐尸還是另有隱情李皇,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布宙枷,位于F島的核電站掉房,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏慰丛。R本人自食惡果不足惜圃阳,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望璧帝。 院中可真熱鬧捍岳,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至银萍,卻和暖如春变勇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贴唇。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工搀绣, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人戳气。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓链患,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瓶您。 傳聞我的和親對象是個殘疾皇子麻捻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355