首页 > 系统相关 >linux c 编程 --- 协程

linux c 编程 --- 协程

时间:2023-12-16 22:12:53浏览次数:28  
标签:func2 func1 协程 --- 线程 linux 上下文 uctx

什么是协程

协程(Coroutines)是一种比线程更加轻量级的存在,协程可以理解为一个特殊的函数,这个函数可以在某个地方挂起去执行别的函数,并且可以返回挂起处继续执行。一个线程内可以由多个协程来交互运行,但是多个协程的运行是绝对串行的,也就是说同一时刻只有一个协程在运行,当一个协程运行时,其它的协程必须被挂起。

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

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

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

 

协程的特点总结如下:

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

 

进程、线程、协程的对比

进程、线程都是由操作系统所管理的,存在用户态和内核态;而协程完全在用户态运行,自己实现调度。

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

 

进程和线程在 Linux 中没有本质区别,他们最大的不同就是进程有自己独立的内存空间,而线程(同进程中)是共享内存空间。

在进程切换时需要转换内存地址空间,而线程切换没有这个动作,所以线程切换比进程切换代价更小。

为什么内存地址空间转换这么慢?Linux 实现中,每个进程的地址空间都是虚拟的,虚拟地址空间转换到物理地址空间需要查页表,这个查询是很慢的过程,因此会用一种叫做 TLB 的 cache 来加速,当进程切换后,TLB 也随之失效了,所以会变慢。

协程适用场景

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

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

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

协程库 - ucontext组件介绍

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

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

ucontext组件的头文件为<ucontext.h> ,该文件中主要关注结构体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 表示栈空间的大小。

 

getcontext()、setcontext()函数介绍

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

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

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

makecontext()、swapcontext()函数介绍

// makecontext, swapcontext - manipulate user context
#include <ucontext.h>
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() 函数的组合。

ucontext组件使用举例

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

#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>

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

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

        return 0;
}

编译并执行上述代码,结果如下:

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

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

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

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

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

……

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

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

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

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

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:

  通过结果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:

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

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

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

 

标签:func2,func1,协程,---,线程,linux,上下文,uctx
From: https://www.cnblogs.com/god-of-death/p/17908467.html

相关文章

  • 初中英语优秀范文100篇-029Sports in Our School-我们学校的运动
    PDF格式公众号回复关键字:SHCZFW029记忆树1Sportsinourschoolhavechangedalot.翻译我们学校的运动会发生了很多变化。简化记忆变化句子结构这是一个主谓宾结构的句子,其中"Sportsinourschool"是主语,表示我们学校的运动项目;"havechanged"是谓语动词,表示......
  • 【理论篇】SaTokenException: 非Web上下文无法获取Request问题解决 -理论篇
    在我们使用sa-token安全框架的时候,有时候会提示:SaTokenException:非Web上下文无法获取Request错误截图:在官方网站中,查看常见问题排查:错误追踪:跟着源码可以看到如下代码:从源码中,我们可以看到,由于非Web上下文中无法直接获取HttpServletRequest对象,因此无法直接在子线程中使用SA-Token......
  • Java-用递归的思想求斐波那契数列第n项的值
    一、思想-多路递归多路递归multirecursion就是在每次递归时包含多次(大于一次)的自身调用。也就是一个问题会被拆分成多个子问题。多路递归比单路递归在分析时间复杂度上比较复杂一些。二、斐波那契数列三、例子以n=4为例,当我们用下面(第四部分)的代码实现时,这个多路递归的求解过......
  • deep - glu:卷积神经网络和Bi-LSTM模型的结合,使用ProtBert和手工特征来识别l
    Deepro-Glu:combinationofconvolutionalneuralnetworkandBi-LSTMmodelsusingProtBertandhandcraftedfeaturestoidentifyl会议时间:2022-10-30会议地点:腾讯会议关键词:lysineglutaryation,BERT,deeplearning,proteinlanguagemodels作者:XiaoWang期刊:Bioinform......
  • 第七章:集成Redis、dubbo和dubbo-ssm
    一、集成redis二、集成dubbo三、集成dubbo-ssm......
  • 金蝶云星空-二次开发笔记
    金蝶云星空-二次开发笔记目录零、资料0.1、公共0.2、报表0.3、插件一、概述1.1、平台介绍1.2、部分业务介绍零、资料0.1、公共知识地图:https://vip.kingdee.com/article/392699482837824512?productLineId=1&isKnowledge=2交流社区:https://vip.kingdee.com/search?productI......
  • 2023-2024-1 20232310 《网络空间安全导论》第六章学习
    教材内容学习总结教材学习过程中的问题和解决过程问题1:不理解半虚拟化解决过程:通过询问ChatGPT对半虚拟化有了初步概念,并获知wsl就是半虚拟化的一种,从而对半虚拟化有了更加具体的认识。问题2:什么叫去中心化?解决方案:询问ChatGPT。了解到去中心化是指从原本的中......
  • 2023-12-16:用go语言,给定整数数组arr,求删除任一元素后, 新数组中长度为k的子数组累加和
    2023-12-16:用go语言,给定整数数组arr,求删除任一元素后,新数组中长度为k的子数组累加和的最大值。来自字节。答案2023-12-16:来自左程云。灵捷3.5大体步骤如下:算法maxSum1分析:1.计算输入数组arr的长度n。2.如果n<=k,则返回0。3.初始化ans为int类型的最小值(math......
  • 无涯教程-Java - int length()函数
    此方法返回此字符串的长度。长度等于字符串中16位Unicode字符的数量。intlength()-语法这是此方法的语法-publicintlength()intlength()-返回值此方法返回此对象表示的字符序列的长度。intlength()-示例importjava.io.*;publicclassTest{publicstati......
  • 2023-12-16:用go语言,给定整数数组arr,求删除任一元素后, 新数组中长度为k的子数组累加和
    2023-12-16:用go语言,给定整数数组arr,求删除任一元素后,新数组中长度为k的子数组累加和的最大值。来自字节。答案2023-12-16:来自左程云。灵捷3.5大体步骤如下:算法maxSum1分析:1.计算输入数组arr的长度n。2.如果n<=k,则返回0。3.初始化ans为int类型的最小值(math.MinInt32)......