python10-协程
1,概念
-
线程:
- 正常的开发语言:多线程可以利用多核。
- cpython解释器下的多个线程不能利用多核:这本质上是规避了所有IO操作的单线程。
-
协程:
- 是操作系统不可见的。
- 协程本质就是一条线程,多个任务在一条线程上来回切换(因)以规避IO操作(果),以达到将一条线程中的IO操作降到最低的目的。
-
进程、线程、协程之间的对比:
操作 数据隔离/共享 数据是否安全 操作级别 开销 多核 进程 数据隔离 数据不安全 操作系统级别(dis.dis) 非常大 能利用多核 线程 数据共享 数据不安全 操作系统级别 开销小(几百倍) 不能利用多核 协程 数据共享(一条线程,肯定共享) 数据安全 用户级别 更小(函数切换级别) 不能利用多核 -
协程相比于线程的缺陷:
- 协程的所有切换都基于用户,只有在用户级别能够感知到的IO操作才会用协程模块做切换以进行规避,比如:socket、请求网页等。
- 但是一些和文件操作相关的IO只有操作系统能够感知到,此时只能用线程。
- 线程的感知更加细腻。
-
用户级别操作的协程有什么好处:
- 减轻了操作系统的负担。
- 一条线程如果开了多个协程,能够多争取一些时间片来被CPU执行,提高程序的效率。
2,模块
- 切换并规避IO的两个模块:
- gevent:利用了 greenlet 底层模块完成的切换+自动规避IO的功能;
- asyncio:利用了 yield 底层语法完成的切换+自动规避IO的功能。
- 基于python原生的协程概念的发展史:
- tornado:一种异步的web框架,基于yield实现。
- yield from,更好的实现协程。
- send,更好的实现协程。
- asyncio模块,基于python原生的协程的概念正式被成立。
- 特殊的在python中提供协程功能的关键字:aysnc,await。
3,gevent模块
-
带有IO操作的命令(conn.recv,使用gevent,务必将import gevent,from gevent import monkey,monkey.patch_all()三行语句放在其他所有的import语句之前)写在函数func中,然后提交func给gevent;如果主程序里没有阻塞,需要自己加上阻塞
'''例子1''' import gevent def func(): print('start func') gevent.sleep(1) print('end func') g1 = gevent.spawn(func) g2 = gevent.spawn(func) g3 = gevent.spawn(func) gevent.joinall([g1, g2, g3]) # g1.join() #必须阻塞,因为协程只有在有IO操作的时候才切换 # g2.join() # g3.join()
-
gevent.sleep与time.sleep不同;如果要想让time.sleep和gevent.sleep起到一样的效果,需要如下操作:
'''例子2''' import gevent from gevent import monkey monkey.patch_all(thread=False, select=False) import time def func(): print('start func') time.sleep(1) #与time.sleep不同,如果想要让 print('end func') g1 = gevent.spawn(func) g2 = gevent.spawn(func) g3 = gevent.spawn(func) gevent.joinall([g1, g2, g3])
-
使用协程进行一个服务端与多个客户端的通信
'''===============================server===============================''' import gevent from gevent import monkey monkey.patch_all() import socket def func(conn): while True: msg = conn.recv(1024).decode('utf-8') MSG = msg.upper() conn.send(MSG.encode('utf-8')) sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() while True: conn, _ = sk.accept() gevent.spawn(func, conn) '''===============================client===============================''' import socket import time sk = socket.socket() sk.connect(('127.0.0.1', 9000)) while True: sk.send(b'hello') msg = sk.recv(1024) print(msg) time.sleep(0.5)
-
协程的效率,起500个客户端
4核CPU 总的进程数 5个 每个进程的线程数 20个 每个线程的协程数 500个 一个四核机器开5个进程,每个进程开20个线程,每个线程开500个协程,极限可以抗50000的并发。(一般每个机器抗30000的并发)
'''===============================client===============================''' import socket import time from threading import Thread def client(): sk = socket.socket() sk.connect(('127.0.0.1', 9000)) while True: sk.send(b'hello') msg = sk.recv(1024) print(msg) time.sleep(0.5) for i in range(500): Thread(target=client).start()
-
测试gevent是否能可以对引起阻塞的操作起到协程的作用,可以进行如下判断:
import socket print(socket.socket) import gevent from gevent import monkey monkey.patch_all() import socket print(socket.socket) '''两次打印的结果相同,则gevent可以规避该IO操作;如果不同,gevent无法规避该IO操作;看patch_all源码,所有为True的都支持''' 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):
4,asyncio模块
- await
- 用在可能会发生阻塞的方法前(await后面必须跟一个IO操作)
- 必须写在一个async函数里
- 底层比较复杂,https://www.cnblogs.com/Eva-J/p/10437164.html
import asyncio
async def func(name):
print('start', name)
await asyncio.sleep(1) #await标志会在此处切走
print('end')
loop = asyncio.get_event_loop()
# loop.run_until_complete(func('a'))
loop.run_until_complete(asyncio.wait([func('a'), func('b')]))
-
原理
-
yield和next配合可以做到在执行函数的过程中从函数中切出去。
-
使用python实现的协程:
import time def sleep(n): print('start sleep') yield time.time()+n print('end sleep') def func(n): print(123) g = sleep(n) yield from g #await就相当于yield from print(456) g1 = func(2) g2 = func(1) ret1 = next(g1) ret2 = next(g2) timeDict = {ret1: g1, ret2: g2} #仅多1s就已经体现出了异步 print(timeDict) while timeDict: minTime = min(timeDict) time.sleep(minTime - time.time()) #异步,同时睡了1s try: next(timeDict[minTime]) except StopIteration: pass del timeDict[minTime]
-
行动是治愈恐惧的良药,而犹豫拖延将不断滋养恐惧。