什么是协程?这次直接教你玩转【建议新手收藏】

一,什么是协程?

协程被称为“轻量级线程”或者“用户态线程”。最近协程在高并发编程领域大放异彩,如Golang天生就支持协程,Lua和Python也支持协程。但其实协程并不是最近才出现的新技术,恰恰相反,协程是一项古老的技术。早期版本的Linux并不支持线程,这时就出现代替线程的轻量级线程–协程。比较有名的有:GNU Pth和Libtask(Go语言的作者之一Russ Cox的作品)。下面我们会以Libtask作为分析案例来解释协程的原理。
在这里插入图片描述

二,基本概念

要理解协程,首先要知道程序是怎么运行的。所以下面我们先来聊聊程序是怎么跑起来的。

我们知道CPU的使命就是执行程序中的指令,而且CPU内部有很多用于存放数据的寄存器,其中比较重要的一个寄存器叫EIP寄存器,它用于存储下一条要执行的指令。除了EIP寄存器之外,还有一个比较重要的寄存器叫ESP寄存器,它用于保存程序的栈顶位置。除此之外,CPU还有很多其他用途的寄存器,如:通用寄存器EAX、EDX和段寄存器CS、DS等等。

当一个程序被执行(称为进程)的时候,这些寄存器的值通常会被修改。所以当要切换进程执行的时候,只需要把这些寄存器的值保存下来,然后把新进程寄存器的值赋值到CPU中,那么就完成进程切换了,通常我们把这个过程称为上下文切换,协程的切换也类似。

在这里插入图片描述

三,协程原理

上面讨论过,只需要切换上下文就可以切换协程。而上下文就是CPU寄存器的值,所以要创建一个协程,首先要创建一个保存CPU寄存器值的对象。在Libtask中,使用mcontext结构体来保存寄存器的值。

mcontext结构体定义如下:

struct mcontext {
   int mc_gs;
   int mc_fs;
   int mc_es;
   int mc_ds;
   int mc_edi;
   int mc_esi;
   int mc_ebp;
   int mc_isp;
   int mc_ebx;
   int mc_edx;
   int mc_ecx;
   int mc_eax;
   int mc_trapno;
   int mc_err;
   int mc_eip;
   int mc_cs;
   int mc_eflags;
   int mc_esp;
   int mc_ss;
};

mcontext结构体用来保存寄存器的值,从mcontext的成员可以看到要保存的寄存器很多,包括CS、DS、EIP、EAX、EBX等等。

四,C函数调用原理

因为协程切换一般是通过调用一个swapcontext()的C函数来进行,这个函数的作用就是保存旧的协程上下文和替换新的协程上下文来进行协程切换,而新旧协程上下文就是通过C函数的参数来传递的,所以我们先来了解下C函数调用过程的原理。

C函数是通过栈空间来传递参数的,通过下图有个感性的认识:
在这里插入图片描述

在上图中,浅绿色部分是调用函数时把参数入栈的。入栈时,C语言是从右到左开始入栈的。例如我们调用swapcontext(old, new)这个函数时,会先把new参数入栈,然后再把old参数入栈。

另外,在调用一个函数时,CPU会自动把当前指令的下一条指令入栈。所以,在上图可以看到在参数后面还有返回地址。在返回地址下面保存的是函数的局部变量。

五,协程切换

现在到了重头戏–协程的切换。协程的切换是通过保存旧协程的上下文和替换新协程的上下文来实现的。

在Libtask库中,保存协程上下文通过getcontext()实现,而替换协程上下文是通过setcontext()实现。这两个函数都是使用汇编语言实现的。所以要看明白这两个函数就必须有汇编的基础。我们来看看这两个函数的实现:

getcontext()

gexcontext:
    movl    4(%esp), %eax
    movl    %fs, 8(%eax)
    movl    %es, 12(%eax)
    movl    %ds, 16(%eax)
    movl    %ss, 76(%eax)
    movl    %edi, 20(%eax)
    movl    %esi, 24(%eax)
    movl    %ebp, 28(%eax)
    movl    %ebx, 36(%eax)
    movl    %edx, 40(%eax)
    movl    %ecx, 44(%eax)
    movl    $1, 48(%eax)
    movl    (%esp), %ecx
    movl    %ecx, 60(%eax)
    leal    4(%esp), %ecx
    movl    %ecx, 72(%eax)
    movl    44(%eax), %ecx
    movl    $0, %eax
    ret

getcontext()函数的原型如下:

int getcontext(struct mcontext *ctx);其作用是把当前寄存器的值保存到参数ctx中。上面这段汇编代码就不详细解说了,有兴趣可以根据C函数参数传递的原理来对照一下就很容易理解。需要说明的一点是,“movl 4(%esp), %eax”这行汇编代码的作用是把ctx参数放置到EAX寄存器中,后面的操作都是通过mcontext结构体的偏移量来赋值的。

setcontext()

setcontext:
    movl    4(%esp), %eax
    movl    8(%eax), %fs
    movl    12(%eax), %es
    movl    16(%eax), %ds
    movl    76(%eax), %ss
    movl    20(%eax), %edi
    movl    24(%eax), %esi
    movl    28(%eax), %ebp
    movl    36(%eax), %ebx
    movl    40(%eax), %edx
    movl    44(%eax), %ecx
    movl    72(%eax), %esp
    pushl   60(%eax)
    movl    48(%eax), %eax
    ret

setcontext()函数是协程切换的切换点,原型如下:

int setcontext(struct mcontext *ctx); 其作用是把ctx参数中寄存器的值替换成CPU寄存器的值来实现切换。最后,我们就可以通过getcontext()和setcontext()这两个函数来实现swapcontext()函数了,实现很简单:

int swapcontext(struct mcontext *new, struct mcontext *old)
{
   getcontext(old);
   setcontext(new);
   return 0;
}

以后我们就可以通过swapcontext()函数来进行协程的切换了。

六,总结

在本文中,我们只要解释了协程的基本原理,但是要真正实现一个可以使用的协程库还需要做很多细节的工作,例如切换协程的栈空间(因为每个协程都需要有自己独立的栈空间才不会影响其协程)。

另外,一个完善的协程库还应该支持定时器和I/O阻塞自动切换协程等功能。对于怎么实现一个完善的协程库可以参考Libtask的源代码。
在这里插入图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
引用\[2\]中提到,在Python 3.5中引入了async/await关键字来定义协程函数。协程是一种轻量级的并发编程方式,它可以在一个线程中实现多个任务的并发执行。协程通过使用yield关键字来实现任务的暂停和恢复,从而实现任务之间的切换。而在Python 3.5之前,协程的实现需要使用asyncio.coroutine装饰器和yield from语句。 举个例子来说明协程的使用。在引用\[3\]中的代码中,我们定义了一个名为do_some_work的协程函数,它接受一个name参数和一个t参数。在函数内部,我们使用await关键字来暂停协程的执行,模拟了一些耗时的工作。然后我们使用asyncio.gather函数来同时运行多个协程任务。最后,我们使用事件循环的run_until_complete方法来运行这些协程任务,并获取它们的返回值。 在这个例子中,我们创建了两个协程任务task1和task2,它们分别执行了do_some_work函数。通过调用asyncio.gather函数,我们将这两个任务一起提交给事件循环进行执行。最后,我们通过调用run_until_complete方法来运行这些任务,并获取它们的返回值。在这个例子中,返回值是一个包含了协程函数的返回值的列表。 总结来说,协程是一种轻量级的并发编程方式,它可以在一个线程中实现多个任务的并发执行。在Python中,我们可以使用async/await关键字来定义和使用协程。通过使用事件循环和相关的方法,我们可以实现协程的并发执行,并获取它们的返回值。 #### 引用[.reference_title] - *1* [Python - 协程开发那点事儿](https://blog.csdn.net/qq_38900563/article/details/127168618)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [python 异步编程之协程](https://blog.csdn.net/qq_43745578/article/details/129862804)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简说Linux内核

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值