基于ucontext库实现协程类

news2024/10/6 21:43:49

文章目录

  • 前言
  • 协程基础知识
    • 协程上下文
    • 对称协程与⾮对称协程
    • 有栈协程与⽆栈协程
  • ucontext库
    • 接口熟悉
    • 一个简单的函数切换
    • 自动调用
  • 协程类的实现
    • 接口
    • 全局变量
    • 线程局部变量
    • malloc封装
    • 协程切换
    • 构造函数
    • 协程执行的方法
  • 测试
    • 协程切换
    • 手动切换+复用


前言

    协程(Coroutine)是一种"轻量级线程, 用户态线程",允许在执行过程中暂停和恢复执行,从而实现更加灵活的控制流程。与线程不同,协程在用户空间由程序自身调度,不需要操作系统的调度器介入, 我们使用ucontext实现一个简单的协程类

协程基础知识

协程上下文

    协程则可以执⾏到⼀半就退出(称为yield),但此时协程并未真正结束,只是暂时让出CPU执⾏权,在后⾯适当的时机协程可以重新恢复运⾏(称为resume),在这段时间⾥其他的协程可以获得CPU并运⾏, 协程能够半路yield、再重新resume的关键是协程存储了函数在yield时间点的执⾏状态,这个状态称为协程上下⽂

对称协程与⾮对称协程

    对称协程,协程可以不受限制地将控制权交给任何其他协程。任何⼀个协程都是相互独⽴且平等的,调度权可以在任意协程之间转移。于是调度起来可以这样, 协程1,协程2, 协程3之间是可以通过协程调度器可以切换到任意协程。
在这里插入图片描述

    ⾮对称协程,是指协程之间存在类似堆栈的调⽤⽅-被调⽤⽅关系。协程出让调度权的⽬标只能是它的调⽤者, 切换线程必须先回到主协程
在这里插入图片描述

有栈协程与⽆栈协程

  • 有栈协程:⽤独⽴的执⾏栈来保存协程的上下⽂信息。当协程被挂起时,栈协程会保存当前执⾏状态(例如函数调⽤栈、局部变量等),并将控制权交还给调度器。当协程被恢复时,栈协程会将之前保存的执⾏状态恢复,从上次挂起的地⽅继续执⾏。类似于内核态线程的实现,不同协程间切换还是要切换对应的栈上下⽂,只是不⽤陷⼊内核⽽已
  • ⽆栈协程:它不需要独⽴的执⾏栈来保存协程的上下⽂信息,协程的上下⽂都放到公共内存中,当协程被挂起时,⽆栈协程会将协程的状态保存在堆上的数据结构中,并将控制权交还给调度器。当协程被恢复时,⽆栈协程会将之前保存的状态从堆中取出,并从上次挂起的地⽅继续执⾏。协程切换时,使⽤状态机来切换,就不⽤切换对应的上下⽂了,因为都在堆⾥的。⽐有栈协程都要轻量许多

ucontext库

    ucontext是GNU C库提供的⼀组创建,保存,切换⽤户态执⾏上下⽂的接口,我们可以使用ucontext库切换和恢复协程

接口熟悉

typedef struct ucontext_t {
 // 当前上下⽂结束后,下⼀个激活的上下⽂对象的指针,只在当前上下⽂是由makecontext创建时有效
 struct ucontext_t *uc_link;
 // 当前上下⽂的信号屏蔽掩码
 sigset_t uc_sigmask;
 // 当前上下⽂使⽤的栈内存空间,只在当前上下⽂是由makecontext创建时有效
 stack_t uc_stack;
 // 平台相关的上下⽂具体内容,包含寄存器的值
 mcontext_t uc_mcontext;
 ...
} ucontext_t;
// 获取当前的上下⽂
int getcontext(ucontext_t *ucp);
// 恢复ucp指向的上下⽂,这个函数不会返回,⽽是会跳转到ucp上下⽂对应的函数中执⾏,相当于变相调⽤了函数, 但这东西不会在函数结束后跳转设定的uc_link
int setcontext(const ucontext_t *ucp);
// 修改由getcontext获取到的上下⽂指针ucp,将其与⼀个函数func进⾏绑定,⽀持指定func运⾏时的参数,
// 在调⽤makecontext之前,必须⼿动给ucp分配⼀段内存空间,存储在ucp->uc_stack中,这段内存空间将作为func函数运⾏时的栈空间,
// 同时也可以指定ucp->uc_link,表示函数运⾏结束后恢复uc_link指向的上下⽂,
// 如果不赋值uc_link,那func函数结束时必须调⽤setcontext或swapcontext以重新指定⼀个有效的上下⽂,否则程序就跑⻜了
// makecontext执⾏完后,ucp就与函数func绑定了,调⽤setcontext或swapcontext激活ucp时,func就会被运⾏
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
// 恢复ucp指向的上下⽂,同时将当前的上下⽂存储到oucp中,
// swapcontext也不会返回,⽽是会跳转到ucp上下⽂对应的函数中执⾏,相当于调⽤了函数, 在函数返回后, 回自动跳转设定的uc_link的上下文
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

一个简单的函数切换

    我们可以使用ucontext库变相调用函数

#include <iostream>
#include <ucontext.h>
#include <cstdlib>
using namespace std;

ucontext_t _main;
ucontext_t f;

void func() {
    cout << "func" << endl;
}
void FiberCreate(ucontext_t* f, void (*func)(), ucontext_t* next) {
    getcontext(f);
    void* s = malloc(1024 * 1024);  // 分配堆栈
    f->uc_stack.ss_sp = s;
    f->uc_stack.ss_size = 1024 * 1024;
    f->uc_link = next;  // 设置返回上下文为 next
    makecontext(f, func, 0);  // 设定函数
}
int main() {
    // 初始化 main 上下文
    getcontext(&_main);
    //设定返回main函数的上下文
    FiberCreate(&f, func, &_main);
    // 切换到 f
    swapcontext(&_main, &f);
    cout << "Main" << endl;

    free(f.uc_stack.ss_sp);
    return 0;
}

在这里插入图片描述

自动调用

    根据swapcontext会使得函数自动跳转设定的上下文, 我们可以设定一定的调用逻辑, 小心程序跑飞

#include <iostream>
#include <ucontext.h>
#include <cstdlib>
using namespace std;

ucontext_t _main;
ucontext_t f1, f2, f3, f4;

void func001() {
    cout << "func001" << endl;
}
void func002() {
    cout << "func002" << endl;
}
void func003() {
    cout << "func003" << endl;
}
void func004() {
    cout << "func004" << endl;
}
void FiberCreate(ucontext_t* f, void (*func)(), ucontext_t* next) {
    getcontext(f);
    void* s = malloc(1024 * 1024);  // 分配堆栈
    f->uc_stack.ss_sp = s;
    f->uc_stack.ss_size = 1024 * 1024;
    f->uc_link = next;  // 设置返回上下文为 next
    makecontext(f, func, 0);  // 设定函数
}

int main() {
    // 初始化 main 上下文
    getcontext(&_main);
    FiberCreate(&f1, func001, &f2);
    FiberCreate(&f2, func002, &f3);
    FiberCreate(&f3, func003, &f4);
    FiberCreate(&f4, func004, &_main);  // f4 结束后返回 main
    // 切换到 f1
    swapcontext(&_main, &f1);
    cout << "Main" << endl;

    free(f1.uc_stack.ss_sp);
    free(f2.uc_stack.ss_sp);
    free(f3.uc_stack.ss_sp);
    free(f4.uc_stack.ss_sp);
    return 0;
}

设定uc_link后就能来来回回切换
在这里插入图片描述

协程类的实现

    掌握上述基础后我们就可以实现一个简单的协程类了
具体实现介绍

  • 无栈协程: 实现的是无栈协程, 协程的上下文保存在堆上
  • 非对称协程: 实现的是非对称协程, 协程间不能自由切换, 必须和主协程交替切换
  • 使用智能指针管理: 每一个协程对象都只能从堆上new, 继承std::enable_shared_from_this方便获得this的智能指针对象
    完整代码

接口

class Fiber : public std::enable_shared_from_this<Fiber>
{
public:
    using func_t=std::function<void()>;
    using ptr=std::shared_ptr<Fiber>;
    enum class state
    {
        INIT,   //初始化
        HOLD,   //挂起
        EXEC,   //运行
        TERM,   //结束
        READY,  //就绪
    };
public:
    Fiber(func_t cb,bool isrunInSchedule=true,uint32_t stack_size=128*1024);
    ~Fiber();
    //重置协程函数,重置状态INIT/TERM,复用栈空间
    void reset(func_t cb);
    //切换到当前协程运行
    void swapIn();
    //切换到后台运行
    void swapOut();
    //调用协程
    void call();
    //获取协程状态
    state getState(){ return m_s;}
public:
    //设置一个协程为当前协程
    static void SetThis(Fiber*f);
    //获取当前执行协程的智能指针
    static Fiber::ptr Get_this();
    //当前运行协程切换到后台, 并设置为Ready状态
    static void YeildtoReady();
    //当前运行协程切换到后台, 并设置为Hold状态
    static void YeildtoHold();
    //获取总协程数
    static uint64_t GetFiberNum();
    //协程执行的方法, 会在这里面执行回调函数
    static void MainFunc();
private:
    //用于创建主协程
    Fiber();
private:
    uint64_t m_id=0;            //协程id
    uint32_t m_stack_size=0;    //协程栈空间大小
    ucontext_t m_ctx;           //协程上下文
    void* m_stack=nullptr;      //协程使用的栈
    state m_s=state::INIT;      //协程状态
    func_t m_cb;                //协程具体回调
    bool m_runInSchedule;       //是否参与调度器调度
};

全局变量

//协程id
std::atomic<uint64_t> s_fiber_id(0);
//协程数量
std::atomic<uint64_t> s_fiber_num(0);

线程局部变量

    每个线程记录自己的主协程和正在调度的协程, 方便切换

//线程当前执行的协程
thread_local Fiber* t_fiber=nullptr;
//线程的主协程
thread_local Fiber::ptr t_thread_fiber=nullptr;//using Fiber::ptr=std::shared_ptr<Fiber>;

malloc封装

    方便后面扩展内存申请方式或者使用内存池

class MallocStackAllocator
{
public:
static void* Alloc(size_t size) {
    return malloc(size);
}
static void Dealloc(void* vp, size_t size) {
    return free(vp);
}
};

协程切换

//切换到该协程执行
void Fiber::swapIn()
{
    SetThis(this);
    swapcontext(&t_thread_fiber->m_ctx,&m_ctx);
}   
//该协程切换到后台运行
void Fiber::swapOut()
{   
    SetThis(t_thread_fiber.get());
    swapcontext(&m_ctx,&t_thread_fiber->m_ctx);
}       
//手动调用协程, 需要提前自己先切换到主协程
void Fiber::call()
{
    m_s=state::EXEC;
    swapcontext(&t_thread_fiber->m_ctx,&m_ctx);
}
//切换回主协程并设置READY状态
void Fiber::YeildtoReady()
{
    Fiber::ptr p=Get_this();
    p->m_s=state::READY;
    p->swapOut();
}
//切换回主协程并设置HOLD状态
void Fiber::YeildtoHold()
{
    Fiber::ptr p=Get_this();
    p->m_s=state::HOLD;
    p->swapOut();
}

构造函数

Fiber::Fiber(func_t cb,bool isrunInSchedule,uint32_t stack_size):m_id(s_fiber_id++),m_stack_size(stack_size),m_cb(cb),m_runInSchedule(isrunInSchedule)                              
{   
    //协程数量增加
    ++s_fiber_num;
    //分配协程栈空间
    m_stack=StackAllocator::Alloc(m_stack_size);
    //初始化上下文
    getcontext(&m_ctx);
    m_ctx.uc_link=nullptr;
    m_ctx.uc_stack.ss_sp=m_stack;
    m_ctx.uc_stack.ss_size=m_stack_size;
    makecontext(&m_ctx,&Fiber::MainFunc,0);
    LOG_ROOT_INFO("create Fiber id:%d",m_id);//日志
}
//获取当前协程的智能指针
Fiber::ptr Fiber::Get_this()
{
    if(t_fiber!=nullptr)
    {
        //返回当前协程的指针指针对象
        return t_fiber->shared_from_this();
    }
    //如果当前线程没有协程, 就创建一个主协程
    Fiber::ptr main_fiber(new Fiber);
    t_thread_fiber=main_fiber;
    return main_fiber->shared_from_this();
}
//私有构造, 只用于创建主协程, 主协程不需要申请堆空间
Fiber::Fiber()
{
    SetThis(this);
    getcontext(&m_ctx);
    m_s=state::EXEC;
    m_id=s_fiber_id;
    ++s_fiber_num;
    ++s_fiber_id;
    LOG_ROOT_INFO("create main Fiber id:%d",m_id);
}

协程执行的方法

void Fiber::MainFunc()
{
    //获取当前协程的智能指针
    Fiber::ptr p=Get_this();
    //执行回调, 执行完后清理
    p->m_cb();
    p->m_cb=nullptr;
    p->m_s=state::TERM;
    auto cur=p.get();
    //之前获取了当前协程的智能指针, 现在手动让计数器--
    p.reset();
    //切换回主协程
    cur->swapOut();
}

测试

    当然我们的协程总不能自己手动调来调去, 我们还需要协程调度器(关注后面的文章)

协程切换

    看看能不能跑, 先简单使用一下

#include<iostream>
#include"Fiber.hpp"
using namespace MindbniM;
void func1()
{
    std::cout<<"func1"<<std::endl;
}
void func2()
{
    std::cout<<"func2"<<std::endl;
}
void func3()
{
    std::cout<<"func3"<<std::endl;                                                                                                      
}                         
int main()                
{                         
    //首先创建主协程      
    Fiber::Get_this();    
    Fiber::ptr f1=std::make_shared<Fiber>(func1);
    Fiber::ptr f2=std::make_shared<Fiber>(func2);
    Fiber::ptr f3=std::make_shared<Fiber>(func3);
    //切换到f1
    f1->swapIn();
    //切换到f2
    f2->swapIn();
    //切换到f3
    f3->swapIn();
    return 0;
}

三个函数顺利执行
在这里插入图片描述

手动切换+复用

    协程一个优势就能可以复用空间

#include<iostream>
#include"Fiber.hpp"
using namespace MindbniM;
void func1()
{
    std::cout<<"func1"<<std::endl;
}
void func2()
{
    std::cout<<"func2"<<std::endl;
}
void func3()
{
    std::cout<<"func3"<<std::endl;
}
int main()
{
    //首先创建主协程
    Fiber::Get_this();
    Fiber::ptr f1=std::make_shared<Fiber>(func1);
    //试试手动切换
    //切换到f1
    Fiber::SetThis(f1.get());
    //执行f1
    f1->call();
    //切换回主协程
    f1->swapOut();
    //复用f1的空间
    f1->reset(func2);
    f1->swapIn();
    f1->reset(func3);
    f1->swapIn();
    return 0;
}

结果一样

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2192974.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Maven安装使用

说明&#xff1a;Maven是Apache旗下的一个开源项目&#xff0c;是一款用于管理和构建java项目的工具。一般来说&#xff0c;它帮助我们管理依赖、构建项目。本文介绍在Windows系统下安装Maven。 下载&安装&验证 下载 首先&#xff0c;在Maven官网&#xff08;https:…

【第十五周】PyTorch深度学习实践2

目录 摘要Abstract1.多分类问题1.1.Softmax1.2.维度问题1.3.NLLLoss v.s. CrossEntropy1.4.代码实践1.4.1.导入相应的包1.4.2.准备数据集1.4.3.模型设计1.4.4.构造损失和优化器1.4.5.模型训练 2.卷积神经网络基础篇2.1.代码实践2.1.1.导入相应的包&#xff1a;2.1.2.准备数据集…

我谈巴特沃斯滤波器

目录 写在前面的内容我谈巴特沃斯滤波器巴特沃斯滤波器的幅频响应频率变换巴特沃斯各种滤波器例子 写在前面的内容 先看看冈萨雷斯对巴特沃斯滤波器的介绍。 低通 高通 带阻 带通 第一个问题&#xff0c;截止频率处的增益。 0.5的增益是不是陡度小了&#xff1f;巴特沃…

ai智能论文生成系统有用吗?分享5款ai文献综述自动生成器

近年来&#xff0c;人工智能&#xff08;AI&#xff09;技术在学术写作领域的应用越来越广泛&#xff0c;尤其是在文献综述的自动生成方面。AI智能论文生成系统通过深度学习和自然语言处理技术&#xff0c;能够帮助研究人员快速生成高质量的文献综述&#xff0c;从而提高写作效…

深度扩展AntSK,让.NET Aspire助力您的AI项目

引言 在现今飞速发展的技术世界中&#xff0c;引用最新的工具和框架来提升项目的性能和可管理性至关重要。作为一名开发者&#xff0c;我最近在自己的AI知识库项目AntSK中集成了.NET Aspire&#xff0c;这为我的项目注入了新的活力。如果你还不清楚什么是.NET Aspire&#xff0…

[单master节点k8s部署]32.ceph分布式存储(三)

基于ceph rbd生成pv 在集群中认证ceph 用下面代码生成ceph的secret .创建 ceph 的 secret&#xff0c;在 k8s 的控制节点操作&#xff1a; 回到 ceph 管理节点创建 pool 池&#xff1a; [rootmaster1-admin ~]# ceph osd pool create k8stest 56 pool k8stest created [rootm…

BERT论文解读及情感分类实战(论文复现)

BERT论文解读及情感分类实战&#xff08;论文复现&#xff09; 本文所涉及所有资源均在传知代码平台可获取 文章目录 BERT论文解读及情感分类实战&#xff08;论文复现&#xff09;简介BERT文章主要贡献BERT模型架构技术细节任务1 Masked LM&#xff08;MLM&#xff09;任务2 N…

【web安全】——常见框架漏洞

1.ThinkPHP框架漏洞 thinkphp是一个国内轻量级的开发框架&#xff0c;采用phpapache&#xff0c;在更新迭代中&#xff0c;thinkphp也经常爆出各种漏洞&#xff0c;thinkphp一般有thinkphp2、thinkphp3、thinkphp5、thinkphp6版本&#xff0c;前两个版本已经停止更新&#xff…

【详细教程】如何使用YOLOv11进行图像与视频的目标检测

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

m4a怎么转换成mp3?音频转换MP3只需要这6个小工具!

m4a怎么转换成mp3&#xff1f;M4A和MP3是两种常见的音频格式&#xff0c;M4A通常使用AAC&#xff08;高级音频编码&#xff09;进行压缩&#xff0c;提供更高的音质和更小的文件体积&#xff0c;特别适合在Apple设备上使用。而MP3则以其高压缩比和广泛的兼容性著称&#xff0c;…

TM1618数码管控制芯片使用共阳极数码管过程中的问题和解决办法

控制芯片的基本了解 相比于不用控制芯片的电路&#xff1a;这里带2根电源线和3个信号线&#xff0c;共使用了5根线&#xff0c;但可以控制4个8段数码管显示。若是电路直接控制4个8段数码管需要84113个接口&#xff0c;这对于MCU的珍贵引脚简直是浪费。 这里不会出现余晖效应也…

大花蔷薇T2T基因组-60

Multi-omics analyzes of Rosa gigantea illuminate tea scent biosynthesis and release mechanisms 多组学分析揭示了大花蔷薇茶香合成及释放机制 摘要 玫瑰是一种全球广泛栽培的重要观赏作物&#xff0c;用于香水生产。然而&#xff0c;由于缺乏茶玫瑰的参考基因组&#x…

鸿蒙开发(NEXT/API 12)【穿戴设备传感器获取】手机侧应用开发

手机侧应用可以通过Wear Engine获取穿戴设备上的传感器信息&#xff0c;并通过打开、关闭命令控制获取传感器数据。 使用传感器相关接口前&#xff0c;需要向手机侧用户申请获取对应权限的授权 传感器类型申请权限ECG、PPG、HR[HEALTH_SENSOR]人体传感器ACC、GYRO、MAG[MOTIO…

汇编DEBUG程序调用

工具 系统&#xff1a;Windows 11 应用&#xff1a;DOSBox 0.74-3 下载安装教程&#xff1a;本人写的《DOSBox下载安装&#xff08;Windows系统 DOSBox 0.74-3&#xff09;》 https://blog.csdn.net/just_do_it_sq/article/details/142715182?spm1001.2014.3001.5501 相关文…

C++ 算法学习——1.3 深度优先搜索

深度优先搜索&#xff1a;简单讲就是搜到某条路尽头&#xff0c;再掉头回溯搜其他的路。此中重点是尽头的判断&#xff0c;和对走过的路进行标记。 一般采用递归的写法&#xff0c;模板大致如下&#xff1a; DFS(node,visited):if node is in visited:returnadd node to visi…

通用mybatis-plus查询封装(QueryGenerator)

结果如下图所示 java类代码分别如下 1 package com.hdx.contractor.util.mybatis;import com.hdx.contractor.common.user.SecurityUser; import com.hdx.contractor.common.user.UserDetail; import com.hdx.contractor.util.query.oConvertUtils; import lombok.extern.slf…

OpenHarmony(鸿蒙南向开发)——轻量系统芯片移植案例(一)

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 轻量带屏解决方案之恒玄芯片移植案例 本文章基于恒玄科技BES2600W…

【AI知识点】交叉验证(Cross-Validation)

交叉验证&#xff08;Cross-Validation&#xff09; 是机器学习中常用的一种模型评估方法&#xff0c;用于评估模型的性能和泛化能力。它通过在不同的训练集和验证集上多次训练和测试模型&#xff0c;从而有效地评估模型在未见数据上的表现&#xff0c;帮助防止模型的过拟合和欠…

【RTD MCAL 篇2】 K312 UART DMA

【RTD MCAL 篇2】 K312 UART DMA 一&#xff0c;文档简介二&#xff0c; 功能实现2.1 K312 MINIEVB硬件配置2.2 EB 配置2.2.1 Mcl module2.2.2 Mcu module2.2.3 Platform module2.2.4 Port module2.2.5 Uart module2.2.6 Rm module 2.3 main code 三&#xff0c;测试结果 一&am…

Clio——麻省理工学院增强机器人场景理解算法

概述 机器人感知长期以来一直受到现实世界环境复杂性的挑战&#xff0c;通常需要固定设置和预定义对象。麻省理工学院的工程师 已经开发了Clio这项突破性的系统可以让机器人直观地理解并优先考虑周围环境中的相关元素&#xff0c;从而提高其高效执行任务的能力。 了解对更智…