赞
踩
本质:协程的本质是函数和函数运行状态的组合。
入口函数:协程创建时指定一个入口函数,类似线程。执行时,可以在未执行完的时候退出(yield),让出cpu,然后在适当的时机恢复执行(resume)。因此,协程也被称为轻量级线程。
上下文:类似线程、进程,协程也有自己的上下文。协程上下文存储了函数的执行状态,包括cpu全部寄存器的值,如函数栈帧、代码的执行位置等。把这些信息设置给cpu,就表明恢复了该协程的运行。在Linux系统里这个上下文用ucontext_t结构体来表示,通getcontext()来获取。
在单线程内,协程不能并行执行,而是顺序执行,只能是一个协程结束或yield后,再执行另一个协程。因为协程本质就是函数,在单线程内,函数只能顺序执行。
不要在协程里使用线程级别的锁来做协程同步:单线程下协程是顺序执行的,如果一个协程在持有锁之后让出执行,那么同线程的其他任何协程一旦尝试再次持有这个锁,整个线程就锁死了,这和单线程环境下,连续两次对同一个锁进行加锁导致的死锁道理完全一样。
协程的yield和resume是完全由用户控制的:与线程不同,线程创建之后,线程的运行和调度也是由操作系统自动完成的,但协程创建后,协程的运行和调度都要由应用程序来完成,就和调用函数一样,所以协程也被称为用户态线程。
创建协程,其实就是把一个函数包装成一个协程对象,然后再用协程的方式把这个函数跑起来;
协程调度,其实就是创建一批的协程对象,然后再创建一个调度协程,通过调度协程把这些协程对象一个一个消化掉(协程可以在被调度时继续向调度器添加新的调度任务);
IO协程调度,其实就是在调度协程时,如果发现这个协程在等待IO就绪,那就先让这个协程让出执行权,等对应的IO就绪后再重新恢复这个协程的运行。
相对于对称协程,非对称协程具有代码实现简单的特点。
在对称协程中,子协程可以直接和子协程切换,也就是说每个协程不仅要运行自己的入口函数代码,还要负责选出下一个合适的协程进行切换,相当于每个协程都要充当调度器的角色,这样程序设计起来会比较麻烦,并且程序的控制流也会变得复杂和难以管理。
而在非对称协程中,可以借助专门的调度器来负责调度协程,每个协程只需要运行自己的入口函数,然后结束时将运行权交回给调度器,由调度器来选出下一个要执行的协程即可。非对称协程的行为与函数类似,因为函数在运行结束后也总是会返回调用者。
栈协程更接近于传统的函数调用模型,因为每个协程都有自己的栈,可以使用递归和局部变量等传统的编程概念。
独立栈和共享栈
上下文结构体定义,这个结构体是平台相关的,因为不同平台的寄存器不一样,下面列出的是所有平台都至少会包含的4个成员:
-
- 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上下文对应的函数中执行,相当于变相调用了函数:
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中。
和setcontext一样,swapcontext也不会返回,而是会跳转到ucp上下文对应的函数中执行,相当于调用了函数。swapcontext是sylar非对称协程实现的关键,线程主协程和子协程用这个接口进行上下文切换。
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
使用非对称协程模型,也就是子协程只能和线程主协程切换,而不能和另一个子协程切换,并且在程序结束时,一定要再切回主协程,以保证程序能正常结束,像下面这样:
注意,子协程不能直接resume另一个子协程:
resume/yield:
协程参与调度器调度,则和调度协程切换,否则,和主协程切换。
在调度线程中,调度协程就是主协程;
在caller线程中,调度协程和主协程不同。
sylar的非对称协程代码实现简单,并且在后面实现协程调度时可以做到公平调度(先来先服务)。
缺点
子协程只能和线程主协程切换,意味着子协程无法创建并运行新的子协程,并且在后面实现协程调度时,完成一次子协程调度需要额外多切换一次上下文(当前协程-主协程-另一个协程 ???)。
使用线程局部变量(C++11 thread_local变量)保存协程上下文对象。
线程局部变量每个线程都有一份,互不影响;而全局变量所有线程共享。由于协程是在线程里运行的,每个线程都有自己的主协程和正在运行的协程,不同线程之间互不影响,因此,使用线程局部变量保存协程上下文。
对于每个线程的协程上下文,sylar设计了两个线程局部变量来存储上下文信息(对应源码的t_fiber和t_thread_fiber),分别存储当前正在运行的协程和当前线程的主协程。
对每个协程,只设计了3种状态:READY,代表就绪态,RUNNING,代表正在运行,TERM,代表运行结束。
创建后即为就绪态,协程主动yield切出后,也为就绪态。非就绪态的情况下,要么在运行,要么结束。
没有INIT、HOLD状态。缺陷是,无法区分一个协程是刚创建,还是运行一半之后yield了,这在重置协程(reset)时有影响。重置协程时,如果协程对象只是刚创建但一次都没运行过,那应该是允许重置的,但如果协程的状态是运行到一半yield了,那应该不允许重置。虽然可以把INIT状态加上以区分READY状态,但既然简化了状态,那就简化到底,让协程只有在TERM状态下才允许重置,问题迎刃而解。
对于非对称协程来说,协程除了创建语句外,只有两种操作,一种是resume,表示恢复协程运行,一种是yield,表示让出执行。
协程的结束没有专门的操作,协程函数运行结束时协程即结束,协程结束时会自动调用一次yield以返回主协程。
- /// 协程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;
两个构造函数
GetThis()
resume()
yield()
reset()
MainFunc()
实现了一个N-M的协程调度器,N个线程运行M个协程。协程可绑定指定线程运行。
实现协程调度后,子协程可以通过向调度器添加调度任务的方式运行另一个子协程。
当你有很多协程时,如何把这些协程都消耗掉,这就是协程调度。
未实现协程调度:
对于每个协程,都需要用户手动调用协程的resume方法将协程运行起来,然后等协程运行结束并返回,再运行下一个协程。这种运行协程的方式其实是用户自己在挑选协程执行,相当于用户在充当调度器,显然不够灵活.
引入协程调度后,则可以先创建一个协程调度器,然后把这些要调度的协程传递给调度器,由调度器负责把这些协程一个一个消耗掉。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。