C语言---协程介绍

1、协程概念

1.1 什么是进程和线程?
  在了解协程之前先复习下进程和线程的概念,有助于我们更好的理解协程。

  进程是正在运行的程序的实例,进程拥有代码和打开的文件资源、数据资源、独立的内存空间。

  线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以有很多线程,每条线程并行执行不同的任务,他们共享进程所拥有的的资源,但线程拥有自己的栈空间。

C语言---协程介绍_第1张图片

  对操作系统而言,线程是最小的执行单元,进程是最小的资源管理单元。无论是进程还是线程,都是被操作系统所管理的,当发生进程(或线程)的切换,操作系统需要执行”用户态–>内核态–>用户态“切换操作,将切换内容(上下文)保存到内存中(或内核栈)中。

1.2 什么是协程?

  协程(Coroutines)是一种比线程更加轻量级的存在,协程可以理解为一个特殊的函数,这个函数可以在某个地方挂起去执行别的函数,并且可以返回挂起处继续执行。

  一个线程内可以由多个协程来交互运行,但是多个协程的运行是绝对串行的,也就是说同一时刻只有一个协程在运行,当一个协程运行时,其它的协程必须被挂起。

C语言---协程介绍_第2张图片

  协程不是被操作系管理的,而是是在用户态执行,完全由程序所控制的,根据程序员所写的调度策略,通过协作(而不是抢占)来进行切换的。协程的本质思想就是控制函数运行时的主动让出(yield)和恢复(resume)。每个协程有自己的上下文,其切换由自己控制,当前协程切换到其它协程是由当前协程自己控制的。

协程函数与普通函数的区别:

  • 普通函数执行完后会退出,并释放栈帧。
  • 协程函数可以在运行过程中保存上下文(栈帧),并主动切换到其它线程执行,还可以通过其它协程协作返回本函数继续执行。
    C语言---协程介绍_第3张图片

协程的特点总结如下:

  • 用户态:协程是在用户态实现调度。
  • 轻量级:协程不在内核调度,不需要内核态和用户态之间切换,使用开销比较小。
  • 非抢占:协程是由用户自己实现调度,并且同一时间只能有一个协程在执行,协程主动交出CPU资源。

1.3 进程、线程、协程的对比

  • 进程、线程都是由操作系统所管理的,存在用户态和内核态;而协程完全在用户态运行,自己实现调度。
  • 一个进程可以包含多个线程,一个线程可以包含多个协程。
  • 一个进程最少包含一个线程;但线程内可以不存在协程。
  • 当CPU存在多个内核时,一个进程的多个线程可以并行执行;但是一个线程中的多个协程一定是串行执行的。
  • 进程、线程、协程的切换都是上下文切换,区别如下:
    • 进程的切换上下文:切换虚拟地址空间,切换内核栈和硬件上下文,切换内容保存在内存中。
    • 线程的切换上下文:切换内核栈和硬件上下文,切换内容保存在内核栈中。
    • 协程的切换上下文:切换硬件上下文,切换内容保存在用户态的变量(用户栈或堆)中。
  • 进程、线程、协程的调度开销程度: 进程 > 线程 >> 协程

1.4 适用场景

  IO密集型 (IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。)。

  在IO密集型场景下,协程的调度开销比线程小,能够快速实现调度,尽可能的提升CPU利用率。

  协程不适用于计算密集型场景,因为协程不能很好的利用多核CPU。

2、ucontext组件

  ucontext使得linux程序可以在用户态执行上下文切换,从而避免了进程或者线程切换导致的切换用户空间、切换堆栈,因此,效率相对更高。

  ucontext属于glibc中的组件,ucontext提供4个函数 getcontext() setcontext() makecontext() swapcontext(),及 2个结构体ucontext_t mcontext_t,使用4个函数可以在线程中实现用户级的协程切换。这四个函数的作用可以通过linux man手册(举例:man getcontext)进行查看。

  ucontext组件的头文件为 ,该文件中主要关注结构体struct ucontext_t,内容如下:

/* Userlevel context.  */
typedef struct ucontext_t
  {
    unsigned long int __ctx(uc_flags);
    struct ucontext_t *uc_link;
    stack_t uc_stack;
    mcontext_t uc_mcontext;
    sigset_t uc_sigmask;
    struct _libc_fpstate __fpregs_mem;
  } ucontext_t;

  其中:

  uc_link 指向后继上下文(即,当前协程运行结束后,接着要被恢复的下一个协程的上下文);

  uc_stack 为该上下文中使用的栈;

  uc_sigmask 为该上下文中的阻塞信号集合(可通过man sigprocmask 命令查看相关信息);

  uc_mcontext 这个结构体依赖于机器且不透明,作用是保存硬件上下文,包括硬件寄存器的值 。

  uc_stack协程栈对应的stack_t 结构体的内容如下:

/* Structure describing a signal stack.  */
 typedef struct 
 {
     void  *ss_sp;     /* Base address of stack */
     int    ss_flags;  /* Flags */
     size_t ss_size;   /* Number of bytes in stack */
 } stack_t;

其中:

ss_sp指针 指向的是协程的栈空间的起始地址,可以是用户级的栈变量指针,也可以是堆变量指针。

ss_flags flag,在协程使用中设置该值为0。具体作用参考man sigaltstack

ss_size 表示栈空间的大小。

2.1 getcontext()、setcontext()函数介绍

// getcontext, setcontext - get or set the user context
#include 
int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);

getcontext() 函数初始化ucp结构体,将当前上下文保存在ucp中 。

setcontext() 函数设置当前上下文为ucp所指向的上下文。 ucp 所指向的上下文应该是调用 getcontext()makecontext() 获得的。

  返回值:当调用成功后,getcontext()返回0,setcontext()不会返回。当调用失败时,两个函数都返回-1并设置合适的errno。

2.2 makecontext()、swapcontext()函数介绍

// makecontext, swapcontext - manipulate user context
#include 
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

  makecontext() 函数修改ucp 指向的上下文,该上下文是之前通过调用getcontext() 获取的,也就是说在调用makecontext()之前需要调用getcontext()。且在调用makecontext()之前,调用者必须分配一个新的栈空间为该上下文,并将栈空间地址设置到 ucp->uc_stack,还要设置ucp->uc_link指向一个后继协程的上下文。如果func协程上下文中uc_link值为NULL,则执行完该协程后,线程会退出。

  makecontext()函数将ucp 对应的上下文中的指令地址指向 func函数(协程)地址,argc表示func的入参个数,如果入参为空,该值设置为0。如果argc有值,入参为一系列 (int)整型的数据。

  swapcontext() 保存当前协程的上下文到oucp,然后激活(切换到) ucp所指向的上下文对应的协程。返回值:当调用成功后,swapcontext()不会返回;当调用失败后,返回-1并设置合适的errno。

  swapcontext() 函数可以理解成为 getcontext()setcontext() 函数的组合。

3、ucontext组件使用举例

3.1 getcontext()、setcontext()函数使用举例

  下面的代码摘自网上示例

#include 
#include 
#include 

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

        getcontext(&context);
        sleep(1);
        puts("Hello world");
        setcontext(&context);

        return 0;
}

  编译并执行上述代码,结果如下:C语言---协程介绍_第4张图片

  如图所示,程序在输出第一个“Hello world"后并没有退出程序,而是持续不断的输出”Hello world“。代码执行过程如下:

1、getcontext(&context); 初始化并保存了当前的上下文;

2、执行sleep语句,接着输出"Hello world";

3、执行setcontext(&context); 语句,将当前上下文设置为context中保存的值。注意:此时指令地址指向了**sleep(1);**语句的地址;

4、代码跳转**sleep(1)**重新执行;

……

如此往复,所以导致程序不断的输出”Hello world“。

3.2 makecontext()、swapcontext()函数使用举例

  下面的代码摘自man手册man makecontext

#include 
#include 
#include 

static ucontext_t uctx_main, uctx_func1, uctx_func2;

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

static void func1(void)
{
    printf("func1: started\n");
    printf("func1: swapcontext(&uctx_func1, &uctx_func2)\n");
    if (swapcontext(&uctx_func1, &uctx_func2) == -1)
        handle_error("swapcontext");
    printf("func1: returning\n");
}

static void func2(void)
{
    printf("func2: started\n");
    printf("func2: swapcontext(&uctx_func2, &uctx_func1)\n");
    if (swapcontext(&uctx_func2, &uctx_func1) == -1)
        handle_error("swapcontext");
    printf("func2: returning\n");
}


int main(int argc, char *argv[])
{
    char func1_stack[16384];
    char func2_stack[16384];

    if (getcontext(&uctx_func1) == -1)
        handle_error("getcontext");
    uctx_func1.uc_stack.ss_sp = func1_stack;
    uctx_func1.uc_stack.ss_size = sizeof(func1_stack);
    uctx_func1.uc_link = &uctx_main;
    makecontext(&uctx_func1, func1, 0);

    if (getcontext(&uctx_func2) == -1)
        handle_error("getcontext");
    uctx_func2.uc_stack.ss_sp = func2_stack;
    uctx_func2.uc_stack.ss_size = sizeof(func2_stack);
    /* Successor context is f1(), unless argc > 1 */
    uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;
    makecontext(&uctx_func2, func2, 0);

    printf("main: swapcontext(&uctx_main, &uctx_func2)\n");
    if (swapcontext(&uctx_main, &uctx_func2) == -1)
        handle_error("swapcontext");

    printf("main: exiting\n");
    exit(EXIT_SUCCESS);
}

编译并执行上述代码

结果1
C语言---协程介绍_第5张图片

  通过结果1的打印,我们可以看到:

  1)程序先从main函数切换到协程func2进行执行;

  2)在func2中主动挂起(yield)并进入协程func1中执行;

  3)然后在func1协程中主动挂起(yield)并返回func2中(resume)执行剩余代码;

  4)协程func2执行完后,根据main函数中设置的“ uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;”又返回协程func1中执行剩余代码;

  5)协程func1执行完后,根据main函数中设置的“uctx_func1.uc_link = &uctx_main;”返回main函数执行剩余的代码;

  6)最后退出。

  注意:swapcontext()函数会保存当前上下文到第一个参数,然后切换到第二个参数所指向的协程上下文。

结果2

C语言---协程介绍_第6张图片

  如图所示,两个结果的差别是图2中多了一个参数(“带参数”),对应代码中的区别是:

“uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;

  当入参中argc值大于1时,设置uctx_func2.uc_link 值为NULL,此时表示该协程执行完后没有后继上下文,该协程执行完后线程就退出了,因为这个程序中就只有一个线程,所以程序也退出了。

4、参考文档

1、协程:https://www.jianshu.com/p/6dde7f92951e

2、C语言实现协程:https://blog.csdn.net/xiaobing1994/article/details/79012848/?

3、linux的man手册

C语言---协程介绍_第7张图片

你可能感兴趣的:(C语言,linux,经验分享,c语言)