python------协程
一、我们知道无论是创建多进程还是创建多线程池来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。
基于单线程来实现并发,这样就可以节省创建线程进程所消耗的时间。
二、如何实现在两个函数之间的切换?
def func1(): print(1) yield print(3) yield def func2(): g = func1() next(g) print(2) next(g) print(4) func2() ''' 1 2 3 4 '''
def consumer(): while True: n = yield print('消费了一个包子%s'%n) def producer(): g = consumer() next(g) for i in range(5): print('生产了包子%s'%i) g.send(i) producer() ''' 生产了包子0 消费了一个包子0 生产了包子1 消费了一个包子1 生产了包子2 消费了一个包子2 生产了包子3 消费了一个包子3 生产了包子4 消费了一个包子4 '''
import time def consumer(): '''任务1:接收数据,处理数据''' while True: x=yield def producer(): '''任务2:生产数据''' g=consumer() next(g) for i in range(10000000): g.send(i) time.sleep(2) start=time.time() producer() #并发执行,但是任务producer遇到io就会阻塞住,并不会切到该线程内的其他任务去执行 stop=time.time() print(stop-start)
对于单线程下,程序中不可避免的会出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能再一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。
三、协程
协程:是单线程下的并发,协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
协程的本质:在但线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
需要强调的是:
1:python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其它线程运行)。
2:单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换的优缺点:
优点:
1.协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级。
2.单线程内就可以实现并发的效果,最大限度地利用cpu。
缺点:
1.协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程。
2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程。
协程的特点:
1.必须在只有一个单线程里实现并发
2.修改共享数据不需加锁
3.用户程序里自己保存多个控制流的上下文栈
4.附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
四、greenlet模块
from greenlet import greenlet def eat(name): print('%s eat 1' %name) g2.switch('haha') 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('hjm')#可以在第一次switch时传入参数,以后都不需要 ''' hjm eat 1 haha play 1 hjm eat 2 haha play 2 '''
import time from greenlet import greenlet # 在单线程中切换状态的模块 def eat1(): print('吃鸡腿1') g2.switch() time.sleep(5) print('吃鸡翅2') g2.switch() def eat2(): print('吃饺子1') g1.switch() time.sleep(3) print('白切鸡') g1 = greenlet(eat1) g2 = greenlet(eat2) g1.switch() ''' 吃鸡腿1 吃饺子1 吃鸡翅2 白切鸡 '''
单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度。
#顺序执行 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)) # run time is 10.494175910949707 #切换 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)) # run time is 63.0725622177124
greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
五、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的返回值
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,'hjm') g2=gevent.spawn(play,name='hjm') g1.join() g2.join() #或者gevent.joinall([g1,g2]) print('主') ''' hjm eat 1 hjm play 1 hjm play 2 hjm eat 2 主 '''
上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
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('主')
from gevent import monkey;monkey.patch_all() import time # time socket urllib requests import gevent # greenlet gevent在切换程序的基础上又实现了规避IO def task(args): time.sleep(1) print(args) def sync_func(): # 同步 for i in range(10): task(i) def async_func(): # 异步 g_l = [] for i in range(10): g_l.append(gevent.spawn(task,i)) # 给写成任务传参数 gevent.joinall(g_l) start = time.time() sync_func() print(time.time() - start) # 10.00815486907959 start = time.time() async_func() print(time.time() - start) # 1.0002970695495605
通过gevent实现单线程下的socket并发
from gevent import monkey monkey.patch_all() import gevent import socket def talk(conn): while True: ret = conn.recv(1024).decode('utf-8') print(ret) conn.send(ret.upper().encode('utf-8')) conn.close() sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen() while True: conn,addr = sk.accept() gevent.spawn(talk,conn) sk.close()
import time import socket import threading def my_client(): sk = socket.socket() sk.connect(('127.0.0.1',8080)) while True: sk.send(b'hi') ret = sk.recv(1024).decode('utf-8') print(ret) time.sleep(1) sk.close() for i in range(500): threading.Thread(target=my_client).start()