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

一、概述

接着上篇文章继续,这篇文章主要讲解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操作期间执行其他任务。

在这里插入图片描述

二、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,一个典型的读操作流程大概是这样:

img

我们之前写的都是阻塞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执行读操作时,流程是这个样子:

img

示例如下:

服务端:

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不拷贝

它的流程如图:

img

  • 管的对象只有一个的时候 其实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模型是所有模型中效率最高的 也是使用最广泛的 。先看一下它的流程:

img

"""
异步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代码执行效率的最有校方法和思路。小伙伴可以关注我的公众号【大数据与原生技术分享】进行深入技术交流~

posted @ 2023-02-10 22:36  大数据老司机  阅读(268)  评论(0编辑  收藏  举报