协程
概念
协程(Coroutine),又称微线程、纤程。
协程的作用是,在执行函数A时可以随时中断去执行函数B,然后中断继续执行函数A(可以自由切换);但这一过程并不是函数调用(没有调用语句)。
这一整个过程看似像多线程,然而协程只有一个线程执行。
优势
- 执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
- 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。
说明:
协程可以处理 IO 密集型程序的效率,但是处理 CPU 密集型不是它的长处
要充分发挥 CPU 利用率可以结合 多进程 + 协程 。
实现方法
生成器(yield)实现
def job():
for i in range(10):
print(i)
yield 'result:%s' % (i)
# 函数里包含yield关键字,调用函数返回的是生成器对象
j = job()
print(j)
# yield工作原理:如果遇到yield就停止运行,调用next方法,从yield停止的地方继续运行
res = next(j)
# 输出yield的返回结果
print(res)
# 生成器可迭代
for i in j:
print(i)
gevent 实现
gevent 是第三方库,通过greenlet实现协程。
基本思想:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
使用说明:
- monkey:可以使一些阻塞的模块变得不阻塞
----> 机制:遇到IO操作则自动切换,手动切换可以用gevent.sleep( )或者yield - gevent.sleep(0) (将爬虫代码换成这个,效果一样可以达到切换上下文)
- gevent.spawn 启动协程(参数为函数名称、参数名称)
- gevent.joinall 等待所有的协程执行结束
- gevent.join()
from gevent import monkey
import gevent
import threading
# 可以使一些阻塞的模块变得不阻塞,修改python内置的标准库
monkey.patch_all()
def job(n):
for i in range(n):
print(gevent.getcurrent(), i, n)
gevent.sleep(1)
def main():
# 创建协程, 分配任务
g1 = gevent.spawn(job, 1)
g2 = gevent.spawn(job, 2)
g3 = gevent.spawn(job, 3)
# g1.join()
# g2.join()
# g3.join()
print("当前线程数:", threading.active_count())
gevent.joinall([g1, g2, g3])
print('任务执行结束......')
if __name__ == '__main__':
main()
案例(与多线程相比)
import threading
import gevent
from gevent import monkey
from urllib.error import HTTPError
from urllib.request import urlopen
from mytimeit import timeit
monkey.patch_all()
def get_page_length(url):
try:
urlObj = urlopen(url)
except HTTPError as e:
print('捕获失败......')
else:
pageContent = urlObj.read()
return len(pageContent)
# 做实验, 访问本地url, 因为获取网络的url与网速有关(网速不稳定)
urls = ['file:///usr/share/doc/HTML/en-US/index.html','file:///usr/share/doc/HTML/en-US/index.html',
'file:///usr/share/doc/HTML/en-US/index.html','file:///usr/share/doc/HTML/en-US/index.html'] * 10000
@timeit
def use_gevent():
gevents = [gevent.spawn(get_page_length, url) for url in urls]
gevent.joinall(gevents)
print('协程执行结束......')
@timeit
def use_thread():
threads = []
for url in urls:
t = threading.Thread(target=get_page_length, args=(url,))
t.start()
threads.append(t)
[thread.join() for thread in threads]
print('线程程执行结束......')
if __name__ == '__main__':
use_gevent()
use_thread()