一、概述

接着上篇文章持续,这篇文章首要解说IO模型和协程,这两块内容也是十分十分重要的。

Python的I/O模型分为**同步(sync)异步(async)**两种:

  • 同步I/O模型是指,当一个线程在等候I/O操作完结时,它不能履行其他使命,需求一直等候I/O操作完结,直到接收到I/O操作的完结告诉后才持续履行。

  • 异步I/O模型是指,当一个线程发起一个I/O操作后,不会等候I/O操作完结,而是直接履行其他使命,当I/O操作完结后,再经过回调或事情告诉来处理I/O操作的结果。

Python 中的协程:

  • 协程是一种轻量级的用户级线程,它在单线程内履行不会堵塞主线程,能够在多个使命间轻松地切换,因而能够用于完结异步I/O操作。协程的完结办法与生成器十分类似,经过运用yield句子来暂停和康复履行
  • 协程能够与asyncio库配合运用,来完结异步I/O操作。这种办法能够极大地进步程序的功率,由于程序不用等候I/O操作完结,能够在等候I/O操作期间履行其他使命。

Python 高级编程之IO模型与协程(四)

二、IO模型

上面已经对IO模型大致描绘了一下,其实细分能够分为五种模型:

  • 同步堵塞IO(Blocking IO):即传统的IO模型。
  • 同步非堵塞IO(Non-blocking IO):默许创立的socket都是堵塞的,非堵塞IO要求socket被设置为NONBLOCK。
  • IO多路复用(IO MulTIplexing):即经典的Reactor规划方式,有时也称为异步堵塞IO,Java中的Selector和Linux中的epoll都是这种模型。
  • 异步IO(Asynchronous IO):即经典的Proactor规划方式,也称为异步非堵塞IO。
  • signal driven IO:信号驱动IO , 在实践中并不常用,所以只剩下四种IO Model。

1)IO 模型准备

IO模型解说之前,先了解一些概念:

  • 用户空间和内核空间
  • 进程切换
  • 进程的堵塞
  • 文件描绘符
  • 缓存 I/O

1、用户空间和内核空间

用户空间和内核空间是操作体系的两个重要概念。

  • 用户空间是一个独立的内存空间,其中存储着用户进程(包含应用程序)运转时运用的数据和代码。用户空间是安全的,由于进程之间相互独立,不能直接拜访对方的内存空间。

  • 内核空间操作体系内核代码和数据结构所运用的内存空间。内核空间是不安全的,由于内核代码运转时有最高的权限,能够拜访体系的一切资源,而且能够直接操控其他一切进程。

  • 用户空间和内核空间之间的分界线是很重要的,由于它保护了操作体系的安全和安稳。例如,假如一个用户进程的代码呈现问题,它可能会崩溃,可是它不会影响整个体系的安稳性。

2、进程切换

进程切换是操作体系的一个基本概念,指的是操作体系在不同的进程之间进行切换,以便完结多使命处理。

进程切换的进程包含如下过程:

  • 保存当时进程的状况:在进程切换前,操作体系需求保存当时进程的履行状况,包含 CPU 寄存器的值、栈的内容等。

  • 选择下一个要运转的进程:操作体系依据调度算法选择下一个要运转的进程,例如先来先服务(FCFS)、最短进程优先(SPN)等。

  • 载入下一个进程的状况:操作体系从存储器中读取下一个进程的状况,并载入到 CPU 寄存器中。

  • 康复下一个进程的履行:操作体系将 CPU 操控权交给下一个进程,让它持续履行。

进程切换是一个高价值的操作,需求很多的时刻和体系资源。因而,操作体系需求合理地调度进程,以最小化进程切换的次数。

3、进程的堵塞

进程堵塞是指一个进程由于等候某个事情的产生而暂时停止履行。这个事情能够是等候 I/O 操作完结、等候资源的分配、等候信号的到达等。当事情产生时,该进程再康复履行。

  • 进程堵塞是一种常见的进程状况,与其他进程状况(例如安排妥当状况、运转状况)不同。当一个进程堵塞时,它不再需求 CPU 的履行,因而操作体系能够切换到其他进程上,以运用 CPU 资源。

  • 进程堵塞能够削减 CPU 的闲暇时刻,并有助于有用地办理体系资源,可是需求消耗很多的体系资源来维护堵塞行列和状况信息。因而,操作体系需求合理地办理进程堵塞,以保证体系的高效运转。

4、文件描绘符fd

文件描绘符(file descriptor)是一个非负整数,用于指代一个翻开的文件或其他 I/O 设备(例如管道、套接字等)。它为程序供给了一种用于拜访文件或 I/O 设备的笼统办法,而不需求知道底层的完结细节。

  • 每个进程都有一个文件描绘符表,该表包含了每个翻开的文件或 I/O 设备的信息。当程序翻开一个文件或 I/O 设备时,操作体系会分配一个文件描绘符,并将其存储在该进程的文件描绘符表中。之后,程序能够运用该文件描绘符来读写文件或操控 I/O 设备。
  • 文件描绘符是体系资源,需求恰当地运用和办理。当一个进程结束时,该进程的一切翻开的文件和 I/O 设备都将封闭,相应的文件描绘符也将被释放。因而,程序必须确保在不再运用文件描绘符时封闭它。

5、缓存 I/O

  • 缓存 I/O 指的是运用缓存(即暂时存储区)来保存常常拜访的数据。在计算机技术中,这能够指的是将数据缓存在体系的内存或存储设备中,或者运用网络中的缓存(如网页浏览器缓存或代理缓存)来削减需求经过网络传输的数据量。
  • 缓存能够经过削减履行的 I/O(输入/输出)操作来进步体系的功能,由于数据能够从缓存中快速获取,而不是每次都从原始来源中获取。这能够导致更快的拜访时刻和削减的延迟,然后进步全体体系功能。

2)IO 模型详解

1、同步堵塞IO(Blocking IO)

Python 中的同步堵塞 I/O 是一种 I/O 操作,在这种情况下,程序的履行会被堵塞,直到 I/O 操作完结。换句话说,程序将等候 I/O 操作完结,才干持续履行下一个使命。这种类型的 I/O 被称为“堵塞”,由于它堵塞了程序的履行,而且被称为“同步”,由于它以同步的办法产生,程序等候 I/O 操作完结后再持续。

  • 例如,当运用 Python 中的 read 办法从文件读取时,程序将等候整个文件读取完毕,才干持续履行下一个使命。这是同步堵塞 I/O 的一个比如。

  • 当 I/O 操作相对较短且程序能够等候其完结后才持续履行下一个使命时,一般会运用同步堵塞 I/O。然而,当 I/O 操作比较长时,程序可能会被堵塞很长一段时刻,然后导致功能下降。在这种情况下,一般更好运用异步 I/O。

  • linux中,默许情况下一切的socket都是堵塞IO,一个典型的读操作流程大概是这样:

Python 高级编程之IO模型与协程(四)

我们之前写的都是堵塞IO模型(协程除外)

在服务端开设多进程或者多线程 进程池线程池 其实还是没有解决IO问题 该等的当地还是得等 没有躲避 只不过多个人等候彼此互不搅扰。示例如下:

服务端:

import socket
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
    conn, addr = server.accept()
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:break
            print(data)
            conn.send(data.upper())
        except ConnectionResetError as e:
            break
    conn.close()

客户端:

import socket
client = socket.socket()
client.connect(('127.0.0.1',8081))
while True:
    client.send(b'hello world')
    data = client.recv(1024)
    print(data)

2、同步非堵塞IO(Non-blocking IO)

Python 中的同步非堵塞 I/O 是一种程序不用等候 I/O 操作完结就能够持续履行下一个使命的 I/O 操作。相反,程序在 I/O 操作正在进行时持续履行下一个使命。这种类型的 I/O 被称为“非堵塞”,由于它不会堵塞程序的履行,而且被称为“同步”,由于它仍以同步的办法操作,程序会定期查看 I/O 操作的状况。

  • 例如,在 Python 中完结同步非堵塞 I/O,能够运用 select 模块,该模块供给了对操作体系底层 I/O 多路复用功能的拜访,允许您一起监督多个 I/O 操作,而不会堵塞。

  • 当 I/O 操作预计需求很长时刻才干完结,且程序需求在此期间持续处理其他使命时,一般运用同步非堵塞 I/O。这能够进步程序的功能和响应性。

  • python下,能够经过设置socket使其变为non-blocking(server.setblocking(False))。当对一个non-blocking socket履行读操作时,流程是这个姿态:

Python 高级编程之IO模型与协程(四)

示例如下:

服务端:

import socket
import time
server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)
server.setblocking(False)
# 将一切的网络堵塞变为非堵塞
r_list = []
del_list = []
while True:
    try:
        conn, addr = server.accept()
        r_list.append(conn)
    except BlockingIOError:
        for conn in r_list:
            try:
                data = conn.recv(1024)  # 没有消息 报错
                if len(data) == 0:  # 客户端断开链接
                    conn.close()  # 封闭conn
                    # 将无用的conn从r_list删除
                    del_list.append(conn)
                    continue
                conn.send(data.upper())
            except BlockingIOError:
                continue
            except ConnectionResetError:
                conn.close()
                del_list.append(conn)
        # 挥手无用的链接
        for conn in del_list:
            r_list.remove(conn)
        del_list.clear()

客户端:

import socket
client = socket.socket()
client.connect(('127.0.0.1',8081))
while True:
    client.send(b'hello world')
    data = client.recv(1024)
    print(data)

3、IO多路复用(IO MulTIplexing)

“IO 多路复用” 是一种用于监听多个网络连接的技术,以进步网络程序的功能和功率。常用的 IO 多路复用技术有 select、poll、epoll

在 Python 中,能够运用 select 模块来完结 IO 多路复用。select 模块供给了三个函数:select、poll、epoll,能够在不同的平台上运用不同的函数来完结 IO 多路复用。它们都是用来监听多个文件描绘符(socket)的读写情况,以完结对多个socket的高效办理。

  • select:select 是最早的 I/O 多路复用 API 之一,并在大多数 Unix 类体系上广泛支撑。select 能够监督很多的文件描绘符,但它有许多约束,例如具有固定的最大文件描绘符数量(1024),以及当正在监督的文件描绘符列表更改时受到竞赛条件的影响。

  • poll:poll 是为了解决 select 中的一些约束而引进的,在大多数 Unix 类体系上广泛支撑。poll 能够监督比 select 更多的文件描绘符(无约束),而且它还供给了关于每个文件描绘符状况的更多信息。

  • epoll:epoll 被引进为 poll 和 select 的更有用替代品,并可在 Linux 体系上运用。epoll 具有许多比 poll 和 select 更有用的功能优势,例如更快更可扩展的规划,以及能够以较低开支监督很多文件描绘符的能力。

select、poll、epoll之间的差异:

select poll epoll
操作办法 遍历 遍历 回调
底层完结 数组 链表 哈希表
IO功率 每次调用都进行线性遍历,时刻杂乱度为O(n) 每次调用都进行线性遍历,时刻杂乱度为O(n) 事情告诉办法,每当fd安排妥当,体系注册的回调函数就会被调用,将安排妥当fd放到rdllist里面。时刻杂乱度O(1)
最大连接数 1024(x86)或 2048(x64) 无上限 无上限
fd复制 每次调用select,都需求把fd集合从用户态复制到内核态 每次调用poll,都需求把fd集合从用户态复制到内核态 调用epoll_ctl时复制进内核并保存,之后每次epoll_wait不复制

它的流程如图:

Python 高级编程之IO模型与协程(四)

  • 管的目标只有一个的时分 其实IO多路复用连堵塞IO都比不上!!!可是IO多路复用能够一次性监管很多个目标
  • 监管机制是操作体系本身就有的 假如你想要用该监管机制(select)需求,
  • 你导入对应的select模块

示例如下:

import socket
import select
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
server.setblocking(False)
read_list = [server]
while True:
    r_list, w_list, x_list = select.select(read_list, [], [])
    """
    帮你监管
    一旦有人来了 立刻给你返回对应的监管目标
    """
    # print(res)  # ([<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>], [], [])
    # print(server)
    # print(r_list)
    for i in r_list:  #
        """针对不同的目标做不同的处理"""
        if i is server:
            conn, addr = i.accept()
            # 也应该添加到监管的行列中
            read_list.append(conn)
        else:
            res = i.recv(1024)
            if len(res) == 0:
                i.close()
                # 将无效的监管目标 移除
                read_list.remove(i)
                continue
            print(res)
            i.send(b'hello python')
 # 客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
    client.send(b'hello world')
    data = client.recv(1024)
    print(data)

4、异步IO(Asynchronous IO)

异步IO模型是一切模型中功率最高的 也是运用最广泛的 。先看一下它的流程:

Python 高级编程之IO模型与协程(四)

"""
异步IO模型是一切模型中功率最高的 也是运用最广泛的
相关的模块和结构
	模块: asyncio模块
	异步结构:sanic tronado twisted
		速度快!!!
"""
import threading
import asyncio
@asyncio.coroutine
def hello():
    print('hello world %s'%threading.current_thread())
    yield from asyncio.sleep(1)  # 换成真实的IO操作
    print('hello world %s' % threading.current_thread())
loop = asyncio.get_event_loop()
tasks = [hello(),hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

三、Python 协程介绍

“协程”是一种在单线程内完结多使命的机制,它的目的是让程序员能够方便地完结异步 I/O 操作,这是经过在单线程中进行切换使命完结的。

  • 在 Python 中,协程是经过生成器完结的。它们与一般生成器有所不同,由于它们需求运用 async/await 语法以及 asyncio 库来工作。

  • 运用协程能够简化异步编程,由于您能够运用类似同步代码的办法来编写异步代码。它们也能够在单线程中有用地运用 CPU 时刻,由于它们能够在没有堵塞的情况下等候 I/O 操作的完结。

  • 举个比如,假设您有一个网络服务器,需求一起处理多个客户端恳求。运用协程,您能够在单线程中为每个客户端创立一个协程,并在客户端恳求完结后从协程中康复,然后承受其他客户端的恳求。

  • 总的来说,协程是一种高效且简洁的异步编程办法,为您的 Python 程序供给了更多的并发性和功率。

以下是一个简略的 Python 协程示例代码:

import asyncio
async def coroutine_example():
    print("This is a coroutine example")
    await asyncio.sleep(1)
    print("Coroutine example is done!")
async def main():
    task = asyncio.create_task(coroutine_example())
    await task
asyncio.run(main())

该代码定义了一个名为 coroutine_example 的协程,该协程打印一条消息,然后运用 asyncio.sleep 函数暂停 1 秒。随后,代码定义了一个名为 main 的函数,该函数创立了一个使命,并运用 await task 等候该使命完结。最后,代码运用 asyncio.run 函数运转 main 函数,并启动协程。

运转该代码后,您将看到以下输出:

This is a coroutine example
Coroutine example is done!

四、进程、线程与协程的联系与差异

进程、线程和协程是操作体系中用来办理程序履行的三种不同的技术。

  • 进程:进程是操作体系中最基本的资源分配单元,是程序的实体。它是体系进行资源分配和调度的独立单位。每个进程都有独立的内存空间,因而在一个进程中呈现故障不会影响其他进程。

  • 线程:线程是进程的一个履行单元,是操作体系分配资源的最小单元。多线程能够同享进程的资源,因而线程间的通讯和协作比进程间更加方便。不幸的是,线程之间存在竞赛联系,而且当一个线程呈现故障时,整个进程都会受到影响。

  • 协程:协程是一种在单线程中履行多使命的机制,是线程的一种特殊方式。它不同于多线程,由于它不会独立分配资源,而是在一个线程中同享资源。因而,协程的完结比线程更加轻量,能够进步程序的功率。不过,协程的完结也比线程更加杂乱,由于需求编写更多的代码来和谐使命的。

这里仅仅简略的讲了一下协程的一些概念和简略示例,下篇文章会重点解说运用生成器完结协程,IO模型和协程是python十分重要的两个知识点,也是提高python代码履行功率的最有校办法和思路。小伙伴能够关注我的大众号【大数据与原生技术分享】进行深入技术交流~

Python 高级编程之IO模型与协程(四)