协程基本概念
协程本质就是一条线程,它能实现多个任务在一条线程上来回切换执行。
使用协程可以在执行任务遇到IO时切换到别的任务继续执行,避免进入阻塞态,提高了CPU利用率。另外降低了操作系统负担。
进程、线程、协程的区别
进程 | 线程 | 协程 |
---|---|---|
操作系统资源分配的最小单位 | 操作系统调度执行的最小单位 | 操作系统不可见 |
内存隔离 | 内存共享 | 内存共享 |
数据不安全 | 数据不安全 | 数据安全 |
开销大 | 开销小 | 开销极小 |
发生IO时都能感知,由操作系统负责切换 | 发生IO时都能感知,由操作系统负责切换 | 发生IO时程序切换,文件、input等io不能感知 |
gevent模块:
安装
这是第三方模块,需要单独安装。
pip install gevent
打补丁
from gevent import monkey
monkey.patch_all()
这里我重点说一下,monkey.patch_all()目的是使用gevent自己的模块替代了原来的模块,只有使用patch_all中支持的模块触发IO时才会被识别并切换任务,若使用了patch_all不支持的模块触发IO时是无法被识别并切换任务的。
-
gevent支持清单:
sockt、dns、time、thread、os、ssl、subprocess、aggressive、Event、builtins、signal、queue、contextvars。
-
gevent不支持清单:
sys及其它不在支持清单中的模块。
-
查看gevent支持清单的方法:
请查看patch_all函数,它有很多默认值参数,默认值为True的模块名是支持的,默认值为False的模块名是不支持的,未列出的模块名是不支持的。
def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True, subprocess=True, sys=False, aggressive=True, Event=True, builtins=True, signal=True, queue=True, contextvars=True, **kwargs):
-
使用patch_all()的注意事项:
一、使用patch_all()时已经导入了所有可支持的模块,不需要再重复导入。
这里举个导入socket的例子:
-
首先看看导入原始的socket:
import socket print(socket.socket) out: <class 'socket.socket'>
-
再看看patch_all的socket:
from gevent import monkey monkey.patch_all() import socket print(socket.socket) out: <class 'gevent._socket3.socket'>
-
最后比较一下gevent.socket:
from gevent import monkey monkey.patch_all() import gevent print(gevent.socket.socket) out: <class 'gevent._socket3.socket'>
-
总结:从前面的案例中可以看到patch_all后,import socket已经没有必要,因为gevent.socket.socket()和socket.socket()是完全一样的。所以patch_all之后不需要再import原来的包,直接使用“gevent.包名”即可。
二、不需要导入的模块建议加False参数不导入它。
patch_all()导入了非常多的模块,假设你使用socket,没用到signal、queue、contextvars、aggressive,那么可以这样做:
from gevent import monkey monkey.patch_all(signal=False, queue=False, contextvars=False, aggressive=False)
-
gevent使用案例
-
案例一:
import random import gevent def func(n): n += 1 print(f'任务{n}开始运行!') gevent.sleep(random.random()) print(f'任务{n}结束运行。') tasks = [] for i in range(5): tasks.append(gevent.spawn(func,i)) gevent.joinall(tasks)
输出:
任务1开始运行! 任务2开始运行! 任务3开始运行! 任务4开始运行! 任务5开始运行! 任务3结束运行。 任务1结束运行。 任务4结束运行。 任务2结束运行。 任务5结束运行。
说明:
gevent.spawn(func,i),这里是创建协程实例,该实例的第一初始化参数是func,即要执行协程任务的函数;第二个及以后任意多个参数都是要传给func的参数。
gevent.joinall(tasks),这里是等待所有协程任务,直到所有协程任务完成后才继续向后执行。
-
案例二:
import gevent def func1(): print('func1开始运行!') gevent.sleep(0.3) print('func1继续运行。') def func2(): print('func2开始运行!') gevent.sleep(0.2) print('func2继续运行。') def func3(): print('func3开始运行!') gevent.sleep(0.1) print('func3继续运行。') gevent.joinall(( gevent.spawn(func1), gevent.spawn(func2), gevent.spawn(func3), ))
输出:
func1开始运行! func2开始运行! func3开始运行! func3继续运行。 func2继续运行。 func1继续运行。
说明:
gevent.joinall((gevent.spawn(func1), gevent.spawn(func2), gevent.spawn(func3),)):这里和案例一功能类似,添加协程任务func1、func2、func3,然后等待这些协程任务执行完毕。
-
案例三:
协程socket的server端:
import gevent from gevent import monkey monkey.patch_all() def func(con): while True: msg = con.recv(1024).decode('utf8') con.send(msg.upper().encode('utf8')) sk = gevent.socket.socket() sk.bind(('127.0.0.1', 9001)) sk.listen() while True: conn, _ = sk.accept() gevent.spawn(func, conn)
多线程socket的client端:
import socket import time from threading import Thread def client(i): sk = socket.socket() sk.connect(('127.0.0.1', 9001)) for j in range(100): sk.send(f'hello -> {i}'.encode('utf8')) msg = sk.recv(1024).decode('utf8') print(msg) time.sleep(0.5) if __name__ =="__main__": for i in range(500): Thread(target=client, args=(i,)).start()
说明:
上述server端是使用多协程方法编写的,while True接收连接,然后给该连接分配服务,即接收client端发来的消息,将其转换成大写再转发回client端。
client端是使用多线程方法编写的,经测试client开500-1000条线程去连接server端单线程多协程的服务没有任何问题。可见协程在高并发方面极具优势。