C++协程实现

First Post:

Last Update:

Word Count:
1.5k

Read Time:
7 min

协程实现

C

1. 利用 C 语言的 setjmp 和 longjmp,函数中使用 static local 的变量来保存协程内部的数据。

函数原型:

1
2
int setjmp(jmp_buf envbuf);
void longjmp(jmp_buf envbuf, int val);

先调用setjmp,用变量envbuf记录当前的位置,然后调用longjmp,返回envbuf所记录的位置,并使setjmp的返回值为val。使用longjmp后,envbuf的内容会被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 1 #include <stdio.h> 
2 #include <setjmp.h>
3
4 jmp_buf buf;
5
6 banana()
7 {
8 printf("in banana() \n");
9 longjmp(buf,1);
10 printf("you'll never see this,because i longjmp'd");
11 }
12
13 main() {
15 if(setjmp(buf))
16 printf("back in main\n");
17 else
18 {
19 printf("first time through\n");
20 banana();
}
}

打印结果:

1
2
3
4
5
first time through

in banana()

back in main

2、利用C语言语法switch-case的技巧来实现

设置一个标识符,改变标识符的值,通过switch-case对标识符值的判断操纵各协程函数轮流执行。

每个协程函数可配一个结构体,保存栈内容和状态机。

代码:https://github.com/georgeredinger/protothreads

3、使用汇编代码来切换上下文(实现c协程) 。

构建一个结构体保存栈内容和当前位置等上下文信息,利用汇编语言的跳转实现协程功能。

详情见:https://www.cnblogs.com/heluan/p/9899824.html

4、利用操作系统提供的接口:Linux的ucontext,Windows的Fiber。(云风的coroutine)

ucontext: makecontext()       创建上下文   

      getcontext()         读取上下文

      setcontext()         设置上下文

      swapcontext()        跳转上下文

Fiber(纤程):ConverThreadToFiber()    从当前线程进入纤程

      CreateFiber()         创建新纤程

      SwitchToFiber()        切换到纤程

      DeleteFiber()         删除纤程

                    如果删除当前纤程,会导致它所在的线程退出

操作系统的接口函数本身,提供了保存栈内容的功能。

C++

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

1
2
3
4
create coroutine
run/resume coroutine
Yield running corouinte
IsCoroutineAlive

存与恢复上下文

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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多行,出乎意料的简单,用起来也很方便了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#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 来的,有些时候可以用来在协程间传递一些参数,很方便,看起来也挺酷的,但在实现上却相当地简洁,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 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;
}

ref: https://www.cnblogs.com/catch/p/3617962.html

打赏点小钱
支付宝 | Alipay
微信 | WeChat