本来我想一口气发完的,但感觉这次的文章写得实在太差,逻辑感不强,也比较长,所以本文尝试分为三部分:
- 原型与api https://zhuanlan.zhihu.com/p/91179318
- 上下文切换 https://zhuanlan.zhihu.com/p/91184528
- 完善功能 https://zhuanlan.zhihu.com/p/91186796
其实一开始我是想实现一个call/cc的,但其中涉及到栈拷贝,以及对象的析构问题无法解决,趟了几天坑之后,遂放弃。寻思着call/cc是一个十分依赖于语言runtime的功能,没有其它设施还真不好做。退而求其次,就实现了一个stackful generator,其实不像generator,而是与boost中context提供的continuation类似,但下文都将其称为generator吧。
初步想法
caller拿着协程的控制权,协程也拥有着主程序的控制权,可以拿着对方的generator继续执行对方的逻辑。主要提供两个接口:
resume
继续执行generator逻辑generator
构造generator
用C语言做一个简单的原型
要完成这样的功能,「上下文切换」是必要的,glibc中ucontext.h就提供了这样的功能,我们可以先用C语言做一个原型。(winapi也提供了WinFiber可以实现类似的功能)
起手式,先在.h
文件写下接口:
typedef long long value;
typedef struct _gen_t * gen_t;
// 接受一个回调函数,返回一个generator,可以用来继续执行回调函数中的逻辑;
// 回调函数可以拿到一个generator,用来继续执行主程序的逻辑。
gen_t generator(void f(gen_t, value start));
// 用来继续执行generator的逻辑,并传入一个数据,返回时接受一个数据
// 返回1可以继续执行,返回0则表示generator已经结束
int resume(gen_t, value send, value * recv);
// 析构一个generator, 不能在协程中析构
void drop_gen(gen_t);
预期行为:
void foo(gen_t gen, value start) {
value x;
printf("foo start with %lldn", start);
resume(gen, 99, &x);
printf("foo resume from main and get %lldn", x);
resume(gen, 98, &x);
printf("foo resume from main and get %lldn", x);
resume(gen, 97, &x);
printf("foo resume from main and get %lldn", x);
}
int main() {
value x = 100;
gen_t foo_gen = generator(foo);
for (int i = 0; resume(foo_gen, i, &x); ++i) {
printf("main resume from foo and get %lldn", x);
}
drop_gen(foo_gen);
return 0;
}
/* output
foo start with 0
main resume from foo and get 99
foo resume from main and get 1
main resume from foo and get 98
foo resume from main and get 2
main resume from foo and get 97
foo resume from main and get 3
*/
为了实现这个功能,我们要
- 保存两个上下文,在之间来回切换;
- 还需要一个send buf来保存交互的信息;
- 当然还有一个协程执行的栈。
我们可以导出一个gen_t的结构:
/*
gen dual_gen
+-----+ +-----+
|state+<-----+dual |
+-----+ +-----+
|send | |ctx |
+-----+ +-----+
|ctx | |send |
+-----+ +-----+
|dual +----->+state|
+-----+ +-----+
|stack|
| |
| |
| |
| |
| |
| |
| |
| |
+-----+
*/
struct _gen_t {
int state; // generator的状态,0代表已完成,1代表还可以运行
value send; // 传递值的buffer
gen_t dual; // 对偶的另外一个generator
ucontext_t ctx; // 使用ucontext保存的上下文信息
char stack[]; // 协程所运行的栈空间
};
对应的结构已经明确,那么实现就可以自明了。
1.generator
函数构造两个生成器,协程的生成器返回给主程序,主程序的生成器传入协程中:
gen_t generator(void f(gen_t, value)) {
// 协程入口点
void bootstrap(gen_t, void (gen_t, value));
gen_t gen = malloc(sizeof(struct _gen_t) + DEFAULT_STACK_SIZE); // 协程generator
gen_t dual_gen = malloc(sizeof(struct _gen_t)); // caller的generator
gen->state = 1;
gen->dual = dual_gen;
dual_gen->state = 1;
dual_gen->dual = gen;
// 初始化协程上下文
getcontext(&gen->ctx);
gen->ctx.uc_stack.ss_sp = &gen->stack;
gen->ctx.uc_stack.ss_size = DEFAULT_STACK_SIZE;
gen->ctx.uc_stack.ss_flags = 0;
// 继承caller上下文,使得协程执行完之后可以继续执行主程序
gen->ctx.uc_link = &dual_gen->ctx;
dual_gen->ctx.uc_link = NULL;
// 协程入口点为booststrap,并传递caller的generator和协程generator的逻辑
makecontext(&gen->ctx, (void (*)(void)) bootstrap, 2, dual_gen, f);
return gen;
}
2.bootstrap
启动协程,调用协程的回调函数
void bootstrap(gen_t gen, void f(gen_t, value)) {
f(gen, gen->dual->send);
// 当generator完成后,将对应的状态设置为0
gen->dual->state = 0;
}
3.resume
继续generator,切换到协程的上下文
int resume(gen_t gen, value send, value* recv) {
if (!gen->state) { return 0; }
gen->send = send;
// 切换到另一个上下文
swapcontext(&gen->dual->ctx, &gen->ctx);
*recv = gen->dual->send;
return gen->state;
}
4.drop_gen
释放generator的内存……不过C语言没有所谓的资源回收,也当然没有unwind stack的概念了。。。
这已经是比较完整的代码了,可以运行上面的”样例“,以及一些更加复杂的例子。
在Rust中设计更安全的API
刚刚C语言的版本,也就仅仅能用。我们希望使用Rust利用「类型系统」、「所有权」、「RAII」机制提供一套更通用更安全的api。
在C语言中之所用long long表示通用的类型,是因为long long足够“长”,足以表示一个指针或者值。而在Rust中有泛型,我们可以用泛型表示更精确的语义:
struct Gen<Send, Recv> { /*...*/ }
在C语言中没有模式匹配,也没有好的封装机制,所以“返回结果”一般用「返回参数」来表示,返回值来代表状态。Rust中就没有这样的约定,resume的签名可以改成:
fn resume(&mut self, send: Send) -> Option<Recv>
在C语言版本中,我们可以在协程中可以析构主线程的generator,但我们并不希望被这样调用。而在Rust中只需要在协程中拿不到generator的所有权就可以防止这样的行为,而resume的确只需要拿到可变引用就可以了(并不,后面再说):
fn generator<Send, Recv>(f: for<'g> fn(&'g mut Gen<Recv, Send>, Send)) -> Box<Gen<Send, Recv>>
/* ^
|
可变引用,防止用户在携程内析构generator ---+
*/
// 其实不应该返回Box<Gen>的,因为deref会move走里面的Gen,会导致ub。
// 至少用一个新的结构体包着,后面会讲
Gen的内部结构我们也能用类型代表更精细的结构:
#[derive(Copy, Clone, Debug)]
enum GenState {
Complete,
Yield,
Ready,
}
pub struct Gen<Send, Recv> {
state: GenState,
ctx: Ctx,
stack: Option<Vec<u8>>, // 主程序generator不拥有栈空间,协程的拥有
send: Option<Send>, // 传递值的buffer,可空
dual: Option<NonNull<Gen<Recv, Send>>>, // 对偶的另一个gen,Option用来防止重复析构
cb: Option<for<'g> fn(&'g mut Gen<Recv, Send>, Send)>
// 协程generator要执行的逻辑
}
现在可以使用ffi使用glibc的ucontext,把Rust版的功能给实现完(和C版本的几乎一样)。但事实上现在这个api还很不完善,我们将在后面的章节,将功能补完。
伪小结
其实这个api其实就是将boost中context提供的continuation实现了一遍(当然没有其完备)。
https://www.boost.org/doc/libs/1_71_0/libs/context/doc/html/context/cc.htmlwww.boost.org他这里说的就叫call_cc,和scheme中提供的call/cc等价,总之行为不一样我就还是不认为这叫call/cc(虽然call/cc也不是什么好东西,但是总有种“原教情节”在里头)。