目录
协程是追求极限性能和优美的代码结构的产物,协程允许我们写同步代码的逻辑,却做着异步的时,避免了回调嵌套,使得代码逻辑清晰。
1.什么是协程
1.1 进程和线程
在了解什么是协程之前,我们先要了解什么是进程和线程。
- 进程是应用程序的启动实例,比如我们打开一个软件,运行一个游戏,就开启了一个进程。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
- 线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
线程的状态转换关系:
但是线程不同状态之间的转化是谁来负责实现的呢?是JVM吗?并不是,JVM需要通过操作系统内核中的TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的CPU资源。
进程和线程存在以下的痛点:
- 涉及到同步锁
- 线程在不同状态之间的转换
- 线程上下文的切换
以上涉及到任何一点,都是非常耗费性能的操作。
1.2 协程
协程(Coroutines)是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
1.2.1 协程挂起与唤醒
协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程。那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且它的异步操作完成时。为了保证唤醒时能正常运行,需要正确保存并恢复其运行时的上下文。
具体操作步骤为:
- 保存当前协程的上下文:运行栈、返回地址、寄存器状态
- 设置将要唤醒的协程的入口指令地址到IP(指令)寄存器
- 恢复将要唤醒的协程的上下文
1.3 进程、线程和协程的区别
上下文切换
- 进程:包括页目录以使用新的地址空间;切换内核栈和硬件上下文
- 线程:线程和进程的最大区别就在于地址空间,所以线程切换只需要切换内核栈和硬件上下文
- 协程又称为轻量级线程,每个协程都自带了一个栈,可以认为一个协程就是一个函数和这个存放这个函数运行时数据的栈,这个栈非常小,一般只有几十kb。
进程,线程,协程三者之间的区别。
| 进程 | 线程 | 协程 |
切换者 | 操作系统 | 操作系统 | 用户 |
切换时机 | 根据操作系统自己的切换策略,用户无法感知 | 根据操作系统自己的切换策略,用户无法感知 | 用户 |
切换内容 | 页全局目录 内核栈 硬件上下文 | 内核栈 硬件上下文 | 硬件上下文 |
切换内容的保存 | 保存于内核栈中 | 保存于内核栈中
| 保存于用户自己的变量(用户栈或者堆) |
切换过程 | 用户态->内核态->用户态 | 用户态->内核态->用户态 | 用户态(不陷入内核态) |
切换效率 | 低 | 中 | 高 |
2.Lua协程
Lua协程的基本语法如下:
方法 | 描述 |
---|---|
coroutine.create() | 创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用 |
coroutine.resume() | 重启 coroutine,和 create 配合使用 |
coroutine.yield() | 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果 |
coroutine.status() | 查看 coroutine 的状态 注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序 |
coroutine.wrap() | 创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复 |
coroutine.running() | 返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 corouting 的线程号 |
我们使用Lua的协程来完成生产者-消费者这一经典问题,实现代码如下:
local newProductor
function productor()
local i = 0
while true do
i = i + 1
send(i) -- 将生产的物品发送给消费者
end
end
function consumer()
while true do
local i = receive() -- 从生产者那里得到物品
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x表示需要发送的值,值返回以后,就挂起该协同程序
end
-- 启动程序
newProductor = coroutine.create(productor)
consumer()
输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
……