协程
协程产生背景
进程、线程存在的问题
进程是资源分配的最小单位,线程是CPU调度的最小单位。但无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。
cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制):
- 一种情况是该任务发生了阻塞
- 另外一种情况是该任务计算的时间过长
在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态 :
- 其中第二种情况并不能提升效率,只是为了让cpu能够雨露均沾,实现看起来所有任务都被“同时”执行的效果,如果多个任务都是纯计算的,这种切换反而会降低效率。
- 第一种情况的切换。在任务一遇到io情况下,切到任务二去执行,这样就可以利用任务一阻塞的时间完成任务二的计算,效率的提升就在于此。
基于yield验证对于纯计算任务,串行与并发效率对比:
yield: 1) yield可以存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级。 2) send可以把一个函数的结果传给另一个函数,以此实现单线程内程序之间的切换。
import time # 串行执行 def consumer(res): pass def producer(): res = [] for i in range(1000000): res.append(i) return res start = time.time() res = producer() consumer(res) stop = time.time() print(stop - start) # 0.13910341262817383 # 基于yield并发执行 def consumer(): while 1: x = yield def producer(): g = consumer() next(g) for i in range(1000000): g.send(i) start = time.time() producer() stop = time.time() print(stop - start) # 0.14209675788879395
import time def consumer(): while 1: x = yield def producer(): g = consumer() next(g) for i in range(2): g.send(i) time.sleep(2) start = time.time() producer() stop = time.time() print(stop - start) # 4.000760316848755
基于单线程实现并发
随着我们对于效率的追求不断提高,基于单线程来实现并发(并发本质:切换+保存状态)又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。
在单线程下,程序中不可避免会出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:
- 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行
- 可以检测IO操作,在遇到IO操作的情况下才发生切换
协程
定义
协程:Coroutine,单线程下的并发,又称微线程,纤程。是一种用户态的轻量级线程,即协程是用户程序自己控制调度的
优点:
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点:
- 协程的开启是在单线程下,无法利用多核。只能一个程序开启多个进程,每个进程开启多个线程,每个线程开启多个协程
- 协程的开启是在单线程下,因而一旦协程出现阻塞,将会阻塞整个线程
协程与线程区别
- 线程属于内核级别的,即由操作系统控制调度(如单线程遇到IO或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
- 协程属于应用程序级别的,一旦遇到IO,就会从应用程序级别(而非操作系统)控制调度,以此减小线程间切换时间来提升效率
特点:
- 必须在只有一个单线程时实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其他协程(检测IO,yield、greenlet都无法实现,gevent模块(select机制)能实现)
- 一个线程一般最大可以开启500个协程
greenlet模块
greenlet
- 安装:pip install greenlet
基本使用
1. 创建greenlet: xxx = greenlet.greenlet(func) 当创建一个greenlet时,首先初始化一个空的栈,switch到这个栈时会运行在greenlet构造时传入的函数 2.切换协程:xxx.switch() 表示切换到xxx, 此时该协程会被挂起,等到切换回来。当一个协程对应函数执行完毕,那么这个协程就变成dead状态。
import greenlet def eat(name): print("%s eat 1" % name) g2.switch("egon") print("%s eat 2" % name) def play(name): print("%s play 1" % name) g1.switch() # 没有切换回来,一直被挂起 print("%s play 2" % name) # 不打印 g1 = greenlet.greenlet(eat) g2 = greenlet.greenlet(play) g1.switch("egon") # 可以在第一次switch时传入参数,以后都不需要 结果: egon eat 1 egon play 1 egon eat 2
greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
gevent模块
gevent
gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
g1=gevent.spawn(func,1,,2,3,x=4,y=5) 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的 g1.join() :等待g1结束 g2.join() :等待g2结束 gevent.joinall([g1,g2]):等待g1,g2结束 g1.value:拿到func1的返回值 gevent.sleep(2):gevent可以识别的阻塞 而time.sleep(2)或其他的阻塞,gevent无法识别,需要在import time,socket等前加上补丁:from gevent import monkey,monkey.patch_all()
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, "egon") gevent.joinall([g1, g2]) print("主线程代码结束")
识别time.sleep()
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, 查看结果为Dummy Thread-n,即假线程
from gevent import monkey; monkey.patch_all() import gevent import time import threading 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("主线程代码结束") 结果: DummyThread-1 eat food 1 DummyThread-2 play 1 play 2 eat food 2 主线程代码结束
gevent之同步与异步
gevent应用——爬虫
from gevent import monkey; monkey.patch_all() import gevent import requests import time def get_page(url): print("GET:%s" % url) response = requests.get(url) if response.status_code == 200: print("%d bytes received from %s" % (len(response.text), url)) if __name__ == '__main__': # 并发 start_time = time.time() gevent.joinall([ gevent.spawn(get_page, "https://www.python.org/"), gevent.spawn(get_page, "https://www.yahoo.com/"), gevent.spawn(get_page, "https://github.com/"), ]) stop_time = time.time() print("run time is %s" % (stop_time - start_time)) # 串行 print("-----------------------------------------") s = time.time() requests.get("https://www.python.org/") requests.get("https://www.yahoo.com/") requests.get("https://github.com/") t = time.time() print("串行时间>>", t-s)
gevent应用——单线程下的socket并发
gevent无法识别socket的阻塞,需在import socket前添加补丁:from gevent import monkey; monkey.patch_all(),也可使用gevent自带的socket
import gevent from gevent import socket def server(ip, port): s = socket.socket() s.bind((ip, port)) s.listen() while 1: conn, addr = s.accept() gevent.spawn(talk, conn, addr) def talk(conn, addr): try: while 1: 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", 8001)
import socket client = socket.socket() client.connect(("127.0.0.1", 8001)) while 1: msg = input("<<<:").strip() if not msg: continue client.send(msg.encode("utf-8")) print(">>>", client.recv(1024).decode("utf-8"))
from threading import Thread import socket import threading def client(server_ip, port): c = socket.socket() c.connect((server_ip, port)) count = 0 while 1: c.send(("%s say hello %s" % (threading.current_thread().getName(), count)).encode("utf-8")) print(">>>", c.recv(1024).decode("utf-8")) count += 1 if __name__ == '__main__': for i in range(500): t = Thread(target=client, args=("127.0.0.1", 8001)) t.start()