1.1 什么是进程和线程?
在了解协程之前先复习下进程和线程的概念,有助于我们更好的理解协程。
进程是正在运行的程序的实例,进程拥有代码和打开的文件资源、数据资源、独立的内存空间。
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以有很多线程,每条线程并行执行不同的任务,他们共享进程所拥有的的资源,但线程拥有自己的栈空间。
对操作系统而言,线程是最小的执行单元,进程是最小的资源管理单元。无论是进程还是线程,都是被操作系统所管理的,当发生进程(或线程)的切换,操作系统需要执行”用户态–>内核态–>用户态“切换操作,将切换内容(上下文)保存到内存中(或内核栈)中。
1.2 什么是协程?
协程(Coroutines)是一种比线程更加轻量级的存在,协程可以理解为一个特殊的函数,这个函数可以在某个地方挂起去执行别的函数,并且可以返回挂起处继续执行。
一个线程内可以由多个协程来交互运行,但是多个协程的运行是绝对串行的,也就是说同一时刻只有一个协程在运行,当一个协程运行时,其它的协程必须被挂起。
协程不是被操作系管理的,而是是在用户态执行,完全由程序所控制的,根据程序员所写的调度策略,通过协作(而不是抢占)来进行切换的。协程的本质思想就是控制函数运行时的主动让出(yield)和恢复(resume)。每个协程有自己的上下文,其切换由自己控制,当前协程切换到其它协程是由当前协程自己控制的。
协程函数与普通函数的区别:
协程的特点总结如下:
1.3 进程、线程、协程的对比
1.4 适用场景
IO密集型 (IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。)。
在IO密集型场景下,协程的调度开销比线程小,能够快速实现调度,尽可能的提升CPU利用率。
协程不适用于计算密集型场景,因为协程不能很好的利用多核CPU。
ucontext使得linux程序可以在用户态执行上下文切换,从而避免了进程或者线程切换导致的切换用户空间、切换堆栈,因此,效率相对更高。
ucontext属于glibc中的组件,ucontext提供4个函数 getcontext()
setcontext()
makecontext()
swapcontext()
,及 2个结构体ucontext_t
mcontext_t
,使用4个函数可以在线程中实现用户级的协程切换。这四个函数的作用可以通过linux man手册(举例:man getcontext
)进行查看。
ucontext组件的头文件为
,该文件中主要关注结构体struct ucontext_t
,内容如下:
/* Userlevel context. */
typedef struct ucontext_t
{
unsigned long int __ctx(uc_flags);
struct ucontext_t *uc_link;
stack_t uc_stack;
mcontext_t uc_mcontext;
sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
其中:
uc_link
指向后继上下文(即,当前协程运行结束后,接着要被恢复的下一个协程的上下文);
uc_stack
为该上下文中使用的栈;
uc_sigmask
为该上下文中的阻塞信号集合(可通过man sigprocmask
命令查看相关信息);
uc_mcontext
这个结构体依赖于机器且不透明,作用是保存硬件上下文,包括硬件寄存器的值 。
uc_stack协程栈对应的stack_t
结构体的内容如下:
/* Structure describing a signal stack. */
typedef struct
{
void *ss_sp; /* Base address of stack */
int ss_flags; /* Flags */
size_t ss_size; /* Number of bytes in stack */
} stack_t;
其中:
ss_sp
指针 指向的是协程的栈空间的起始地址,可以是用户级的栈变量指针,也可以是堆变量指针。
ss_flags
flag,在协程使用中设置该值为0。具体作用参考man sigaltstack
ss_size
表示栈空间的大小。
2.1 getcontext()、setcontext()函数介绍
// getcontext, setcontext - get or set the user context
#include
int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);
getcontext()
函数初始化ucp
结构体,将当前上下文保存在ucp
中 。
setcontext()
函数设置当前上下文为ucp
所指向的上下文。 ucp
所指向的上下文应该是调用 getcontext()
或 makecontext()
获得的。
返回值:当调用成功后,getcontext()返回0,setcontext()不会返回。当调用失败时,两个函数都返回-1并设置合适的errno。
2.2 makecontext()、swapcontext()函数介绍
// makecontext, swapcontext - manipulate user context
#include
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
makecontext()
函数修改ucp
指向的上下文,该上下文是之前通过调用getcontext()
获取的,也就是说在调用makecontext()
之前需要调用getcontext()
。且在调用makecontext()
之前,调用者必须分配一个新的栈空间为该上下文,并将栈空间地址设置到 ucp->uc_stack
,还要设置ucp->uc_link
指向一个后继协程的上下文。如果func协程上下文中uc_link
值为NULL,则执行完该协程后,线程会退出。
makecontext()
函数将ucp
对应的上下文中的指令地址指向 func
函数(协程)地址,argc
表示func
的入参个数,如果入参为空,该值设置为0。如果argc
有值,入参为一系列 (int)整型的数据。
swapcontext()
保存当前协程的上下文到oucp
,然后激活(切换到) ucp
所指向的上下文对应的协程。返回值:当调用成功后,swapcontext()
不会返回;当调用失败后,返回-1并设置合适的errno。
swapcontext()
函数可以理解成为 getcontext()
和 setcontext()
函数的组合。
3.1 getcontext()、setcontext()函数使用举例
下面的代码摘自网上示例:
#include
#include
#include
int main(int argc, const char *argv[])
{
ucontext_t context;
getcontext(&context);
sleep(1);
puts("Hello world");
setcontext(&context);
return 0;
}
如图所示,程序在输出第一个“Hello world"后并没有退出程序,而是持续不断的输出”Hello world“。代码执行过程如下:
1、getcontext(&context); 初始化并保存了当前的上下文;
2、执行sleep语句,接着输出"Hello world";
3、执行setcontext(&context); 语句,将当前上下文设置为context中保存的值。注意:此时指令地址指向了**sleep(1);**语句的地址;
4、代码跳转**sleep(1)**重新执行;
……
如此往复,所以导致程序不断的输出”Hello world“。
3.2 makecontext()、swapcontext()函数使用举例
下面的代码摘自man手册man makecontext
#include
#include
#include
static ucontext_t uctx_main, uctx_func1, uctx_func2;
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
static void func1(void)
{
printf("func1: started\n");
printf("func1: swapcontext(&uctx_func1, &uctx_func2)\n");
if (swapcontext(&uctx_func1, &uctx_func2) == -1)
handle_error("swapcontext");
printf("func1: returning\n");
}
static void func2(void)
{
printf("func2: started\n");
printf("func2: swapcontext(&uctx_func2, &uctx_func1)\n");
if (swapcontext(&uctx_func2, &uctx_func1) == -1)
handle_error("swapcontext");
printf("func2: returning\n");
}
int main(int argc, char *argv[])
{
char func1_stack[16384];
char func2_stack[16384];
if (getcontext(&uctx_func1) == -1)
handle_error("getcontext");
uctx_func1.uc_stack.ss_sp = func1_stack;
uctx_func1.uc_stack.ss_size = sizeof(func1_stack);
uctx_func1.uc_link = &uctx_main;
makecontext(&uctx_func1, func1, 0);
if (getcontext(&uctx_func2) == -1)
handle_error("getcontext");
uctx_func2.uc_stack.ss_sp = func2_stack;
uctx_func2.uc_stack.ss_size = sizeof(func2_stack);
/* Successor context is f1(), unless argc > 1 */
uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;
makecontext(&uctx_func2, func2, 0);
printf("main: swapcontext(&uctx_main, &uctx_func2)\n");
if (swapcontext(&uctx_main, &uctx_func2) == -1)
handle_error("swapcontext");
printf("main: exiting\n");
exit(EXIT_SUCCESS);
}
编译并执行上述代码
通过结果1的打印,我们可以看到:
1)程序先从main函数切换到协程func2进行执行;
2)在func2中主动挂起(yield)并进入协程func1中执行;
3)然后在func1协程中主动挂起(yield)并返回func2中(resume)执行剩余代码;
4)协程func2执行完后,根据main函数中设置的“ uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;”又返回协程func1中执行剩余代码;
5)协程func1执行完后,根据main函数中设置的“uctx_func1.uc_link = &uctx_main;”返回main函数执行剩余的代码;
6)最后退出。
注意:swapcontext()函数会保存当前上下文到第一个参数,然后切换到第二个参数所指向的协程上下文。
结果2:
如图所示,两个结果的差别是图2中多了一个参数(“带参数”),对应代码中的区别是:
“uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;”
当入参中argc值大于1时,设置uctx_func2.uc_link
值为NULL,此时表示该协程执行完后没有后继上下文,该协程执行完后线程就退出了,因为这个程序中就只有一个线程,所以程序也退出了。
1、协程:https://www.jianshu.com/p/6dde7f92951e
2、C语言实现协程:https://blog.csdn.net/xiaobing1994/article/details/79012848/?
3、linux的man手册