基于协程的并发 |
一、什么是协程
协程是单线程下的并发,又称微线程,纤程。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。我们需要知道的是:
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行) #2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)
协程的特点是:
- 必须在只有一个单线程里实现并发;
- 修改共享数据不需加锁;
- 用户程序里自己保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其它协程
二、Greenlet
如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦,而使用greenlet模块可以非常简单地实现这20个任务直接的切换。
#安装 pip3 install greenlet
from greenlet import greenlet def test1(): print(12) gr2.switch() #切换到gr2 print(34) gr2.switch() #切换到gr2 def test2(): print(56) gr1.switch() #切换到gr1 print(78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr2.switch() #切换到gr2
单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度。greenlet当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。如此,才能提高效率,这就用到了Gevent模块。
三、Gevent
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程。
具体使用如下:
#用法 g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的 g2=gevent.spawn(func2) g1.join() #等待g1结束 g2.join() #等待g2结束 #或者上述两步合作一步:gevent.joinall([g1,g2]) g1.value#拿到func1的返回值
遇到IO阻塞时会自动切换任务
import gevent import requests,time start = time.time() def f(url): print('GRT:%s'%url) resp = requests.get(url) data = resp.text print('%d bytes received from %s'%(len(data),url)) gevent.joinall([ gevent.spawn(f,'http://www.python.org/'), gevent.spawn(f,'http://www.yahoo.com/'), gevent.spawn(f,'http://www.baidu.com/'), gevent.spawn(f,'http://www.sina.com.cn/'), ]) # f('http://www.python.org/') # f('http://www.yahoo.com/') # f('http://www.baidu.com/') # f('http://www.sina.com.cn/') print('cost time:',time.time()-start)
注释部分是串行执行,在网速稳定情况下,明显使用gevent模块比串行执行效率高很多。
我们可以通过gevent实现单线程下的socket并发:
from gevent import monkey;monkey.patch_all() #from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞 from socket import * import gevent def server(server_ip,port): s=socket(AF_INET,SOCK_STREAM) s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) s.bind((server_ip,port)) s.listen(5) while True: conn,addr=s.accept() gevent.spawn(talk,conn,addr) def talk(conn,addr): try: while True: res=conn.recv(1024) print('client %s:%s msg: %s' %(addr[0],addr[1],res)) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server('127.0.0.1',8080)
from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if not msg:continue client.send(msg.encode('utf-8')) msg=client.recv(1024) print(msg.decode('utf-8'))
from threading import Thread from socket import * import threading def client(server_ip,port): c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了 c.connect((server_ip,port)) count=0 while True: c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8')) msg=c.recv(1024) print(msg.decode('utf-8')) count+=1 if __name__ == '__main__': for i in range(500): t=Thread(target=client,args=('127.0.0.1',8080)) t.start()