zoukankan      html  css  js  c++  java
  • 实现一个简单的C++协程库

    之前看协程相关的东西时,曾一念而过想着怎么自己来实现一个给 C++ 用,但在保存现场恢复现场之类的细节上被自己的想法吓住,也没有深入去研究,后面一丢开就忘了。近来微博上看人在讨论怎么实现一个 user space 上的线程库,有人提到了 setcontext,swapcontext 之类的函数,说可以用来保存和切换上下文,我忽然觉得这应该也能用来实现协程,回头搜,果然已经有人曾用这些函数做过相关的事情,略略看了几个,觉得到底不大好用,还不如自己搞一个简单点的。

    说到 c++ 上的协程,boost 里其实已经有相关的实现了,不过接口上看用起来有些麻烦,单纯从语法上来说,我觉得 Lua 的协程最简洁易用了,概念上也比较直接,为什么不做一个类似的呢?所以我就打算照着 Lua 来山寨一个,只需要支持四个接口就够了:

    1)create coroutine。

    2)run/resume coroutine。

    3)Yield running corouinte。

    4)IsCoroutineAlive。 

    保存与恢复上下文

    实现协程/线程,最麻烦莫过于保存和切换上下文了,好在 makecontext,swapcontext 这几个函数相当好用,已经完全帮忙解决了这个难题:makecontext 可以帮我们建立起协程的上下文,swapcontext 则可以切换不同的上下文,从而实现那种把当前函数暂时停住,切换出去执行别的函数然后再切换回来继续执行的效果:

    #include <iostream>
    #include <ucontext.h>
    using namespace std;
    
    static char g_stack[2048];
    static ucontext_t ctx,ctx_main;
    
    void func()
    {
        // do something.
        cout << "enter func" << endl;
    
        swapcontext(&ctx, &ctx_main);
    
        cout << "func1 resume from yield" << endl;
        // continue to do something.
    }
    
    int main()
    {
       getcontext(&ctx);
       ctx.uc_stack.ss_sp = g_stack;
       ctx.uc_stack.ss_size = sizeof g_stack;
       ctx.uc_link = &ctx_main;
        
       makecontext(&ctx, func, 0);
    
       cout << "in main, before coroutine starts" << endl;
    
       swapcontext(&ctx_main, &ctx);
    
       cout << "back to main" << endl;
    
       swapcontext(&ctx_main, &ctx);
       
       cout << "back to main again" << endl;
       return 0;
    }

    如上代码所示,显然我们只要简单包装一下 swapcontext,很容易就可以实现 Yield 和 Resume,有了它们的帮助协程做起来就容易多了。

    使用与实现

    在使用 makecontext,swapcontext 的基础上,我花了一个多小时简单实现了一个协程库,参看这里,代码写下来总共才200多行,出乎意料的简单,用起来也很方便了:

    #include "coroutine.h"
    
    #include <iostream>
    
    using namespace std;
    
    CoroutineScheduler* sched = NULL;
    
    void func1(void* arg)
    {
        uintptr_t ret;
        cout << "function1 a now!,arg:" << arg << ", start to yield." << endl;
        ret = sched->Yield((uintptr_t)"func1 yield 1");
        cout << "1.fun1 return from yield:" << (const char*)ret << endl;
        ret = sched->Yield((uintptr_t)"func1 yield 2");
        cout << "2.fun1 return from yield:" << (const char*)ret << ", going to stop" << endl;
    
    }
    
    void func2(void* s)
    {
        cout << "function2 a now!, arg:" << s << ", start to yield." << endl;
        const char* y = (const char*)sched->Yield((uintptr_t)"func2 yield 1");
        cout << "fun2 return from yield:" << y <<", going to stop" << endl;
    }
    
    int main()
    {
        sched = new CoroutineScheduler();
    
        bool stop = false;
        int f1 = sched->CreateCoroutine(func1, (void*)111);
        int f2 = sched->CreateCoroutine(func2, (void*)222);
    
        while (!stop)
        {
            stop = true;
            if (sched->IsCoroutineAlive(f1))
            {
                stop = false;
                const char* y1 = (const char*)sched->ResumeCoroutine(f1, (uintptr_t)"resume func1");
                cout << "func1 yield:" << y1 << endl;
            }
    
            if (sched->IsCoroutineAlive(f2))
            {
                stop = false;
                const char* y2 = (const char*)sched->ResumeCoroutine(f2, (uintptr_t)"resume func2");
                cout << "func2 yield:" << y2 << endl;
            }
        }
    
        delete sched;
        return 0;
    }

    如上所示,Yield 里传的参数会在调用 Resume 时被返回,同理 Resume 里的第二个参数,会在 Yield 里被返回,这种机制也是模仿 Lua 来的,有些时候可以用来在协程间传递一些参数,很方便,看起来也挺酷的,但在实现上却相当地简洁,核心代码如下:

    // static function
    void CoroutineScheduler::SchedulerImpl::Schedule(void* arg)
    {
        assert(arg);
        SchedulerImpl* sched = (SchedulerImpl*) arg;
    
        int running = sched->running_;
    
        coroutine* cor = sched->id2routine_[running];
        assert(cor);
    
        cor->func(cor->arg);
    
        sched->running_ = -1;
        cor->status = CO_FINISHED;
    }
    
    // resume coroutine.
    uintptr_t CoroutineScheduler::SchedulerImpl::ResumeCoroutine(int id, uintptr_t y)
    {
        coroutine* cor = id2routine_[id];
        if (cor == NULL || cor->status == CO_RUNNING) return 0;
    
        cor->yield = y;
        switch (cor->status)
        {
            case CO_READY:
                {
                    getcontext(&cor->cxt);
    
                    cor->status = CO_RUNNING;
                    cor->cxt.uc_stack.ss_sp = cor->stack;
                    cor->cxt.uc_stack.ss_size = stacksize_;
                    // sucessor context.
                    cor->cxt.uc_link = &mainContext_;
    
                    running_ = id;
                    makecontext(&cor->cxt, (void (*)())Schedule, 1, this);
                    swapcontext(&mainContext_, &cor->cxt);
                }
                break;
            case CO_SUSPENDED:
                {
                    running_ = id;
                    cor->status = CO_RUNNING;
                    swapcontext(&mainContext_, &cor->cxt);
                }
                break;
            default:
                assert(0);
        }
    
        uintptr_t ret = cor->yield;
    
        if (running_ == -1 && cor->status == CO_FINISHED) DestroyCoroutine(id);
    
        return ret;
    }
    
    uintptr_t CoroutineScheduler::SchedulerImpl::Yield(uintptr_t y)
    {
        if (running_ < 0) return 0;
    
        int cur = running_;
        running_ = -1;
    
        coroutine* cor = id2routine_[cur];
    
        cor->yield = y;
        cor->status = CO_SUSPENDED;
    
        swapcontext(&cor->cxt, &mainContext_);
        return cor->yield;
    }

    单就代码量和程序结构而言,以上的实现很简洁,但细节上看,每个协程都要分配一个一定大小的栈空间,空间效率上可能不大好,不够轻量;运行效率上来说,swapcontext 的执行效率如何,现在也未知,只是出于学习的目的,就先这样吧,可以再了解了解别人是怎么做的。 

  • 相关阅读:
    Codeforces Round #251 (Div. 2) A
    topcoder SRM 623 DIV2 CatAndRat
    topcoder SRM 623 DIV2 CatchTheBeatEasy
    topcoder SRM 622 DIV2 FibonacciDiv2
    topcoder SRM 622 DIV2 BoxesDiv2
    Leetcode Linked List Cycle II
    leetcode Linked List Cycle
    Leetcode Search Insert Position
    关于vim插件
    Codeforces Round #248 (Div. 2) B. Kuriyama Mirai's Stones
  • 原文地址:https://www.cnblogs.com/catch/p/3617962.html
Copyright © 2011-2022 走看看