C++协程项目之协程库学习与实践(协程函数学习、线程切换实践)

server/2024/9/24 12:28:37/

网上协程实现原理大概有这么几种:调库、汇编、原语级别(可能会破坏原本语义)。我们今天简单学习和实践的是一种利用linux下库函数实现的协程。

首先来看这样一段代码:

#include <iostream>
#include <ucontext.h>
#include <unistd.h>int main(int argc, char** argv) {ucontext_t context;getcontext(&context);std::cout << "hello world" << std::endl;sleep(1);setcontext(&context);return 0;
}

编译运行指令如下:

g++ new.cpp -o new

./new

注意这个库只有在linux下才支持,可以用虚拟机进行尝试。

头文件unistd.h是unix std标准命名空间的缩写,我们用到了sleep休眠函数,所以要包含这个头文件。

运行效果如图,如果不用Ctrl+C或者Ctrl+Z终止进程的话,就会一直运行。这是怎么回事呢?

其实是我们用getcontext函数保存了context地址处的上下文信息,具体就是这个结构体保存的信息,这个解释起来有些复杂,最简单的解释就是这个结构体包含一个该结构体指针类型的变量,和众多的上下文信息,包括上下文中的阻塞信号,使用的内存栈的地址、寄存器名字等等。我们这里用到的是恢复到原先设置好的栈位置上去,让其达到了循环运行的效果。

这也就是协程的奇妙之处了。

ucontext.h库中还包含了这样几个函数:

int getcontext(ucontext_t *uc);int setcontext(ucontext_t *uc);void makecontext(ucontext_t *uc, void (*func)(), int argc, char** argv);int swapcontext(ucontext_t *tuc, ucontext_h *nuc); 

其中的参数名我按照自己的风格改写了下,第四个函数第一个参数是保存地址,第二个参数是唤醒的协程的上下文地址。

第三个函数是将指定上下文中栈地址处的值设置为该函数和其参数,方便直接执行该函数。

第一第二个上面已经使用过了。就不看了。

需要注意的是三个有返回值的函数在失败时返回-1。成功时getcontext返回0,其他两个不返回。

下面我们尝试用这几个函数实现两个线程之间的切换。

需求如下:

#include <iostream>
#include <ucontext.h>ucontext_t main_context;
ucontext_t thread_context;void thread1() {int n = 3;while (n--)std::cout << "I'm thread1" << std::endl;setcontext(&main_context);
}void thread2() {int n = 4;while (n--)std::cout << "I'm thread2" << std::endl;setcontext(&main_context);
}void thread3() {int n = 5;while (1)std::cout << "I'm threa3" << std::endl;setcontext(&main_context);
}int main(int argc, char** argv) {makecontext(&thread_context, &thread1, 0);while (swapcontext(&main_context, &thread_context) == -1);makecontext(&thread_context, &thread2, 0);while (swapcontext(&main_context, &thread_context) == -1);makecontext(&thread_context, &thread3, 0);while (swapcontext(&main_context, &thread_context) == -1);return 0;
}

效果如下:

简化后代码如下:

#include <iostream>
#include <ucontext.h>void thread1() {int n = 3;while (n--)std::cout << "I'm thread1" << std::endl;
}void thread2() {int n = 4;while (n--)std::cout << "I'm thread2" << std::endl;
}void thread3() {int n = 5;while (n--)std::cout << "I'm threa3" << std::endl;
}int main(int argc, char** argv) {ucontext_t main_context;ucontext_t thread_context;char stack[1024*128];getcontext(&thread_context);thread_context.uc_stack.ss_sp = stack;thread_context.uc_stack.ss_size = sizeof(stack);thread_context.uc_stack.ss_flags = 0;thread_context.uc_link = &main_context;makecontext(&thread_context, (void (*)())thread1, 0);swapcontext(&main_context, &thread_context);makecontext(&thread_context, (void (*)())thread2, 0); swapcontext(&main_context, &thread_context);makecontext(&thread_context, (void (*)())thread3, 0);swapcontext(&main_context, &thread_context);return 0;
}

实现的思路如下:

定义两个ucontext_t变量,main_context用来存放main函数上下文,thread_context用来存放线程上下文。首先要用getcontext取当前上下文到thread_context,方便我们后续对它进行操作,注意,不先取当前上下文而是用其他方式进行初始化,包括用new,都会引发段错误!这背后的原因大概是ucontext_t本身是个结构体,没有提供足够安全的构造函数,所以我们要用现成的main函数提供的上下文来改造。同样会引发段错误的还有不给栈空间赋值,这里用char的原因在于ucontext_t本身的大小我们并不确定,为了避免bus error,我们选择了每个元素只有一个字节的char数组。另外,char数组也不能用new,否则就不是栈空间了。而且指定栈空间之后,也是一定要指定大小,否则也会引发段错误。ss_flags倒是无所谓,是个指定栈增长方向的,默认情况下是不会报错的。。

上面主要是会引发段错误的一些情况,下面简要说下思路。将thread_context的后继(这里使用其内部指针元素uc_link实现)设置为main_context,这样每次该线程执行完之后都会返回main函数中,然后简单调用makecontext函数将对应函数设定在thread_context处就可以正常使用了,线程运行结束后会自动返回到main中。这里其实也是一开始就要将当前上下文保存的原因了——保证栈空间在main函数开始栈空间之后,使用main函数的栈空间中的资源。因为原先协程就是在线程内运行的,原本线程切换我们用协程来切换,但一个线程还是对应一个协程,这是栈空间上的对应。


http://www.ppmy.cn/server/37250.html

相关文章

Qt | QComboBox(组合框)

01、上节回顾 Qt 基础教程合集02、QComBox 一、QComboBox 类(下拉列表、组合框) 1、QComboBox 类是 QWidget 类的直接子类,该类实现了一个组合框 2、QComboBox 类中的属性 ①、count:const int 访问函数:int count() const; 获取组合框中的项目数量,默认情况下,对于空…

利用Jenkins完成Android项目打包

问题和思路 目前存在的问题 打包操作由开发人员完成&#xff0c;这样开发进度容易被打断。 解决问题的思路 将打包操作交测试/产品/开发人员来完成&#xff0c;主要是测试/开发。 按照以上的思路&#xff0c;那么JenkinsGradle的解决方案是比较经济的&#xff0c;实现起来…

RTSP和RTP/UDP有什么区别

RTSP&#xff08;Real Time Streaming Protocol&#xff09;和RTP&#xff08;Real-time Transport Protocol&#xff09;结合UDP&#xff08;User Datagram Protocol&#xff09;在流媒体传输中各自扮演着不同的角色&#xff0c;并存在显著的差异。以下是它们之间的主要区别&a…

Linux 文件管理命令Lawk wc comm join fmt

文章目录 2.Linux 文件管理命令2.44 awk&#xff1a;模式匹配语言1&#xff0e;变量2&#xff0e;运算符3&#xff0e;awk 的正则4&#xff0e;字符串函数5&#xff0e;数学函数案例练习 2.45 wc&#xff1a;输出文件中的行数、单词数、字节数案例练习2.46 comm&#xff1a;比较…

C++笔试强训day12

目录 1.删除公共字符 2.两个链表的第一个公共结点 3.mari和shiny 1.删除公共字符 链接https://www.nowcoder.com/practice/f0db4c36573d459cae44ac90b90c6212?tpId182&tqId34789&ru/exam/oj 暴力枚举就行&#xff0c;也可以用哈希表&#xff0c;哈希更省一点时间 …

6.Docker端口映射与容器互联

文章目录 端口映射与容器互联1、端口映射实现容器访问1.1、从外部访问容器应用1.2 映射所有接口的地址1.3 映射到指定地址的指定端口1.4 映射到指定地址的任意端口1.5 查看映射端口配置 2、互联机制实现容器互访2.1、自定义容器名称2.2、容器互联 端口映射与容器互联 在生产实…

探索Linux中的VI编辑器:全方位命令详解与实战应用

探索Linux中的VI编辑器&#xff1a;全方位命令详解与实战应用 引言一、VI编辑器的三种模式二、VI中的翻页操作三、搜索与替换功能四、退出VI编辑器应用场景举例&#xff1a;总结 引言 VI编辑器作为Linux世界中的基石工具之一&#xff0c;其简洁高效的设计理念贯穿了多种操作系统…

质因数分解(cpp实现)--一种快速求得一个数有多少个因子的黑魔法

前言 最近机试没少吃不会质因数分解的亏&#xff0c;用传统的求得因子个数只能过一点点…(ex, 20%) 质因数分解后&#xff0c;可以将因子问题转化为 集合的组合问题&#xff0c;因此会很快&#xff0c;目测是 l o g n log n logn (n是该整数的值)。 传统解法 假设输入整数的…