对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,
这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,
让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
协程
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
优点:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级 2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点:
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程 2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
协程特点:
-
必须在只有一个单线程里实现并发
-
修改共享数据不需加锁
-
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:
#1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。 #2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换
协程实例:
# 生产者消费者模型 # def consumer(): # g = producer() # for num in g: # print(num) # # # def producer(): # for i in range(100): # yield i # 保存当前程序的状态 # # consumer()
更好的利用协程 一个线程的执行明确的切分开 两个任务, 帮助你记住哪个任务执行到哪个位置上了, 并且实现安全的切换 一个任务不得不陷入阻塞了, 在这个任务阻塞的过程中切换到另一个任务继续执行 你的程序只要还有任务需要执行, 你的当前线程永远不会阻塞 利用协程在多个任务陷入阻塞的时候进行切换, 来保证一个线程在处理多个任务的时候总是忙碌的,这样能够更加充分的利用CPU,来抢占更多的时间片 无论是进程、还是线程都是由操作系统来切换的,开启过多的线程会给操作系统的调度带来更大的压力 如果我们是使用协程,协程在程序之间的切换操作系统感知不到。无论开启多少个协程对操作系统的调度不会由任何压力。 协程的本质就是一条线程,所有完全不会产生数据安全问题。
Greenlet模块
greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator(生成器)
# import time # from greenlet import greenlet # def eat(): # print('eating 1') # g2.switch() # 切换到play # time.sleep(1) # print('eating 2') # # def play(): # print('playing 1') # time.sleep(1) # print('playing 2') # g1.switch() # 切换回eat # # g1 = greenlet(eat) # g2 = greenlet(play) # g1.switch()
上例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 time import gevent def eat(): print('eating 1') # gevent.sleep(1) time.sleep(1) print('eating 2') def play(): print('playing 1') # gevent.sleep(1) time.sleep(1) print('playing 2') g1 = gevent.spawn(eat) # 自动的检测阻塞事件,遇见阻塞了就会进行切换,有些阻塞它不认识。。 g2 = gevent.spawn(play) g1.join() # 阻塞直到g1结束 g2.join()
Gevent模块
gevent 是一个第三方库,通过greenlet实现协程,核心就是在遇到IO操作,会自动切换状态
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执行完后才会继续向下走。
from gevent import monkey; monkey.patch_all() # 导入其他模块 带有一些阻塞事件 # spawn(函数名) 产生了一个协程任务,在遇到IO操作的时候帮助我们在多任务之间自动切换 # join() 阻塞 直到某个任务被执行完毕 # join_all() # value 属性 获取返回值
# from gevent import monkey # monkey.patch_all() # 把下面导入的模块打包进来 # import time # import gevent # # # def eat(): # print('eating 1') # # gevent.sleep(1) # time.sleep(1) # print('eating 2') # return 'eat finished' # # def play(): # print('playing 1') # # gevent.sleep(1) # time.sleep(1) # print('playing 2') # return 'play finished' # g1 = gevent.spawn(eat) # 自动的检测阻塞事件,遇见阻塞了就会进行切换,有些阻塞它不认识。。 # g2 = gevent.spawn(play) # gevent.joinall([g1, g2]) # print(g1.value) # print(g2.value)
# 爬虫 from gevent import monkey; monkey.patch_all() import time import requests url_lst = [ 'https://www.baidu.com', 'https://www.zhihu.com/topics', 'https://www.sogou.com', 'https://www.sohu.com', 'https://www.sina.com', 'https://www.jd.com', 'https://www.luffycity.com/home', 'https://www.douban.com', ] def get_url(url): response = requests.get(url) if response.status_code == 200: # 状态码 200正常结果 print(url, len(response.text)) start = time.time() for url in url_lst: get_url(url) print(time.time()-start)
通过gevent实现单线程下的socket并发
注意 :from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞
from gevent import monkey;monkey.patch_all() import socket import gevent from threading import current_thread def talk(conn): print('-->',current_thread()) while True: conn.send(b'hello') # print(conn.recv(1024)) sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() while True: conn, addr = sk.accept() gevent.spawn(talk, conn) # 主线程 accept 基本上都是阻塞 # 任务一 talk # 任务二 talk # 任务三 talk
import socket from threading import Thread def client(): sk = socket.socket() sk.connect(('127.0.0.1', 9000)) while True: print(sk.recv(1024)) sk.send(b'byebye') for i in range(10): Thread(target=client,).start()