Goroutine 随笔

很早之前在lua中实现过一版协程,lua的栈是虚拟的,当要切换协程时虚拟栈不需要退栈,只需要从C的栈(物理栈)退出。恢复也简单,直接在lua的虚拟栈压入返回值,lua就可以继续运行了。这版协程比较简单,虽然不支持lua的栈与C栈交替存在,但当时也没这种需求,运行的也算问题,没什么特别的Bug。

最近在看Go,开始以为Go的routine也是虚拟栈,很快发现不对,Go是编译语言,不是解释语言,Go的栈是物理栈。当时就想物理栈退栈了怎么再回到现场的,搜了一下资料才发现自己秀逗了。物理栈不需要退栈,切换时保存好指令寄存器和栈指针寄存器,恢复的时候再把3个寄存器一恢复就行了。反而比虚拟栈还要简单些,顺着原理自己简单的实现了一把,虽然过程有点曲折,效果还算满意的。


首先是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来模拟线程存储数据,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结果溢出了 各种Bug 以为栈指针操作出了问题
        const int stackSize = 16 * 1024 * 1024;

        //构建堆栈
        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; //标识位 0表示routine还没有运行栈,栈中都是数据
        esp_ = (DWORD)p;

        dead_ = false;
    }

private:
    G() //g0的构造函数
    {
        id = _id++;
        dead_ = false;

        //g0不需要栈,用系统的栈
    }

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的切换,没实现调度

//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 //参数
        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 初始化调用栈
        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释放内存
        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()
{
    //保存现场
    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 切换堆栈,进入G的routine函数执行,由于没实现调度,所以需要代码中调用runtime::yield来切换routine。runtime::yield会调用M::yieldCurrentGM::yieldCurrentG保存现场后切换到g0g0选择下一个routine,再通过resumeG切换堆栈和指令寄存器,yieldCurrentG得以继续执行。routine函数执行完成后会回到最初的resumeGresumeG再调用M::freeG来销毁G,至此routine完结销毁。


//测试代码
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;
}
交替运行效果图

最关键就是M默认的g0持有的是原生的系统栈,对其他routine的栈的操作都要在g0中进行。避免内存溢出,堆栈破坏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350