协程:英文名Coroutine,是单线程下的并发,又称微线程,纤程。chrome
协程是一种用户态的轻量级线程,即协程是由用户程序本身控制调度的。对比操做系统控制线程的切换,用户在单线程内控制协程的切换。编程
协程本身自己没法实现并发(甚至性能会下降),协程+IO切换性能提升。数组
一般程序中子程序调用老是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不一样。多线程
协程看上去也是子程序,但执行过程当中,在子程序内部可中断,而后转而执行别的子程序,在适当的时候再返回来接着执行。并发
注意,在一个子程序中中断,去执行其余子程序,不是函数调用,有点相似CPU的中断。异步
看起来A、B的执行有点像多线程,但协程的特色在因而一个线程执行,那和多线程比,协程有何优点?socket
最大的优点就是协程极高的执行效率。由于子程序切换不是线程切换,而是由程序自身控制,所以,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优点就越明显。async
第二大优点就是不须要多线程的锁机制,由于只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只须要判断状态就行了,因此执行效率比多线程高不少。异步编程
由于协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可得到极高的性能。函数
Python对协程的支持是经过generator实现的。
在generator中,咱们不但能够经过for
循环来迭代,还能够不断调用next()
函数获取由yield
语句返回的下一个值。
可是Python的yield
不但能够返回一个值,它还能够接收调用者发出的参数。
来看例子:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,经过锁机制控制队列和等待,但一不当心就可能死锁。
若是改用协程,生产者生产消息后,直接经过yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) r = '200 OK' def produce(c): c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() c = consumer() produce(c)
执行结果:
[PRODUCER] Producing 1... [CONSUMER] Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 2... [CONSUMER] Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 3... [CONSUMER] Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 4... [CONSUMER] Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 5... [CONSUMER] Consuming 5... [PRODUCER] Consumer return: 200 OK
注意到consumer
函数是一个generator
,把一个consumer
传入produce
后:
c.send(None)
启动生成器;c.send(n)
切换到consumer
执行;consumer
经过yield
拿到消息,处理,又经过yield
把结果传回;produce
拿到consumer
处理的结果,继续生产下一条消息;produce
决定不生产了,经过c.close()
关闭consumer
,整个过程结束。整个流程无锁,由一个线程执行,produce
和consumer
协做完成任务,因此称为“协程”,而非线程的抢占式多任务。
最后套用Donald Knuth的一句话总结协程的特色:“子程序就是协程的一种特例。”
若是咱们在单个线程内有20个任务,要想实如今多个任务之间切换,使用yield生成器的方式过于麻烦(须要先获得初始化一次的生成器,而后再调用send。。。很是麻烦),而使用greenlet模块能够很是简单地实现这20个任务直接的切换。
pip3 install greenlet
单纯的切换(在没有io的状况下或者没有重复开辟内存空间的操做),反而会下降程序的执行速度。
from greenlet import greenlet def eat(name): print('%s eat 1' % name) g2.switch('nick') print('%s eat 2' % name) g2.switch() def play(name): print('%s play 1' % name) g1.switch() print('%s play 2' % name) g1 = greenlet(eat) g2 = greenlet(play) g1.switch('nick') # 能够在第一次switch时传入参数,之后都不须要
greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时若是遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提高效率的问题。
单线程里的这20个任务的代码一般会既有计算操做又有阻塞操做,咱们彻底能够在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2...如此,才能提升效率,这就用到了Gevent模块。
#顺序执行 import time def f1(): res=1 for i in range(100000000): res+=i def f2(): res=1 for i in range(100000000): res*=i start=time.time() f1() f2() stop=time.time() print('run time is %s' %(stop-start)) #10.985628366470337 #切换 from greenlet import greenlet import time def f1(): res=1 for i in range(100000000): res+=i g2.switch() def f2(): res=1 for i in range(100000000): res*=i g1.switch() start=time.time() g1=greenlet(f1) g2=greenlet(f2) g1.switch() stop=time.time() print('run time is %s' %(stop-start)) # 52.763017892837524
Gevent 是一个第三方库,能够轻松实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,它是以C扩展模块形式接入Python的轻量级协程。
pip3 install 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])
:#拿到func1的返回值g1.value
import gevent
def eat(name): print('%s eat 1' %name) gevent.sleep(2) print('%s eat 2' %name) def play(name): print('%s play 1' %name) gevent.sleep(1) print('%s play 2' %name) g1=gevent.spawn(eat,'egon') g2=gevent.spawn(play,name='egon') g1.join() g2.join() # 或者gevent.joinall([g1,g2]) print('主')
上例gevent.sleep(2)
模拟的是gevent能够识别的io阻塞,而time.sleep(2)
或其余的阻塞,gevent是不能直接识别的须要用下面一行代码,打补丁,就能够识别了。
from gevent import monkey;monkey.patch_all()
必须放到被打补丁者的前面,如time,socket模块以前。或者咱们干脆记忆成:要用gevent,须要将from gevent import monkey;monkey.patch_all()
放到文件的开头。
from gevent import monkey;monkey.patch_all() import gevent import time
def eat(): print('eat food 1') time.sleep(2) print('eat food 2') def play(): print('play 1') time.sleep(1) print('play 2') g1=gevent.spawn(eat) g2=gevent.spawn(play) gevent.joinall([g1,g2]) print('主')
咱们能够用threading.current_thread().getName()
来查看每一个g1和g2,查看的结果为DummyThread-n,即假线程
from gevent import monkey;monkey.patch_all() import threading import gevent import time
def eat(): print(threading.current_thread().getName()) print('eat food 1') time.sleep(2) print('eat food 2') def play(): print(threading.current_thread().getName()) print('play 1') time.sleep(1) print('play 2') g1=gevent.spawn(eat) g2=gevent.spawn(play) gevent.joinall([g1,g2]) print('主')
from gevent import spawn,joinall,monkey;monkey.patch_all() import time
def task(pid): """ Some non-deterministic task """ time.sleep(0.5) print('Task %s done' % pid) def synchronous(): # 同步 for i in range(10): task(i) def asynchronous(): # 异步 g_l=[spawn(task,i) for i in range(10)] joinall(g_l) print('DONE') if __name__ == '__main__': print('Synchronous:') synchronous() print('Asynchronous:') asynchronous()
# 上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。 # 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数, # 后者阻塞当前流程,并执行全部给定的greenlet任务。执行流程只会在 全部greenlet执行完后才会继续向下走。
经过gevent实现单线程下的socket并发
注意:from gevent import monkey;monkey.patch_all()必定要放到导入socket模块以前,不然gevent没法识别socket的阻塞。
from gevent import monkey;monkey.patch_all() from socket import * import gevent #若是不想用money.patch_all()打补丁,能够用gevent自带的socket # from gevent import socket # s=socket.socket() 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 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()