关于协程运行原理,你了解吗?

– 此片主要一起探讨了解下协程的运作流程,对这几天的学习做一个总结

协程

为什么有协程的概念
  • 当前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)

以上是对协程的理解,描述不对的地方还请指出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值