Python协程
一、一些基本概念:
协程(Coroutine),又称微线程,纤程,一种用户级的轻量级线程。
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态。
在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源
协程需要用户自己来编写调度逻辑,对于CPU来说,协程其实是单线程,所以cpu不用去考虑怎么调度,切换上下文,这就省去了cpu的切换开销,所以协程一定程度上又好于多线程
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;
二、yield回顾:
def f(): print('ok1') count=yield 5 print(count) print('ok2') yield 6 gen=f() # ret=next(gen) # print(ret) ret=gen.send(None) print(ret) ret2=gen.send(7) #7赋值给count。 print(ret2) output: ok1 5 7 ok2 6
#__author: greg1617 #date: 2017/9/22 9:21 def consumer(name): print("--->starting eating baozi...") while True: new_baozi = yield print("[%s] is eating baozi %s" % (name, new_baozi)) # time.sleep(1) def producer(): r = con.__next__()# 等于next(con) r = con2.__next__()# 等于next(con2) n = 0 while n < 5: n += 1 con.send(n) con2.send(n) print("\033[32;1m[producer]\033[0m is making baozi %s" % n) if __name__ == '__main__': con = consumer("c1")# 创建一个生成器对象 con2 = consumer("c2") #又创建一个 p = producer() #执行producer函数,p是返回值
三 gevent
Gevent 是一个基于协程的Python网络函数库,可以轻松通过gevent实现并发同步或异步编程,对协程的支持,本质上是greenlet在实现切换工作。
在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。
Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
greenlet工作流程:加入进行访问网络的IO操作时,出现阻塞,greenlet就显式切换到另一段没有被阻塞的代码段执行,知道原先的阻塞状况消失以后,再自动切换回原来的代码段继续处理。因此,greenlet是一种合理安排的串行方式。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换携程,就保证总有greenlet在运行,而不是等待IO,这就是协程一般比多线程效率高的原因。
由于切换实在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,将一些常见的阻塞,如socket、select等地方实现协程跳转,这一过后才能在启动时通过monkey patch完成。
#__author: greg #date: 2017/9/22 11:06 import gevent,time def func1(): print('\033[31;1mAAAAAAAAAA...\033[0m',time.ctime()) gevent.sleep(2) #遇到阻塞,IO操作,自动切换 print('\033[31;1mBBBBBBBBBB...\033[0m',time.ctime()) def func2(): print('\033[32;1mCCCCCCCCCCC...\033[0m',time.ctime()) gevent.sleep(1) print('\033[32;1mDDDDDDDDDDDDDDDDD...\033[0m',time.ctime()) gevent.joinall([ gevent.spawn(func1), gevent.spawn(func2), ])
AAAAAAAAAA... Sat Nov 25 11:41:03 2017
CCCCCCCCCCC... Sat Nov 25 11:41:03 2017
DDDDDDDDDDDDDDDDD... Sat Nov 25 11:41:04 2017
BBBBBBBBBB... Sat Nov 25 11:41:05 2017
SPAWN方法可以看做用来形成协程,joinall方法就是添加这些协程任务,并且启动运行。
四 greenlet
#__author: greg1617 #date: 2017/9/22 10:58 from greenlet import greenlet #greenlet是一个用C实现的协程模块,相比与python自带的yield, # 它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator def test1(): print(1) gr2.switch() print(2) gr2.switch() def test2(): print(3) gr1.switch() print(4) gr1 = greenlet(test1) # print(gr1) #<greenlet.greenlet object at 0x00000196A3AC33D8> gr2 = greenlet(test2) gr1.switch() # 感觉确实用着比generator还简单了呢,但好像还没有解决一个问题, # 就是遇到IO操作,自动切换,对不对?
同步和异步的比较
#__author: greg #date: 2017/9/22 11:12 import gevent,time start=time.time() def task(pid): """ Some non-deterministic task """ gevent.sleep(0.5) print('Task %s done' % pid) def synchronous(): for i in range(1, 10): task(i) def asynchronous(): threads = [gevent.spawn(task, i) for i in range(10)] gevent.joinall(threads) # print('Synchronous:') # synchronous() #按顺序执行,时间4.510729074478149 # print('Asynchronous:') # asynchronous() #争先抢后的执行,时间0.5047242641448975 end=time.time() print(end-start)
遇到IO操作自动切换
#__author: greg1617 #date: 2017/9/22 11:29 from gevent import monkey import time monkey.patch_all() import gevent from urllib.request import urlopen def f(url): print('GET: %s' % url) resp = urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) start=time.time() # l=['https://www.python.org/','https://www.yahoo.com/','https://github.com/'] # for url in l: # f(url) #output: # GET: https://www.python.org/ # 48847 bytes received from https://www.python.org/. # GET: https://www.yahoo.com/ # 510810 bytes received from https://www.yahoo.com/. # GET: https://github.com/ # 51383 bytes received from https://github.com/. # 4.774596929550171 gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), gevent.spawn(f, 'https://www.yahoo.com/'), gevent.spawn(f, 'https://github.com/'), ]) #output: # GET: https://www.python.org/ # GET: https://www.yahoo.com/ # GET: https://github.com/ # 48847 bytes received from https://www.python.org/. # 513713 bytes received from https://www.yahoo.com/. # 51383 bytes received from https://github.com/. # 3.432187795639038 print(time.time()-start)
五 gevent池
在处理大量网络和IO操作时,进行并发管理,可以使用池
# -*- coding: utf-8 -*- # 2017/11/25 11:58 from gevent import monkey monkey.patch_all() import urllib.request from gevent.pool import Pool def run_task(url): print('Visit --> %s' % url) try: response = urllib.request.urlopen(url) data = response.read() print('%d bytes received from %s.' % (len(data), url)) except Exception as e: print(e) return 'url:%s --->finish'% url if __name__=='__main__': pool = Pool(2) urls = ['https://github.com/','https://www.python.org/','http://www.cnblogs.com/'] results = pool.map(run_task,urls) print(results)
六 通过gevent实现单线程下的多socket并发
服务端:
#__author: greg #date: 2017/9/22 19:43 # 通过gevent实现单线程下的多socket并发 import sys import socket import time import gevent from gevent import socket, monkey monkey.patch_all() def server(port): s = socket.socket() s.bind(('0.0.0.0', port)) s.listen(500) while True: cli, addr = s.accept() gevent.spawn(handle_request, cli) def handle_request(conn): try: while True: data = conn.recv(1024) print("recv:", data) conn.send(data) if not data: conn.shutdown(socket.SHUT_WR) except Exception as ex: print(ex) finally: conn.close() if __name__ == '__main__': server(8001)
客户端
#__author: greg #date: 2017/9/22 19:44 import socket HOST = 'localhost' # The remote host PORT = 8001 # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) while True: msg = bytes(input(">>:"), encoding="utf8") s.sendall(msg) data = s.recv(1024) # print(data) print('Received', repr(data)) s.close()