要想了解协程,最好先搞清楚进程,线程,这样才能将三者区分开来!
进程 | 线程 | 协程 | |
---|---|---|---|
切换者 | 操作系统 | 操作系统 | 用户(编程者) |
切换时机 | 根据操作系统自己的切换策略,用户不感知 | 根据操作系统自己的切换策略,用户不感知 | 用户(编程者)自己决定 |
切换内容 | 页全局目录、内核栈、硬件上下文 | 内核栈、硬件上下文 | 硬件上下文 |
切换内容的保存 | 保存于内核栈中 | 保存于内核栈中 | 保存于用户栈中 |
切换过程 | 用户态-内核态-用户态 | 用户态-内核态-用户态 | 用户态(没有陷入内核态) |
协程优点:
协程缺点:
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体 有自己独立的地址空间,有自己的堆 ,上级挂靠单位是操作系统。 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位 。
【进程间通信(IPC)】:
线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。
假如我们把整条道路看成是一个 “进程” 的话,那么由白色虚线分隔开来的各个车道就是进程中的各个 “线程” 了。
这些 线程(车道) 共享了 进程(道路) 的公共资源(土地资源)。
这些 线程(车道) 必须依赖于 进程(道路) ,也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。
这些 线程(车道) 之间可以 并发执行(各个车道你走你的,我走我的) ,也可以 互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕) 。
这些 线程(车道) 之间依靠 代码逻辑(交通灯) 来控制运行,一旦 代码逻辑控制有误(死锁,多个线程同时竞争唯一资源) ,那么线程将陷入混乱,无序之中。
亲缘性的意思是进程/线程只在某个 CPU 上运行(多核系统),比如:
BOOL WINAPI SetProcessAffinityMask(
_In_ HANDLE hProcess,
_In_ DWORD_PTR dwProcessAffinityMask
);
/*
dwProcessAffinityMask 如果是 0 , 代表当前进程只在 cpu0 上工作;
如果是 0x03 , 转为2进制是 00000011 . 代表只在 cpu0 或 cpu1 上工作;
*/
使用CPU亲缘性的好处:
设置CPU亲缘性是为了防止进程/线程在CPU的核上频繁切换 ,从而 避免因切换带来的CPU的L1/L2 cache失效 ,cache 失效会降低程序的性能。
概要速览:
协程(coroutine)是一种在程序设计中用于实现多任务并发执行的技术。它允许在单个线程内进行多个任务之间的切换,这种切换是协作式的,即任务主动让出控制权,而不是抢占式的。协程的概念最早可以追溯到1958年,由马尔文·康威(Marvin Conway)提出,并在1963年首次公开发表。
在程序中,协程通常通过函数或方法的暂停和恢复来实现。一个协程函数可以在执行到某个点时暂停,将控制权让给其他的协程函数,并在适当的时候恢复执行。这种机制使得一个线程可以在等待某些操作(如I/O)完成时切换到其他任务,从而提高资源的利用率。
协程的实现通常依赖于事件循环(event loop)或类似的机制,用于调度和管理协程的执行。在许多编程语言中,协程已经成为并发编程的标准库部分,如 Python 中的 asyncio 模块、Go 语言中的协程以及 Kotlin 中的协程支持。
协程的使用场景非常广泛,包括但不限于:
- 异步 I/O 操作:在网络请求或文件操作中,协程可以在等待 I/O 完成时执行其他任务。
- 并发任务调度:在图形用户界面(GUI)应用程序中,协程可以实现平滑的用户界面响应,同时处理后台任务。
- 协程异常处理:协程支持异常的传播和捕获,允许在协程结构中传播异常信息。
协程的一个重要特性是它们可以携带状态,这意味着当协程被挂起时,它当前的状态(包括局部变量和程序计数器等)会被保存,并在恢复执行时恢复到挂起前的状态。
在不同的编程语言和平台中,协程的实现和调度机制可能有所不同,但其核心概念和目的是相似的:通过协作式的多任务并发执行,提高程序的性能和响应能力。
协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
子程序,或者称为函数,在所有语言中都是层级调用,比如 A 调用 B,B 在执行过程中又调用了 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。
def A():
print '1'
print '2'
print '3'
def B():
print 'x'
print 'y'
print 'z'
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:1 2 x y 3 z
。
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
所谓的有栈,无栈并不是说这个协程运行的时候有没有栈,而是说协程之间是否存在调用栈(callbackStack)。
协程可分为两种:
无栈协程和线程的区别:
C++20 协程中采纳的是微软提出并主导(源于C#)的无栈协程。
C++世界演化的主旋律:异步化和并行化,而 C++20 协程能够以同步原语写异步代码的特性,使其成为编写异步代码的好工具。
具体可以看这篇文章:https://blog.csdn.net/weixin_43705457/article/details/106924435
有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。
有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是 C++20 最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑。
栈空间的限制:
有栈协程的“栈”空间普遍是比较小的,在使用中有栈溢出的风险;而如果让“栈”空间变得很大,对内存空间又是很大的浪费。无栈协程则没有这些限制,既没有溢出的风险,也无需担心内存利用率的问题。
性能:
有栈协程在切换时确实比系统线程要轻量,但是和无栈协程相比仍然是偏重的,这一点虽然在我们目前的实际使用中影响没有那么大(异步系统的使用通常伴随了 IO,相比于切换开销多了几个数量级),但也决定了无栈协程可以用在一些更有意思的场景上。
无栈协程是一个可以暂停和恢复的函数,是函数调用的泛化。
一个函数的函数体(function body)是顺序执行的,执行完之后将结果返回给调用者,我们没办法挂起它并稍后恢复它,只能等待它结束。而无栈协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行。
所以,从这个角度来说,无栈协程是普通函数的泛化。
C++20 提供了三个新关键字(co_await、co_yield 和 co_return),如果一个函数中存在这三个关键字之一,那么它就是一个协程。
当 caller 调用一个协程的时候会先创建一个协程帧,协程帧会构建 promise 对象,再通过 promise 对象产生 return object。
协程帧中主要有这些内容:
协程参数
局部变量
promise 对象
promise_type 是 promise 对象的类型。promise_type 用于定义一类协程的行为,包括协程创建方式、协程初始化完成和结束时的行为、发生异常时的行为、如何生成 awaiter 的行为以及 co_return 的行为等等。promise 对象可以用于记录/存储一个协程实例的状态。每个协程桢与每个 promise 对象以及每个协程实例是一一对应的。
它是 promise.get_return_object() 方法创建的,一种常见的实现手法会将 coroutine_handle 存储到 coroutine object 内,使得该 return object 获得访问协程的能力。
协程帧的句柄,主要用于访问底层的协程帧、恢复协程和释放协程帧。
程序员可通过调用 std::coroutine_handle::resume() 唤醒协程。
co_await:一元操作符;
awaitable:支持 co_await 操作符的类型;
awaiter:定义了 await_ready、await_suspend 和 await_resume 方法的类型。
co_await expr 通常用于表示等待一个任务(可能是 lazy 的,也可能不是)完成。co_await expr 时,expr 的类型需要是一个 awaitable,而该 co_await 表达式的具体语义取决于根据该 awaitable 生成的 awaiter。
看起来和协程相关的对象还不少,这正是协程复杂又灵活的地方,可以借助这些对象来实现对协程的完全控制,实现任何想法。但是,需要先要了解这些对象是如何协作的,把这个搞清楚了,协程的原理就掌握了,写协程应用也会游刃有余了。
https://hkrb7870j3.feishu.cn/docx/RUMldRX8koIgxIxvAmccy4PYnCc
利用操作系统提供的接口来实现协程:
typedef struct ucontext {
struct ucontext *uc_link;
sigset_t uc_sigmask;
stack_t uc_stack;
mcontext_t uc_mcontext;
unsigned long int uc_flags;
} ucontext_t;
具体 ucontext 函数族可以看这篇文章:
https://blog.csdn.net/qq_44443986/article/details/117739157
示例一:
#include
#include
#include
int main() {
int i = 0;
ucontext_t ctx; //定义上下文结构体变量
getcontext(&ctx); //获取当前上下文
printf("i = %d\n", i++);
sleep(1);
setcontext(&ctx); //恢复上下文
return 0;
}
示例二:
#include
#include
#include
void foo() {
printf("this is child context\n");
}
int main() {
char stack[1024 * 64] = {0};
ucontext_t child, main;
// 获取当前上下文
getcontext(&child);
// 分配栈空间, uc_stack.ss_sp 指向栈顶
child.uc_stack.ss_sp = stack;
child.uc_stack.ss_size = sizeof(stack);
child.uc_stack.ss_flags = 0;
// 指定后续的上下文
child.uc_link = &main;
//切换到child上下文,保存当前上下文到main
makecontext(&child, (void (*)(void))foo, 0);
// 如果设置了后继上下文,foo函数执行结束会返回此处 如果设置为NULL,就不会执行这一步
swapcontext(&main, &child);
printf("this is main context\n");
return 0;
}
协程的方法:
class Routine {
friend class Schedule;
public:
enum Status {
RT_READY = 0,
RT_RUNNING,
RT_SUSPEND
};
Routine();
virtual ~Routine();
virtual void run() = 0;
void resume();
void yield();
int status();
protected:
static void func(void * ptr);
private:
ucontext_t m_ctx;
int m_status;
char * m_stack;
int m_stack_size;
Schedule * m_s;
};
调度器的方法:
class Schedule {
friend class Routine;
public:
Schedule();
~Schedule();
void create(int stack_size = 1024 * 1024);
void append(Routine * c);
bool empty() const;
int size() const;
void resume();
private:
ucontext_t m_main;
char * m_stack;
int m_stack_size;
std::list m_queue;
};
定义一个 A 线程:a_routine.h
#pragma once
#include
#include
using namespace yazi::routine;
class ARoutine : public Routine {
public:
ARoutine(int * num) : Routine(), m_num(num) {}
~ARoutine() {
if (m_num != nullptr) {
delete m_num;
m_num = nullptr;
}
}
virtual void run() {
for (int i = 0; i < *m_num; i++) {
std::cout << "a run: num=" << i << std::endl;
yield();
}
}
private:
int * m_num;
};
main.cpp
#include
#include
#include
#include
using namespace yazi::routine;
int main() {
Schedule s;
s.create(1024 * 16);
Routine * a = new ARoutine(5);
s.append(a);
Routine * b = new BRoutine(10);
s.append(b);
s.run();
return 0;
}
视频讲解:【c++20协程太难用了,我还是自己设计一个吧】 https://www.bilibili.com/video/BV1SR4y1Y7tb/?share_source=copy_web
https://blog.csdn.net/sanmi8276/article/details/111375619
https://blog.csdn.net/weixin_43705457/article/details/106924435
https://hkrb7870j3.feishu.cn/docx/RUMldRX8koIgxIxvAmccy4PYnCc
https://blog.csdn.net/qq_44443986/article/details/117739157
https://blog.csdn.net/tennysonsky/article/details/46046317
https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013868328689835ecd883d910145dfa8227b539725e5ed000
https://www.cnblogs.com/work115/p/5620272.html
https://blog.csdn.net/liu251890347/article/details/38509943
https://www.cnblogs.com/fah936861121/articles/8043187.html
http://blog.chinaunix.net/uid-25601623-id-5095687.html
http://t.csdnimg.cn/RolRj
http://t.csdnimg.cn/s6wn0
https://blog.csdn.net/tennysonsky/article/details/46046317