这就是协程

什么是协程?

简单来说,协程是一个可以暂停和恢复执行的函数。

普通函数是线程相关的,而协程不依赖特定线程。

协程是一种用户态的轻量级线程,它的调度完全由用户程序控制,而不是像线程那样依赖操作系统内核调度。

线程相关是指:普通函数执行时所使用的栈空间等资源,是属于调用它的线程的。比如,当线程被阻塞时,在该线程中执行的普通函数也会暂停执行。而且不同线程同时调用同一个普通函数,由于各自拥有独立的栈空间和执行上下文,函数内的局部变量等状态在不同线程中是相互隔离的。

有栈与无栈协程

有栈协程:用独立的执行栈来保存协程的上下文。当协程被挂起时,栈协程会保存当前执行状态(函数调用栈,局部变量等),并将控制权交换给调度器,当协程被恢复时,栈协程会将之前保存的执行状态恢复,从上次挂起的地方继续执行。类似于内核态线程的实现,不同协程间切换还是要切换对应的栈上下文,只是不用陷入内核而已。

无栈协程:它不需要独立的执行栈来保存协程的上下文信息,协程的上下文信息都放到公共内存中,当协程被挂起时,无栈协程会将协程的状态保存在堆上的数据结构中,并将控制权交换给调度器,当协程被回复时,无栈协程会将之前保存的信息从堆区中取出,并从上次挂起的地方继续执行。协程切换时,使用状态机来切换,就不用切换对应的上下文了,因为都在堆里,比有栈协程要轻量许多。

对称/非对称协程
 

对称协程:所有协程是平等的,任何协程可以直接将控制权转移给其他协程,无需通过中心调度点

  • 对等性:协程之间是平等的,没有明确的调用者和被调用者关系
  • 自由切换:协程A可以主动跳转到协程B,协程B 也可以直接跳转到协程A或者其他协程
  • 无主从关系:没有主协程概念,调度逻辑由协程自己决定

非对称协程:协程之间存在调用层级关系,控制权必须通过特定的调用点(yield/resume)转移。

  • 主从关系:存在一个调用者(主线程或者调度器),协程只能将控制权返回给调用者
  • 受限切换:协程通过yield挂起自身并将控制权交换给调用者,调用者通过resume恢复协程
     

独立栈和共享栈

独立栈和共享栈都是有栈协议

共享栈的本质是所有的协程在运行的时候都使用同一个栈空间,每次协程切换时都要把自身用的共享栈空间拷贝,对协程调用yield的时候,该协程栈内容暂时保存起来,保存的时候需要用到多少内存就开辟多少,这样就减少了内存的浪费,resume该协程的时候,协程之前保存的栈内容,会重新拷贝到运行栈中。

独立栈,也就是每个协程栈空间都是独立的,固定大小。好处是协程切换时,内存不用来回拷贝。坏处是内存空间浪费。因为栈空间在运行时不能随时扩容,所以每个协程都要预先开辟一个足够的栈空间使用。

协程类的实现

首先先了解一下Linux下的`ucontext`族函数,`ucontext`机制是C库提供的一组创建,保存,切换用户态执行上下文的API,这是协程能够随时切换和恢复的关键。

上下文结构体:

typedef struct ucontext {
    struct ucontext *uc_link;    // 上下文结束后切换到的下一个上下文
    sigset_t uc_sigmask;         // 该上下文中阻塞的信号集
    stack_t uc_stack;            // 该上下文使用的栈信息(栈基地址、大小等)
    mcontext_t uc_mcontext;      // 机器相关的上下文(寄存器、程序计数器等)
    ...                          // 其他系统相关字段
} ucontext_t;

核心函数:

1. int getcontext(ucontext_t*ucp):获取当前上下文,保存在ucp中,(0/-1)
2. int setcontext(const ucontext_t*ucp):恢复ucp指向的上下文,即切换到该上下文继续执行 (失败 -1)
3. void makecontext(ucontext_t*ucp,void(*func)(void),int argc,...):配置一个上下文,让这个上下文未来被激活时执行指定函数。
4. int swapcontext(ucontext_t*oucp,const ucontext_t*ucp):保存当前上下文到oucp,同时切换(激活)到ucp上下文。

我们采用非对称的独立栈协程设计

我们都知道进程有就绪、阻塞、执行三种状态,要实现协程也要考虑协程的状态,这里我们尽可能简化,只设置三种协程状态:就绪、运行、结束

// 协程状态
	enum State
	{
		READY, 
		RUNNING, 
		TERM 
	};

对于非对称协程来说,协程除了创建语句外,只有两种操作,一种是resume表示恢复协程运行,另一种是yield表示让出执行。协程的结束没有专门的操作,协程函数运行结束时即协程结束,协程结束其前会自动调用一次yield以返回主协程。

在实现协程时要给协程绑定一个运行函数,除此之外还要给协程一个运行的栈空间

private:
	// id
	uint64_t m_id = 0;
	// 栈大小
	uint32_t m_stacksize = 0;
	// 协程状态
	State m_state = READY;
	// 协程上下文
	ucontext_t m_ctx;
	// 协程栈指针
	void* m_stack = nullptr;
	// 协程函数
	std::function<void()> m_cb;

首先是协程的构建函数。Fiber类提供了两个构造函数,带参数的构造函数用于构造子协程,初始化子协程的`ucontext_t`上下文和栈空间,要求必须传入协程入口函数,以及可选的协程栈大小,协程构造函数要负责分配栈内存空间。不带参的构造函数用于初始化当前线程的协程功能,构造线程主协程对象,以及对`t_fiber,t_thread_fiber`进行赋值。这个构造函数被定义成私有方法,不允许在类外部调用,只能通过`GetThis()`方法,在返回当前正在运行的协程时,如果发现当前线程的主协程未被初始化,那就用不带参的构造函数初始化线程主协程,因为`GetThis()`兼具初始化主协程的功能,在使用协程之前需显示调用一次`GetThis()`。

Fiber::Fiber()
{
	SetThis(this);
	m_state = RUNNING;
	
	if(getcontext(&m_ctx))
	{
		std::cerr << "Fiber() failed\n";
		pthread_exit(NULL);
	}
	
	m_id = s_fiber_id++;
	s_fiber_count ++;
	
}

Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool run_in_scheduler):
m_cb(cb), m_runInScheduler(run_in_scheduler)
{
	m_state = READY;

	// 分配协程栈空间
	m_stacksize = stacksize ? stacksize : 128000;
	m_stack = malloc(m_stacksize);

	if(getcontext(&m_ctx))
	{
		std::cerr << "Fiber(std::function<void()> cb, size_t stacksize, bool run_in_scheduler) failed\n";
		pthread_exit(NULL);
	}
	
	m_ctx.uc_link = nullptr;
	m_ctx.uc_stack.ss_sp = m_stack;
	m_ctx.uc_stack.ss_size = m_stacksize;
	makecontext(&m_ctx, &Fiber::MainFunc, 0);
	
	m_id = s_fiber_id++;
	s_fiber_count ++;
	
}

接下来就是协程切换的实现,也就是`resume()`和`yield()`

void Fiber::resume()
{
	assert(m_state==READY);
	
	m_state = RUNNING;

	if(m_runInScheduler)
	{
		SetThis(this);
		if(swapcontext(&(t_scheduler_fiber->m_ctx), &m_ctx))
		{
			std::cerr << "resume() to t_scheduler_fiber failed\n";
			pthread_exit(NULL);
		}		
	}
	else
	{
		SetThis(this);
		if(swapcontext(&(t_thread_fiber->m_ctx), &m_ctx))
		{
			std::cerr << "resume() to t_thread_fiber failed\n";
			pthread_exit(NULL);
		}	
	}
}

void Fiber::yield()
{
	assert(m_state==RUNNING || m_state==TERM);

	if(m_state!=TERM)
	{
		m_state = READY;
	}

	if(m_runInScheduler)
	{
		SetThis(t_scheduler_fiber);
		if(swapcontext(&m_ctx, &(t_scheduler_fiber->m_ctx)))
		{
			std::cerr << "yield() to to t_scheduler_fiber failed\n";
			pthread_exit(NULL);
		}		
	}
	else
	{
		SetThis(t_thread_fiber.get());
		if(swapcontext(&m_ctx, &(t_thread_fiber->m_ctx)))
		{
			std::cerr << "yield() to t_thread_fiber failed\n";
			pthread_exit(NULL);
		}	
	}	
}

在非对称协程里,执行`resume`时的当前执行环境一定是位于线程主协程里,所以这里的`swapcontext`操作的结果是把主协程的上下文保存到`t_thread_fiber->m_ctx`中,并且激活子协程的上下文;而执行`yield`时,当前执行环境一定是位于子协程里,所以这里的`swapcontext`操作的结果是把子协程的上下文保存到自己协程里的`m_ctx`中,同时从`t_thread_fiber`获取主协程的上下文并激活。`this`指针指的是子协程。

接下来是协程入口函数,通过封装协程入口函数,可以实现协程在结束时自动实行`yeild`操作
 

void Fiber::MainFunc()
{
	std::shared_ptr<Fiber> curr = GetThis();
	assert(curr!=nullptr);

	curr->m_cb(); 
	curr->m_cb = nullptr;
	curr->m_state = TERM;

	// 运行完毕 -> 让出执行权
	auto raw_ptr = curr.get();
	curr.reset(); 
	raw_ptr->yield(); 
}

接下来时协程的重置,重置协程就是重复利用已结束的协程,复用其栈空间,创建新协程

void Fiber::reset(std::function<void()> cb)
{
	assert(m_stack != nullptr&&m_state == TERM);

	if(getcontext(&m_ctx))
	{
		std::cerr << "reset() failed\n";
		pthread_exit(NULL);
	}

	m_ctx.uc_link = nullptr;
	m_ctx.uc_stack.ss_sp = m_stack;
	m_ctx.uc_stack.ss_size = m_stacksize;
	makecontext(&m_ctx, &Fiber::MainFunc, 0);

	m_state = READY;
	m_cb = cb;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值