0%

协程库实现(一)

  • 参考

    ucontext.h上下文切换

  • 上下文结构体定义
  • mcontext_t类型与机器相关,并且不透明.ucontext_t结构体则至少拥有以下几个域:
    typedef struct ucontext {
    struct ucontext *uc_link;
    sigset_t uc_sigmask;
    stack_t uc_stack;
    mcontext_t uc_mcontext;
    ...
    } ucontext_t;
  • 当当前上下文(如使用makecontext创建的上下文)运行终止时系统会恢复uc_link指向的上下文;uc_sigmask为该上下文中的阻塞信号集合;uc_stack为该上下文中使用的栈;uc_mcontext保存的上下文的特定机器表示,包括调用线程的特定寄存器等

    四个操作函数

  • int getcontext(ucontext_t *ucp);
    • 初始化ucp结构体,将当前的上下文保存到ucp中
  • int setcontext(const ucontext_t *ucp);
    • 设置当前的上下文为ucp,setcontext的上下文ucp应该通过getcontext或者makecontext取得,如果调用成功则不返回。
    • 如果上下文是通过调用getcontext()取得,程序会继续执行这个调用。如果上下文是通过调用makecontext取得,程序会调用makecontext函数的第二个参数指向的函数,如果func函数返回,则恢复makecontext第一个参数指向的上下文第一个参数指向的上下文context_t中指向的uc_link
    • 如果uc_link为NULL,则线程退出。
  • void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
    • makecontext修改通过getcontext取得的上下文ucp(这意味着调用makecontext前必须先调用getcontext)。然后给该上下文指定一个栈空间ucp->stack,设置后继的上下文ucp->uc_link.
    • 当上下文通过setcontext或者swapcontext激活后,执行func函数,argc为func的参数个数,后面是func的参数序列。当func执行返回后,继承的上下文被激活,如果继承上下文为NULL时,线程退出
  • int swapcontext(ucontext_t *oucp, ucontext_t *ucp);
    • 保存当前上下文到oucp结构体中然后激活upc上下文
  • 如果执行成功,getcontext返回0,setcontextswapcontext不返回;如果执行失败,getcontext,setcontext,swapcontext返回-1,并设置对于的errno.
函数名 作用
getcontext 获取上下文
setcontext 设置上下文
swapcontext 保存当前上下文,切换上下文
makecontext 创建新的上下文
  • 例子
    #include <stdio.h>
    #include <ucontext.h>
    #include <unistd.h>

    int main(int argc, const char *argv[]){
    ucontext_t context;

    getcontext(&context);
    puts("Hello world");
    sleep(1);
    setcontext(&context);
    return 0;
    }
  • 上述函数会不断重复输出Hello World,因为getcontext将上下文设置在了输出之前,setcontext每次都把整个执行流返回到输出的位置

更换为执行时间最短先服务逻辑的调度器版本

  • 参考的原版
  • 我改进代码的github仓库
  • 使用了优先级队列重写携程选择逻辑,切换的时候会选择当前已经运行时间最短的携程上处理机执行
  • 运行结果
    • picture 0
    • 四个线程的优先级分别是4,3,2,1,可见是符合设置的

      1月12日修改

  • 修改为了多线程版本,分别调度,分别设置优先级
  • picture 1
  • picture 2

注意事项

  • C/C++文件编译的时候每个源文件都是独立编译的,这样会导致即使头文件使用了ifndef之类的保护,仍然可能在整个项目中被不同的文件引用多次
    • 因此如果头文件中出现了函数或者是变量的定义的话,这个变量在整个项目中会被定义多次
    • 因此头文件中只能声明,变量使用extern关键字声明
    • 必须定义的函数用inline
  • 注意类的静态static成员变量必须在某个源文件中定义,才能在其他源文件中使用类名和作用域运算符::访问
  • C++可以使用宏__FILE__判断自己所处的文件名称
  • makecontext的第二个参数无论传入的是什么函数,都要将其转换为void(*)(void)类型的函数指针,然后再给出参数
  • 执行流路径
    -----------调度器或main中-----------------
    创建协程(create)-->
    获取互斥锁
    使用makecontext生成开始执行协程函数的上下文
    令目标函数的上下文的后继位置时刻指向调度器schedule的上下文main
    释放互斥锁
    切换到协程(resume)-->
    将当前的上下文保存在调度器schedule.main中,切换到目标函数的上下文
    -----------协程函数中----------------
    切换回main(yield)-->
    获取锁
    将协程当前的上下文保存在ctx中
    释放锁
    切换回之前指向的main上下文
    -----------------main中-----------------
    ...
  • 因为添加协程create函数与协程调度器不在一个线程中,可能会有竞争关系,因此添加了互斥锁防止冲突

<setjmp.h>实现切换

setjmp 函数:

  • setjmp 用于保存当前程序的执行状态,并返回一个整数值。

  • 当首次调用 setjmp 时,它返回0,表示保存了当前执行状态。

  • 当从 longjmp 调用返回时,setjmp 返回一个非零值,通常用于区分正常返回和通过 longjmp 返回

    longjmp函数

  • longjmp 用于恢复之前由 setjmp 保存的执行状态。

  • 它接受两个参数:保存的执行状态(由 setjmp 返回的值)和一个非零的返回值。

  • 调用 longjmp 会导致程序跳转到相应 setjmp 处,并且 setjmp 返回的值为 longjmp 的返回值。

    一个例子

  • 参考

    #include <stdio.h>
    #include <stdlib.h>
    #include <setjmp.h>

    jmp_buf env;

    int my_func(int a, int b) {
    if (b == 0) {
    printf("do not allow division by 0\n");
    longjmp(env, 1);
    }
    return a / b;
    }

    int main(int argc, char const *argv[]) {
    int res = setjmp(env);
    if (res == 0) {
    printf("return from setjmp\n");
    my_func(10, 0);
    } else {
    printf("return from longjmp: %d\n", res);
    }
    return 0;
    }

    C++语言的锁机制

    unique_lock

  • #include <mutex>#include <thread>

    std::unique_lock<std::mutex> lock(myMutex);  // 构造 unique_lock,并锁定互斥量
  • 构造就是加锁

  • 析构就是解锁

    lock.lock();  // 手动锁定互斥量

    // 锁定期间的其他操作...

    lock.unlock(); // 手动释放锁

    // 在 unique_lock 离开作用域时,会自动释放锁
  • 同样支持手动操作,加锁和解锁