setjmp的使用

以前对于C语言的setjmp和longjmp从来都是知道有这么个函数, 但是不知道什么情况下要使用, 甚至于不知道setjmp的实现机制是什么样子的.

这次在实现coroutine的过程中虽然没有使用setjmp来实现, 但是由于setjmp来实现coroutine的所有操作都是可以在用户态进行的, 因此顺便研究了一下setjmp的实现机制.

与我猜测大致一样, 在call stack上来讲setjmp一定先于或等于longjmp处于的位置, 这样其实longjmp所做的不过就是将当前寄存器上下文恢复成setjmp函数保存的值即可. 调用longjmp的函数与调用setjmp的函数共用一个栈空间, 因此longjmp恢复完寄存器后造成的结果就是恢复寄存器上下文并将栈空间释放到setjmp位于的地方.

那么这就很好解释为什么不能将setjmp使用函数再封装一层了, 假设使用函数封装setjmp代码如下:

int try(jmp_buf jmp)
{
int err;
err = setjmp(jmp);
printf("%d\n", err);
return err;
}

void execption(jmp_buff j, int err)
{
return longjmp(j, err)
}

int main()
{
jmp_buff j;
try(j);
execption(j, 2);
}

如上代码其实try函数与execption共用部分栈空间, 当longjmp回到try函数时, 即使用寄存器被恢复了, 但是事实上try函数的栈结构已经被破坏了, 此时longjmp之后try函数的行为是无法预料的.

------------------------
|        main          |
|-----------------------
|     try |  excption  |
|---------|            |
|---------|------------|

在使用setjmp时一定要注意栈是否被覆盖的问题, 即调用setjmp时所保存的esp的信息直到调用前longjmp之前, 不应该会出更有比setjmp保存esp时更多的空闲空间(如栈是自顶向下增加, 则setjmp到longjmp之间出现的esp的值绝不能大于setjmp保存的esp的值.

实现了一个coroutine

在学习了lua之后, 总算是搞清楚了颇为流传的coroutine是怎么回事. 我发现coroutine就是我一直希望使用的那种多任务处理方式, 即有线程的清晰抽象, 又可以避免锁带来的开销以及其他并发问题, 可以说是线程和异步处理的一个最佳折中点.

在大约一年前, 我一直在为不能控制多线程的调度而苦恼(那里还不知道在Windows下Sleep(0)可以暂时让出cpu的控制权), 我甚至试图使用状态机来模拟多线程以便我可以控制线程调度, 后来因为这个方法对于代码的可维护性来讲付出的代价太大, 最终没敢使用. 再后来又饱受线程同步, 锁碰撞造成上下文切换的困扰. 在coroutine概念中, 每一个coroutine自已来决定什么时间来让出cpu时间, 由于是coroutine自行让出cpu执行时间, 那么也就不存在有些操作被中断的问题, 自然也就避免了锁碰撞的问题.

lua的coroutine是半对称的, 因此最坏的情况要比对称的coroutine多出一倍的开销. 但是相比对称协程而言半对称协程的调度可以更灵活, 逻辑上也更为清晰, 而对称协程则使得程序略显不清晰, 每一个协程可能需要对整个协作过程有所了解, 当然是否要使用对称coroutine就要看情况而定. 花了大概一天半业余时间终于实现linux和Windows版本的coroutine, 以后会尽量采用coroutine而非thread来解决多任务工作.

btw, 协程与线程相比, 开销就小在不会有锁碰撞造成上下文切换, 但是coroutine的切换不当有时候可能会比线程的锁碰撞带来的开销更大, 因此在写coroutine时一定要注意.


2月1号补充:
原生协程是在用户态切换上下文因此要比类似swapcontext之类的系统调用销小很多.
2月4号补充:
线程开销除了context切换开销之外还有调度开销.