引子---
当我们了解进程、线程的概念后,知道进程:操作系统资源分配的最小单位,线程:CPU调度的最小单位.虽然两者不断的提高了CPU利用率,但操作系统创建和销毁进程和线程的还是消耗时间,还要管理他们之间的分时切换.例如核心矛盾:当一条线程陷入阻塞后,这一整条线程就不能再做其他事情了,开启和销毁多条线程以及CPU在多条线程之间切换依然依赖操作系统 单线程能否实现并发就比较重要了!因此引出协程的概念
并发的本质---> 切换和状态保存
示例代码1 使用生成器函数 yield,实现并发的效果 def func(): for i in range(5): yield i g = func() for i in g: print('生活需要勇敢面对') 输出: 生活需要勇敢面对 生活需要勇敢面对 生活需要勇敢面对 生活需要勇敢面对 生活需要勇敢面对
# yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
再看示例2
import time def consumer(): #消费者 while True: res = yield def producer(): #生产者 g = consumer() next(g) for i in range(10000): g.send(i) start = time.time() producer() print( time.time()- start) 输出: 0.002000093460083008 yield这种切换,就已经在线程中出现了多个任务,这多个任务的切换本质上就是协程. 其中consumer 是一个协程,producer也是一个协程. 单纯的切换还是会消耗时间,但是如果能够阻塞的时候切换,并且多个程序阻塞时间共享,协程能够非常大限度的提升效率
什么是协程?
协程也叫纤程/轻型线程,基于单线程实现并发,对于操作系统来说,协程是不可见的,不需要操作系统调度,协程是程序级别的单位,由用户程序自己控制调度的.
协程的效率问题
协程的效率跟操作系统本身没有关系,和线程也没有关系,而是看程序的调度是否合理.
协程指的只是在同一条线程上,能够互相切换的多个任务,遇到I/O就切换,实际上我们利用协程提高工作效率的一种方式!
操作系统切换和程序切换对比
- 线程是属于内核级别的,由操作系统控制调度,如果单线程I/O后执行时间过长,就会被迫交出CPU使用权限,切换到其他线程使用
- 单线程内开启协程,一旦遇到I/O,就会从应用程序级别进行切换,以此来提升效率,但是非I/O操作的切换与效率无关.
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
协程的优缺点
优点: 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级,
单线程内就可以使用并发的效果,最大限度的利用CPU
缺点: 协程的本质是单线程下,无法利用多核,
协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程.
协程的特点:
1.必须在单线程下实现并发
2.一条协程在多个任务之间相互切换
3.数据是安全的,不能利用多核
4.能够规避一个线程上的I/O阻塞,提高了CPU的利用率
Greenlet模块
美 /'grinlɪt/ 协程模块,多个任务之间来回切换
import time from greenlet import greenlet # 美 /'grinlɪt/ def play(): print('start play') g2.switch() #开关 只负责切换,无法规避IO time.sleep(1) #遇到阻塞后切换g2 print('end play') def study(): print('start study') time.sleep(1) #遇到阻塞后再执行g1 print('end study?') g1.switch() g1 = greenlet(play) g2 = greenlet(study) g1.switch() 输出 start play start study end study? end play #还是得等待2s
Gevent模块
gevent模块是基于greenlet模块实现的,多个任务交给gevent管理,遇到I/O就使用greenlet进行切换
示例1:
import gevent def play(): 协程1 print('play start') gevent.sleep(1) print('end play') def eat():协程2 print('start eat') gevent.sleep(1) print('end eat') g1 = gevent.spawn(play) #遇到io立即切换 g2 = gevent.spawn(eat) # 美 /spɔn/ 'b'
#主线程 # gevent.sleep(2) #如果无法确定协程阻塞时间,因此我们一般用join g1.join() #精准的控制协程任务,一定是执行完毕后,join立即结束阻塞 g2.join() gevent.joinable([g1,g2]) #简单写法
输出: play start start eat end play end eat
示例2:
from gevent import monkey;monkey.patch_all() # monkey的存在就是把以下引用的模块中所有的io阻塞都打成一个包,然后gevent就可以识别这些阻塞时间啦 import time,gevent def play(): # 协程1 print('start play') time.sleep(1) #不用gevent.sleep print('end play') def run(): # 协程2 print('start run') time.sleep(1) #monkey处理后time.sleep阻塞 可以被gevent识别 print('end run') g1 = gevent.spawn(play) g2 = gevent.spawn(run) gevent.joinall([g1,g2])
示例3
#server端 from gevent import monkey;monkey.patch_all() import socket import gevent def talk(conn): while True: msg = conn.recv(1024).decode() conn.send(msg.upper().encode()) sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() while True: conn,addr = sk.accept() gevent.spawn(talk,conn) # client端 import socket import threading def task(): sk = socket.socket() sk.connect(('127.0.0.1',9000)) while True: sk.send(b'hello') print(sk.recv(1024)) for i in range(500): threading.Thread(target=task).start()
鉴于一条线程能够支持起500个协程,比如我们4C的机器开5个进程,每条进程20条线程,并发50000条并发是完全没问题的!
...