rust设置里面那个是能见度_给Rust实现一个简单的stackful generator(上)原型与api...

d5e9ba9795641d1c6a6f1fda9c5bdd9c.png

本来我想一口气发完的,但感觉这次的文章写得实在太差,逻辑感不强,也比较长,所以本文尝试分为三部分:

  1. 原型与api https://zhuanlan.zhihu.com/p/91179318
  2. 上下文切换 https://zhuanlan.zhihu.com/p/91184528
  3. 完善功能 https://zhuanlan.zhihu.com/p/91186796

其实一开始我是想实现一个call/cc的,但其中涉及到栈拷贝,以及对象的析构问题无法解决,趟了几天坑之后,遂放弃。寻思着call/cc是一个十分依赖于语言runtime的功能,没有其它设施还真不好做。退而求其次,就实现了一个stackful generator,其实不像generator,而是与boost中context提供的continuation类似,但下文都将其称为generator吧。

初步想法

79ac1935096cab75b2c6cf5ff927da26.png

caller拿着协程的控制权,协程也拥有着主程序的控制权,可以拿着对方的generator继续执行对方的逻辑。主要提供两个接口:

  1. resume继续执行generator逻辑
  2. 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.html​www.boost.org

他这里说的就叫call_cc,和scheme中提供的call/cc等价,总之行为不一样我就还是不认为这叫call/cc(虽然call/cc也不是什么好东西,但是总有种“原教情节”在里头)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值