概念
1、进程
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
- 进程和进程之间占有独立的内存空间,不共享数据,不同进程之间可以通过FIFO,PIPE等共享数据。
2、线程
- 线程是进程的一个执行单元,是CPU调度和分配的基本单位。
- 线程不具有独立的运行资源,进程内的不同线程共享进程分配到的内存空间。
- 线程和线程之间通过内存共享共享数据,可以较为快速的进行context切换。资源开销较小。
3、协程
- 协程是用户态轻量级线程。虽然叫用户态线程,但是协程的create并不需要调用系统资源,而是由语言内部进行实现的。所以协程可以大量创建。
- 协程是跑在线程上的线程片,并且可以人为的选择创建,销毁,切换,挂起,恢复。
- 协程和线程之间最大的不同在于,对于多核CPU,线程可以并行执行程序,而协程是跑在线程上的线程片,只能并发。
- 协程在并发上的修饰在于,创建,切换所消耗的资源远远小于线程创建和切换的开销。所以,对于IO密集型场景,协程就极为匹配。
三者之间的关系,可以用图片表示
还有就是有很多小伙伴会问,协程既然这么好用,其使用场景是什么。网上有很多讲协程的文章,但是的确没有怎么说,协程的使用场景。现在很多互联网大厂都在致力于做自己的编译器和脚本运行工具,当脚本语言在运行到异步逻辑的时候,如果都通过线程和加锁来实现,无疑,这样,脚本的运行效率会相当低下。这时,如果用协程来实现脚本运行阶段的异步逻辑,会大大加速脚本的运行速度。
C++实现
c++ 实现协程分系统不同,有不同实现。
1、windows系统
windows系统中,微软针对coroutine提供了自己的一套API,起了一个名字叫Fiber(纤程),这组API智能在windows平台进行使用,无法跨平台。Fiber的两个特点
- 纤程拥有独立的栈空间和寄存器环境;
- 纤程在用户态实现调调度,也就是说完全由程序员控制;
接下来,来了解一下windows系统下如何使用这套API
- 线程转换为纤程
LPVOID ConvertThreadToFiberEx(
[in, optional] LPVOID lpParameter,//指针类型数据传入,可以通过GetFiberData索回数据
[in] DWORD dwFlags /*可以填0和 FIBER_FLAG_FLOAT_SWITCH 两种状态。
0的话,因为x86系统的CPU的浮点数状态信息默认情况下在纤程看来不属于CPU寄存器,浮点运算会导致错误。
FIBER_FLAG_FLOAT_SWITCH可以解决这个问题*/
);
此接口是为了让开发者能够使用Fiber调度所进行操作。要想使用Fiber,就要把当前线程变成Fiber。需要注意的是此接口只在windowsXP之上的系统可以使用,而windowsXP系统,使用如下接口。
LPVOID ConvertThreadToFiber(
[in, optional] LPVOID lpParameter
);
ConvertThreadToFiber(Ex)返回Fiber的运行地址,之后就可以使用这个地址。
- 创建纤程
我们将线程转换成Fiber是为了有多个Fiber能够并发,这就需要我们进行Fiber的创建。
LPVOID CreateFiberEx(
[in] SIZE_T dwStackCommitSize, //堆栈的初始化大小,默认为1MB(默认填0)
[in] SIZE_T dwStackReserveSize,//预留的虚拟内存,默认为1MB(默认填0)
[in] DWORD dwFlags,
[in] LPFIBER_START_ROUTINE lpStartAddress, // Fiber运行函数指针
[in, optional] LPVOID lpParameter
);
同样,windowsXP版本之上才能够使用,XP版本使用
LPVOID CreateFiber(
[in] SIZE_T dwStackSize,
[in] LPFIBER_START_ROUTINE lpStartAddress,
[in, optional] LPVOID lpParameter
);
像ConvertThreadToFiber(Ex)函数一样,CreateFiber(Ex)也返回纤程执行环境的内存地址,这个内存地址就像句柄一样,直接标识着一个纤程。
- 切换纤程
创建纤程后,系统不会像开辟Thread时,自动运行,而是需要程序中由程序员决定何时运行。这就需要用到纤程的切换
void SwitchToFiber(
[in] LPVOID lpFiber
);
lpFiber就是ConvertThreadToFiber(Ex)和CreateFiber(Ex)的返回值。
- 删除纤程
当一个纤程运行完毕之后,需要删除,调用删除纤程的接口
void DeleteFiber(
[in] LPVOID lpFiber
);
2、Linux系统
前文提到,Fiber只能在windows系统中使用,在linux上,需要用到ucontext。
the ucontext_t type is a structure type suitable for holding the context for the user thread of execution. A thread’s context include stack,saved registersm a list of block signals
这需要引入头文件
<ucontext.h>
其中包含了ucontext_t和配套的四个函数
#include <ucontext.h>
typedef struct ucontext_t {
struct ucontext_t* uc_link;
stack_t uc_stack;
__sigset_t uc_sigmask;
mcontext_t uc_mcontext;
...
};
- uc_link 后续执行的context,若uc_link为空,当前context执行完毕后,退出程序
- uc_stack 当前context的栈
- uc_sigmask 当前context需要屏蔽的信号集合
- uc_mcontext 保存上下文的特定机器, 包括调用线程的特定寄存器等等
#include <ucontext.h>
int getcontext(ucontext_t* ucp);
void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t* olducp, ucontext_t* newucp);
int setcontext(const ucontext_t* ucp);
- getcontext
创建一个ucontext_t对象,进行执行空间分配
创建方式:
(1) getcontext,
(2) 指定分配给上下文的栈uc_stack.ss_sp,
(3) 指定这块栈的大小uc_stack.ss_size,
(4) 指定uc_stack.ss_flags,
(5) 指定后继上下文uc_link
ucontext_t ucp;
getcontext(&ucp);
ucp.uc_stack.ss_sp = new char[100];
ucp.uc_stack.ss_size = new char[100];
ucp.uc_stack.ss_flags = 0;
协程运行时使用主协程划分的栈空间,而协程切回主线程时需要将该部分栈空间的内容copy到每个协程各自的一个空间缓存起来,因为主协程中划分的栈空间并不是只用于一个协程,而是会用于多个协程
- makecontext
makecontext可以修改getcontext初始化过的context,func参数指明了该context的入口函数,argc是入参个数
- swapcontext
切换context,保存当前context并切换到新的context运行
- setcontext
将当前程序切换到新的context,在执行正确的情况下该函数直接切换到新的执行状态,不会返回。
代码实现
思路
- 协程调度类,可以当做协程池,用来管理用户协程对象
- 用户协程类,用户协程类负责实际的协程接口实现,以及任务相关的部分
- 任务类,用来执行具体业务
代码结构
- corountine.h 和 coroutine.cpp 是用户携程类
- coroutine_pool.h 和 coroutine_pool.h 是协程调度类,也就是协程池
- coroutine_task.h 和 count.h 分别是任务类基类以及具体实现数数的任务类
- coroutine_common.h 和 type.h 是自定义资源
代码具体实现在附录
附录
- coroutine.h
#pragma once
#ifndef __Coroutine_H__
#define __Coroutine_H__
#include "type.h"
#include "coroutine_common.h"
#include "coroutine_task.h"
class CoroutinePool;
class Coroutine
{
public:
Coroutine(CoroutineId id, std::shared_ptr<CoroutinePool> coroutinePool);
virtual ~Coroutine();
public:
error_t initialize();
void unInitialize();
error_t setStatus(const CoroutineStatusEnum& status);
CoroutineStatusEnum getStatus() const;
void setResumeReason(const ResumeReason& reason);
ResumeReason getResumeReason() const;
CoroutineId getCoroutineId() const;
error_t setCoroutineTask(const CoroutineTaskPtr& task);
CoroutineContext getContext() const;
static void coroutineEntry(void* lpParameter);
void resetContext();
private:
error_t getCoroutinePool(OUT std::shared_ptr<CoroutinePool>& coroutinePool);
CoroutineContext getMainContext() const;
void run();
private:
std::weak_ptr<CoroutinePool> m_coroutinePool;
CoroutineId m_coroutineId;
CoroutineTaskPtr m_coroutineTask;
CoroutineStatusEnum m_coroutineStatus;
CoroutineContext m_context;
ResumeReason m_resumeReason;
};
using CoroutinePtr = std::shared_ptr<Coroutine>;
#endif __Coroutine_H__
- couroutine.cpp
#include "corountine.h"
#include "coroutine_task.h"
#include "coroutine_pool.h"
Coroutine::Coroutine(CoroutineId id, std::shared_ptr<CoroutinePool> coroutinePool) :
m_coroutinePool(coroutinePool),
m_coroutineId(id),
m_coroutineTask(nullptr),
m_coroutineStatus(CoroutineStatusEnum::COROUTINE_STATUS_UNKNOWN),
m_context(nullptr),
m_resumeReason(ResumeReason::REASON_NORMAL)
{
}
Coroutine::~Coroutine()
{
unInitialize();
}
error_t Coroutine::initialize()
{