– 此片主要一起探讨了解下协程的运作流程,对这几天的学习做一个总结
协程
为什么有协程的概念
- 当前web服务和互联网服务,本质上是一个IO密集性的服务,就是说需要处理网络连接或者读写相关的高耗时任务,这种高耗时的任务相对于CPU计算逻辑处理型的任务,处理的时间天差地别
IO 密集型服务的瓶颈不在 CPU 处理速度,而在于尽可能快速的完成高并发、多连接下的数据读写 - 以前处理方式:使用多进程,或者市多线程
– 多线程 高并发大量的IO等待会多线程被频繁的挂起和切换,非常消耗系统资源,同时多线程访问共享资源存在竞争问题。
– 多进程 不仅存在频繁调度切换问题,同时还会存在每个进程资源不共享的问题,需要额外引入进程间通信机制来解决。
协程出现给高并发和 IO 密集型服务开发提供了另一种选择
协程是什么
- 协程是一个比线程更轻量的微线程,我们一个进程可以拥有多个线程,一个线程可以拥有多个协程。因为协程被称为微线程,也叫迁程.
– 但是协程和线程又是不一样的,线程是属于内核调度的,线程间切换的时候,这些操作会涉及到用户态和内核态转换,开销会比较大。而协程用用户来调度的,它拥有自己的寄存器上下文和栈,协程切换的时间,只涉及到用户空间栈的切换,没有内核切换的开销,会大大的提高效率
– 从代码理解 协程是 生成器函数(yield)结合系统 IO多路复用技术 组建的一种事件循环的运行机制
协程精密代码实现(重点)
- 100行代码实现精密的协程库,掌握 协程运行原理 和 协程库设计精髓
- 在这个代码之前需要先自行了解 async await / 以及 yiled 生成器
(await其实是一个魔术方法,通过async的装饰 相当于给某个函数A添加了魔术方式 await , 而__await__是一个拥有yiled的生成器。这样在需要等待A函数执行的时候可以使用 “await A”)
"""
使用python实现用于演示应用代码 TcpServer
基于Liunx 的epoll(IO多路复用技术)来演示
"""
import select
from collections import deque
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
def create_listen_socket(bind_addr='0.0.0.0', bind_port=55555, backlogs=102400):
sock = socket(AF_INET, SOCK_STREAM)
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind((bind_addr, bind_port))
sock.listen(backlogs)
return sock
"""
引入 Future ,代表一个在未来才能获取到的数据。Future 一般由协程创建,
典型的场景是这样的:协程在等待一个 IO 事件,这时它便创建一个 Future 对象,并把执行权归还给事件循环。
"""
class Future:
def __init__(self, loop):
self.loop = loop # 当前事件循环对象;
self.done = False # 标识目标数据是否就绪
self.result = None # 目标数据
self.co = None # 关联的协程,Future 就绪后,事件循环 loop 将把它放入可执行队列重新调度
def set_coroutine(self, co):
self.co = co
def set_result(self, result):
self.done = True
self.result = result # 当前协程I/O操作结束后的数据
if self.co:
self.loop.add_coroutine(self.co) # 将就绪后的事件再次添加到执行队列中
def __await__(self): # 通过yiled让出主动劝
if not self.done:
yield self
return self.result
"""
封装了异步的sock连接及对send,accept,recv的异步改写
避免同步阻塞的情况
"""
class AsyncSocket:
def __init__(self, sock, loop):
sock.setblocking(False) # 设置成非阻塞的模式
self.sock = sock
self.loop = loop
def fileno(self):
return self.sock.fileno()
def create_future_for_events(self, events):
future = self.loop.create_future()
# 协程的回调函数,在对应的IO事件处理结束后调用
def handler(fileno, active_events): # 主要处理一下事情
self.loop.unregister_for_polling(fileno) # 1:从epoll中解除绑定
future.set_result(active_events) # 2:重新添加到事件循环的可执行队列中
self.loop.register_for_polling(self.fileno(), events, handler) # 注册到 epoll中
return future
async def accept(self):
while True:
try:
sock, addr = self.sock.accept()
return AsyncSocket(sock=sock, loop=self.loop), addr
except BlockingIOError: # 当前没有套接字连接的时候抛出异常
future = self.create_future_for_events(select.EPOLLIN)
await future # future拥有 __await__ 魔术方法 await可以驱动生成器的执行 通过yield挂起,等待下次的send触发执行
async def recv(self, bufsize):
while True:
try:
print("recv")
return self.sock.recv(bufsize)
except BlockingIOError: # 假设原生套接字未就绪,它将抛出 BlockingIOError 异常;
future = self.create_future_for_events(select.EPOLLIN)
await future
async def send(self, data):
while True:
try:
return self.sock.send(data)
except BlockingIOError: # 协程调用 create_future_for_events 方法创建一个 Future 订阅读事件 ( EPOLLIN ),并等待事件到达。
future = self.create_future_for_events(select.EPOLLOUT) # 订阅 可写事件 ( EPOLLOUT )
await future
class EventLoop:
"""模拟一个事件循环"""
def __init__(self):
self.epoll = select.epoll() # 创建一个 epoll 描述符, 用于订阅 IO 事件
# deque是栈和队列的一种广义实现,deque是"double-end queue"的简称;deque支持线程安全
self.runnables = deque() # 可执行协程队列
self.handlers = {} # IO 事件回调处理函数映射表;
def create_future(self):
return Future(loop=self)
"""
协程库还将套接字进行 异步化 封装,抽象出 AsyncSocket 类;
接口与原生 socket 对象类似。除了保存原生 socket 对象,
它还保存事件循环对象,以便通过事件循环订阅 IO 事件
"""
def create_listen_socket(self, bind_addr, bind_port, backlogs=102400):
sock = create_listen_socket(bind_addr, bind_port, backlogs)
return AsyncSocket(sock=sock, loop=self)
def register_for_polling(self, fileno, events, handler):
print(f"register fileno={fileno} for events {events}")
self.handlers[fileno] = handler
self.epoll.register(fileno, events)
def unregister_for_polling(self, filneo):
print(f"unregister fileno={filneo}")
self.epoll.unregister(filneo)
self.handlers.pop(filneo)
def add_coroutine(self, co):
self.runnables.append(co)
def run_coroutine(self, co):
try:
future = co.send(None) # 驱动协程的执行, 如果当前协程是TcpServer.serve_forever, 相当于执行协程
future.set_coroutine(co)
except StopIteration as e:
print(f"cououtine {co.__name__} stoppend")
def schedule_runabble_coroutines(self):
while self.runnables:
self.run_coroutine(co=self.runnables.popleft())
def run_forever(self):
while True:
self.schedule_runabble_coroutines()
events = self.epoll.poll(1) # 取出已经处理好的I/O事件
for filneo, event in events: # filneo:文件标识符, event:事件
handler = self.handlers.get(filneo)
if handler:
handler(filneo, event)
class TcpServer:
def __init__(self, loop, bind_addr='0.0.0.0', bind_port=55555):
self.loop = loop
self.listen_sock = self.loop.create_listen_socket(bind_addr=bind_addr, bind_port=bind_port) # 套接字对象
self.loop.add_coroutine(self.serve_forever())
async def serve_client(self, sock):
while True:
data = await sock.recv(1024)
if not data:
print('client disconnected')
break
await sock.send(data.upper)
async def serve_forever(self):
"""
循环的等待连接,如果有连接过来, 才会添加到事件循环中
没有连接的时候,就是个阻塞的状态
:return:
"""
while True:
sock, (addr, port) = await self.listen_sock.accept() # 等待客户端的连接
print(f"cleint connected addr= {addr} port={port}")
self.loop.add_coroutine(self.serve_client(sock)) # 有了连接之后,将连接的sock添加到loop的可执行队列中
def main():
loop = EventLoop()
server = TcpServer(loop=loop)
loop.run_forever()
if __name__ == '__main__':
main()
- 对于上述代码运行流程文字描述
- 1、创建事件循环 EventLoop 对象,它将创建 epoll 描述符;
- 2、创建 TcpServer 对象,它通过事件循环 loop 创建监听套接字,并将 serve_forever 协程放入可执行队列
- 3、事件循环 loop.run_forever 开始执行,它先调度可执行队列
- 4、可执行队列一开始只有一个协程 TcpServer.serve_forever ,它将开始执行 (由 run_coroutine 驱动);
- 5、执行权来到 TcpServer.serve_forever 协程,它调用 AsyncSocket.accept 准备接受一个新连接;
- 6、假设原生套接字未就绪,它将抛出 BlockingIOError 异常;
- 7、由于 IO 未就绪,协程创建一个 Future 对象,用来等待一个未来的 IO 事件 ( AsyncSocket.accept );
- 8、于此同时,协程调用事件循环 register_for_polling 方法订阅 IO 事件,并注册回调处理函数 handler
- 9、 future 是可以个可等待对象,await future 将执行权交给它的 await 函数;
- 10、由于一开始 future 是未就绪的,这时 yield 将协程执行逐层归还给事件循环,future 对象也被同时上报;
- 11、执行权回到事件循环,run_coroutine 收到协程上报的 future 后将协程设置进去,以便 future 就绪后重新调度协程;
- 12、可执行队列变空后,事件循环开始调用 epoll.poll 等待协程注册的 IO 事件 ( serve_forever );
- 13、当注册事件到达后,事件循环取出回调处理函数并调用
- 14、handler 先将套接字从 epoll 解除注册,然后调用 set_result 将活跃事件作为目标数据记录到 future 中;
- 15、set_result 将协程重新放回可执行队列;
- 16、IO 事件处理完毕,进入下一次事件循环;
- 17、事件循环再次调度可执行队列,这时 TcpServer.serve_forever 协程再次拿到执行权;
- 18、TcpServer.serve_forever 协程从 yield 语句恢复执行,开始返回目标数据,也就是先前设置的活跃事件;
- 19、AsyncSocket.accept 内 await future 语句取得活跃事件,然后循环继续;
- 20、循环再次调用原生套接字,这时它早已就绪,得到一个新套接字,简单包装后作为结果返回给调用者;
- 21、TcpServer.serve_forever 拿到代表新连接的套接字后,创建一个 serve_client 协程并交给事件循环 loop ;
- 22、TcpServer.serve_forever 进入下一次循环,调用 accept 准备接受下一个客户端连接;
- 23、如果监听套接字未就绪,执行权再次回到事件循环;
- 24、事件循环接着调度可执行队列里面的协程,*TcpServer.*serve_client 协程也开始执行了;
- 25、etc
事件循环流程图
- 基于上面是代码对事件循环的理解,下面又简单绘制了流程图,加深理解(不知绘图否清晰)
我测试好像打开图片始终不太能清晰显示,估计平台做了优化,可以右键保存图片查看哈 (_)
偏外赘述
IO多路复用技术
-
IO多路复用技术是什么:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力。
-
为什么使用IO多路复用技术: (这里做了简单的概念,可以跳过直奔主题)
应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标的输入、中断信号等等事件,再比如web服务器如nginx,需要同时处理来来自N个客户端的事件。 逻辑控制流在时间上的重叠叫做 并发 而CPU单核在同一时刻只能做一件事情,一种解决办法是对CPU进行时分复用(多个事件流将CPU切割成多个时间片,不同事件流的时间片交替进行)。在计算机系统中,我们用线程或者进程来表示一条执行流,通过不同的线程或进程在操作系统内部的调度,来做到对CPU处理的时分复用。这样多个事件流就可以并发进行,不需要一个等待另一个太久,在用户看起来他们似乎就是并行在做一样。 但凡事都是有成本的。线程/进程也一样,有这么几个方面: 线程/进程创建成本 CPU切换不同线程/进程成本 Context Switch 多线程的资源竞争 那么有没有一种可以在单线程/进程中处理多个事件流的方法呢?一种答案就是IO多路复用。
IO多路复用解决的本质问题是在用更少的资源完成更多的事。
-
IO多路复用技术的解决方案有哪些,Linux: select、poll; epollMacOS/FreeBSD: kqueue; Windows/Solaris:IOCP
(对于具体的运行机制,这个属于系统层面运行逻辑拉,我也还没有真正了解,需要了解的小伙伴可以自行查阅资料)
参考学习部分连接:
干货 | 进程、线程、协程 10 张图讲明白了!
一文看懂IO多路复用!
基于python认识生成器 generator(yield)
以上是对协程的理解,描述不对的地方还请指出