首页 后端开发 Python教程 Python协程、IO多路复用的方法

Python协程、IO多路复用的方法

Mar 23, 2017 pm 03:22 PM
python

协程

是一种用户态的请链接线程微线程

好处

无需线程上下文切换的开销

无需原子操作锁定及同步的开销

方便切换控制简化编程模型

高并发+高扩展性+低成本

缺点

无法利用多核的资源协程本质上就是一个单线程无需使用多核要结合多进程才能利用多核

进行阻塞操作会阻塞掉整个程序

用yield实现一个协程操作

def consumer(name):
    print("--->starting eating baozi...")
    while True:
        new_baozi = yield
        print("[%s] is eating baozi %s" % (name, new_baozi))
        # time.sleep(1)
 
 
def producer():
    r = con.__next__()
    r = con2.__next__()
    n = 0
    while n < 5:
        n += 1
        con.send(n)
        con2.send(n)
        print("\033[32;1m[producer]\033[0m is making baozi %s" % n)
 
 
if __name__ == &#39;__main__&#39;:
    con = consumer("c1")
    con2 = consumer("c2")
    p = producer()
登录后复制

对协程进行一个定义

1.必须在只有一个单线程里实现并发

2.修改共享数据不需加锁

3.用户程序里自己保存多个控制流的上下文栈

4.一个协程遇到IO操作自动切换到其它协程

一个协程的小例子手动切换IO操作

greenlet是一个用C实现的协程模块相比与python自带的yield它可以使你在任意函数之间随意切换而不需把这个函数先声明为generator

import greenlet
 
def test1():
    print(12)
    g2.switch()
    print(34)
    g2.switch()
 
def test2():
    print(56)
    g1.switch()
    print(78)
 
g1 = greenlet.greenlet(test1)
g2 = greenlet.greenlet(test2)
g1.switch()
登录后复制

Gevent 是一个第三方库可以轻松通过gevent实现并发同步或异步编程在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部但它们被协作式地调度

协程遇到IO操作自动切换

import gevent
 
 
def foo():
    print("Running in foo")
    gevent.sleep(2)
    print("Explicit context switch to foo again")
 
 
def bar():
    print("Explicit context to bar")
    gevent.sleep(1)
    print("Implicit context switch back to bar")
 
 
def func3():
    print("running func3")
    gevent.sleep(0)
    print("running func3 again")
 
gevent.joinall([
    gevent.spawn(foo),  # 生成
    gevent.spawn(bar),
    gevent.spawn(func3),
])
登录后复制

同步与异步的性能区别

import gevent
 
 
def task(pid):
    gevent.sleep(0.5)
    print("Task %s done" % pid)
 
 
def synchronous():
    for i in range(1,10):
        task(i)
 
 
def asynchronous():
    threads = [gevent.spawn(task, i) for i in range(10)]
    gevent.joinall(threads)
 
print("Synchronous:")
synchronous()
 
print("Asynchronous:")
asynchronous()
登录后复制

gevent并发爬网页

from urllib import request
import gevent
from gevent import monkey
 
 
def f(url):
    print("GET: %s" % url)
    resp = request.urlopen(url)
    data = resp.read()
    # file = open("url.html", "wb")
    # file.write(data)
    # file.close()
    print("%d bytes received from %s." % (len(data), url))
 
 
monkey.patch_all()  # 把当前程序的所有IO操作给我单独的做上标记
gevent.joinall([
    gevent.spawn(f, "http://www.yitongjia.com"),
    gevent.spawn(f, "http://www.jinyuncai.cn"),
    gevent.spawn(f, "https://www.guoshipm.com"),
])
登录后复制

通过gevent实现单线程下多socket并发

服务端

import sys
import socket
import time
import gevent
  
from gevent import socket,monkey
monkey.patch_all()
  
  
def server(port):
    s = socket.socket()
    s.bind((&#39;0.0.0.0&#39;, port))
    s.listen(500)
    while True:
        cli, addr = s.accept()
        gevent.spawn(handle_request, cli)
  
  
  
def handle_request(conn):
    try:
        while True:
            data = conn.recv(1024)
            print("recv:", data)
            conn.send(data)
            if not data:
                conn.shutdown(socket.SHUT_WR)
  
    except Exception as  ex:
        print(ex)
    finally:
        conn.close()
if __name__ == &#39;__main__&#39;:
    server(8001)
登录后复制

客户端

import socket
import threading
 
def sock_conn():
 
    client = socket.socket()
 
    client.connect(("localhost",8001))
    count = 0
    while True:
        #msg = input(">>:").strip()
        #if len(msg) == 0:continue
        client.send( ("hello %s" %count).encode("utf-8"))
 
        data = client.recv(1024)
 
        print("[%s]recv from server:" % threading.get_ident(),data.decode()) #结果
        count +=1
    client.close()
 
 
for i in range(100):
    t = threading.Thread(target=sock_conn)
    t.start()
 
并发100个sock连接
登录后复制

事件驱动

通常我们在写服务器处理模型程序的时候有几种模型

每收到一个请求生成一个进程处理这个请求

每收到一个请求生成一个线程处理这个请求

每收到一个请求放入一个事件列表让主进程通过非阻塞IO方式来处理请求

第1中方法由于创建新的进程的开销比较大所以会导致服务器性能比较差,但实现比较简单。

第2种方式由于要涉及到线程的同步有可能会面临死锁等问题。

第3种方式在写应用程序代码时逻辑比前面两种都复杂。

综合考虑各方面因素一般普遍认为第3种方式是大多数网络服务器采用的方式

IO多路复用

一.介绍

内核空间和用户空间

现在操作系统都是采用虚拟存储器那么对32位操作系统而言它的寻址空间虚拟存储空间为4G2的32次方。操作系统的核心是内核独立于普通的应用程序可以访问受保护的内存空间也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核kernel保证内核的安全操心系统将虚拟空间划分为两部分一部分为内核空间一部分为用户空间。针对linux操作系统而言将最高的1G字节从虚拟地址0xC0000000到0xFFFFFFFF供内核使用称为内核空间而将较低的3G字节从虚拟地址0x00000000到0xBFFFFFFF供各个进程使用称为用户空间。

进程切换

为了控制进程的执行内核必须有能力挂起正在CPU上运行的进程并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说任何进程都是在操作系统内核的支持下运行的是与内核紧密相关的。

进程阻塞

正在执行的进程由于期待的某些事件未发生如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等则由系统自动执行阻塞原语(Block)使自己由运行状态变为阻塞状态。可见进程的阻塞是进程自身的一种主动行为也因此只有处于运行态的进程获得CPU才可能将其转为阻塞状态。当进程进入阻塞状态是不占用CPU资源的。

文件描述符fd

文件描述符File descriptor是计算机科学中的一个术语是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上它是一个索引值指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时内核向进程返回一个文件描述符。在程序设计中一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存IO

缓存 I/O 又被称作标准 I/O大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中操作系统会将 I/O 的数据缓存在文件系统的页缓存 page cache 中也就是说数据会先被拷贝到操作系统内核的缓冲区中然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的缺点

数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

二.IO模式

刚才说了对于一次IO访问以read举例数据会先被拷贝到操作系统内核的缓冲区中然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说当一个read操作发生时它会经历两个阶段

1. 等待数据准备 (Waiting for the data to be ready)

2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段linux系统产生了下面五种网络模式的方案。

- 阻塞 I/Oblocking IO

- 非阻塞 I/Ononblocking IO

- I/O 多路复用 IO multiplexing

- 信号驱动 I/O signal driven IO

- 异步 I/Oasynchronous IO

阻塞IO

在linux中默认情况下所有的socket都是blocking一个典型的读操作流程大概是这样

当用户进程调用了recvfrom这个系统调用kernel就开始了IO的第一个阶段准备数据对于网络IO来说很多时候数据在一开始还没有到达。比如还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来。这个过程需要等待也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边整个进程会被阻塞当然是进程自己选择的阻塞。当kernel一直等到数据准备好了它就会将数据从kernel中拷贝到用户内存然后kernel返回结果用户进程才解除block的状态重新运行起来。

所以blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞IO

linux下可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时流程是这个样子

当用户进程发出read操作时如果kernel中的数据还没有准备好那么它并不会block用户进程而是立刻返回一个error。从用户进程角度讲 它发起一个read操作后并不需要等待而是马上就得到了一个结果。用户进程判断结果是一个error时它就知道数据还没有准备好于是它可以再次发送read操作。一旦kernel中的数据准备好了并且又再次收到了用户进程的system call那么它马上就将数据拷贝到了用户内存然后返回。

所以nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

IO多路复用

IO multiplexing就是我们说的selectpollepoll有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是selectpollepoll这个function会不断的轮询所负责的所有socket当某个socket有数据到达了就通知用户进程。

当用户进程调用了select那么整个进程会被block而同时kernel会“监视”所有select负责的socket当任何一个socket中的数据准备好了select就会返回。这个时候用户进程再调用read操作将数据从kernel拷贝到用户进程。

所以I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符套接字描述符其中的任意一个进入读就绪状态select()函数就可以返回。

这个图和blocking IO的图其实并没有太大的不同事实上还更差一些。因为这里需要使用两个system call (select 和 recvfrom)而blocking IO只调用了一个system call (recvfrom)。但是用select的优势在于它可以同时处理多个connection。

所以如果处理的连接数不是很高的话使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快而是在于能处理更多的连接。

在IO multiplexing Model中实际中对于每一个socket一般都设置成为non-blocking但是如上图所示整个用户的process其实是一直被block的。只不过process是被select这个函数block而不是被socket IO给block

异步IO

用户进程发起read操作之后立刻就可以开始去做其它的事。而另一方面从kernel的角度当它受到一个asynchronous read之后首先它会立刻返回所以不会对用户进程产生任何block。然后kernel会等待数据准备完成然后将数据拷贝到用户内存当这一切都完成之后kernel会给用户进程发送一个signal告诉它read操作完成了。

IO多路复用select、poll、epoll

select例子

server端

import select
import socket
import sys
import queue
 
server = socket.socket()
server.bind(("0.0.0.0", 6666))
server.listen(1000)
 
server.setblocking(False)   # 不阻塞
 
msg_dic = {}
 
inputs = [server, ]
outputs = []
 
while True:
    readable, writeable, exceptional = select.select(inputs, outputs, inputs)
    print(readable, writeable, exceptional)
    for r in readable:
        if r is server:     # 代表来了一个新连接
            conn, addr = server.accept()
            print("来了个新连接", addr)
            inputs.append(conn)     # 因为这个新建立的连接还没发数据过来,现在就接收的话程序就报错了
            # 所以要想实现这个客户端发数据来时server端能知道,就需要让select再监测这个conn
            msg_dic[conn] = queue.Queue()   # 初始化一个队列,后面存要返回客户端的数据
        else:
            data = r.recv(1024)
            print("收到数据", data)
            msg_dic[r].put(data)
            outputs.append(r)   # 放入返回的连接队列里
            # r.send(data)
            # print("send end...")
    for w in writeable:     # 要返回给客户端的连接列表
        data_to_client = msg_dic[w].get()
        w.send(data_to_client)      # 返回给客户端的元数据
        outputs.remove(w)       # 确保下次循环的时候,writeable不返回已经处理完的连接
 
    for e in exceptional:
        if e in outputs:
            outputs.remove(e)
        inputs.remove(e)
        del msg_dic[e]
登录后复制

client端

import socket
 
HOST = "127.0.0.1"  # The remote host
PORT = 6666  # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
    msg = bytes(input(">>:"), encoding="utf8")
    s.sendall(msg)
    data = s.recv(1024)
    # print(data)
    print(&#39;Received&#39;, repr(data))
s.close()
登录后复制
登录后复制

selectors例子

selectors封装了底层的select或epoll,它可以根据不同的操作系统去判断使用select或是epoll

selectors服务端

import socket
import selectors
 
 
sel = selectors.DefaultSelector()
 
 
def accept(sock, mask):
    conn, addr = sock.accept()
    print(&#39;accepted&#39;, conn, &#39;from&#39;, addr)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)
 
 
def read(conn, mask):
    data = conn.recv(1024)
    if data:
        print(&#39;echoing&#39;, repr(data), &#39;to&#39;, conn)
        conn.send(data)
    else:
        print(&#39;closing&#39;, conn)
        sel.unregister(conn)
        conn.close()
 
sock = socket.socket()
sock.bind(("0.0.0.0", 6666))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
 
while True:
    events = sel.select()
    for key, mask in events:
        callback = key.data
        callback(key.fileobj, mask)
登录后复制

selectors客户端

import socket
 
HOST = "127.0.0.1"  # The remote host
PORT = 6666  # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
    msg = bytes(input(">>:"), encoding="utf8")
    s.sendall(msg)
    data = s.recv(1024)
    # print(data)
    print(&#39;Received&#39;, repr(data))
s.close()
登录后复制
登录后复制

以上是Python协程、IO多路复用的方法的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

PHP和Python:代码示例和比较 PHP和Python:代码示例和比较 Apr 15, 2025 am 12:07 AM

PHP和Python各有优劣,选择取决于项目需求和个人偏好。1.PHP适合快速开发和维护大型Web应用。2.Python在数据科学和机器学习领域占据主导地位。

Python vs. JavaScript:社区,图书馆和资源 Python vs. JavaScript:社区,图书馆和资源 Apr 15, 2025 am 12:16 AM

Python和JavaScript在社区、库和资源方面的对比各有优劣。1)Python社区友好,适合初学者,但前端开发资源不如JavaScript丰富。2)Python在数据科学和机器学习库方面强大,JavaScript则在前端开发库和框架上更胜一筹。3)两者的学习资源都丰富,但Python适合从官方文档开始,JavaScript则以MDNWebDocs为佳。选择应基于项目需求和个人兴趣。

docker原理详解 docker原理详解 Apr 14, 2025 pm 11:57 PM

Docker利用Linux内核特性,提供高效、隔离的应用运行环境。其工作原理如下:1. 镜像作为只读模板,包含运行应用所需的一切;2. 联合文件系统(UnionFS)层叠多个文件系统,只存储差异部分,节省空间并加快速度;3. 守护进程管理镜像和容器,客户端用于交互;4. Namespaces和cgroups实现容器隔离和资源限制;5. 多种网络模式支持容器互联。理解这些核心概念,才能更好地利用Docker。

visual studio code 可以用于 python 吗 visual studio code 可以用于 python 吗 Apr 15, 2025 pm 08:18 PM

VS Code 可用于编写 Python,并提供许多功能,使其成为开发 Python 应用程序的理想工具。它允许用户:安装 Python 扩展,以获得代码补全、语法高亮和调试等功能。使用调试器逐步跟踪代码,查找和修复错误。集成 Git,进行版本控制。使用代码格式化工具,保持代码一致性。使用 Linting 工具,提前发现潜在问题。

vscode怎么在终端运行程序 vscode怎么在终端运行程序 Apr 15, 2025 pm 06:42 PM

在 VS Code 中,可以通过以下步骤在终端运行程序:准备代码和打开集成终端确保代码目录与终端工作目录一致根据编程语言选择运行命令(如 Python 的 python your_file_name.py)检查是否成功运行并解决错误利用调试器提升调试效率

vscode 扩展是否是恶意的 vscode 扩展是否是恶意的 Apr 15, 2025 pm 07:57 PM

VS Code 扩展存在恶意风险,例如隐藏恶意代码、利用漏洞、伪装成合法扩展。识别恶意扩展的方法包括:检查发布者、阅读评论、检查代码、谨慎安装。安全措施还包括:安全意识、良好习惯、定期更新和杀毒软件。

Python:自动化,脚本和任务管理 Python:自动化,脚本和任务管理 Apr 16, 2025 am 12:14 AM

Python在自动化、脚本编写和任务管理中表现出色。1)自动化:通过标准库如os、shutil实现文件备份。2)脚本编写:使用psutil库监控系统资源。3)任务管理:利用schedule库调度任务。Python的易用性和丰富库支持使其在这些领域中成为首选工具。

vs code 可以在 Windows 8 中运行吗 vs code 可以在 Windows 8 中运行吗 Apr 15, 2025 pm 07:24 PM

VS Code可以在Windows 8上运行,但体验可能不佳。首先确保系统已更新到最新补丁,然后下载与系统架构匹配的VS Code安装包,按照提示安装。安装后,注意某些扩展程序可能与Windows 8不兼容,需要寻找替代扩展或在虚拟机中使用更新的Windows系统。安装必要的扩展,检查是否正常工作。尽管VS Code在Windows 8上可行,但建议升级到更新的Windows系统以获得更好的开发体验和安全保障。

See all articles