协程的定义和原理:
- 协程,又叫做纤程、微线程。协程的本质上是一个单线程程序,所以协程不能够使用计算机多核资源(但是可以利用多进程+协程实现利用多核资源)。通过记录应用层的上下文栈区,实现在运行中进行上下文跳转,达到可以选择性地运行想要运行的部分,以此提高程序的运行效率。
协程的作用:
- 协程能够高效的完成并发任务,占用较少的资源,因此协程的并发量较高。
协程的优点:
- 消耗资源少
- 无需切换,节约系统开销
- 无需同步互斥
- IO并发性好
协程相对于多线程的优势:
- 协程具有极高的执行效率。因为协程本身就是一个线程程序,所以协程中的切换是子程序切换而不是线程切换,子程序切换是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
- 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
协程的缺点:
- 无法利用计算机多核(但是可以利用多进程+协程实现利用多核资源)
注意:
- 协程是一个线程执行,针对上面提的协程的缺点,如果要利用计算机多核,可以使用多进程+协程,充分利用多核,发挥协程的高效率,又获得极高的性能。
Python中通常使用greenlet和gevent这两个模块实现协程。
greenlet
主要方法:
greenlet.greenlet()
- 功能:生成协程对象
gr.switch()
- 功能:选择要执行的协程事件
代码实现:
from greenlet import greenlet
def test1():
print("111")
# 执行协程g2
g2.switch()
print("222")
g2.switch()
def test2():
print("333")
g1.switch()
print("444")
#协程对象
g1 = greenlet(test1)
g2 = greenlet(test2)
# 首先执行协程g1
g1.switch()
运行结果:
gk@gk-vm:~/python/test$ python3 greenlet_test.py
111
333
222
444
观察运行结果可以发现两个协程内部发生了相互调用,达到了子程序的相互切换。
gevent
主要方法:
gevent.spawn(func,argv)
- 功能:生成协程对象
- 参数:func(协程函数),argv(给协程函数传参)
- 返回值:返回协程对象
gevent.joinall()
- 功能:回收协程
- 参数:列表,将要回收的协程放入列表
gevent.sleep(n)
- 功能:设置协程阻塞,让协程跳转
- 参数:n(阻塞时间)
代码实现:
import gevent
from time import sleep
def test1():
print("test1第一次运行")
# 阻塞自动跳转执行test2
gevent.sleep(1)
print("test1第二次运行")
def test2():
print("test2第一次运行")
# 阻塞自动跳转执行test1
gevent.sleep(2)
print("test2第二次运行")
#生成协程
g1 = gevent.spawn(test1)
g2 = gevent.spawn(test2)
# 回收协程
gevent.joinall([g1,g2])
print("结束")
运行结果:
gk@gk-vm:~/python/test$ python3 gevent_test.py
test1第一次运行
test2第一次运行
test1第二次运行
test2第二次运行
结束
另外:
上面的gevent.sleep()可以说只是一种模拟的阻塞,在实际的开发中,如果发生IO阻塞,gevent通常会自动切换。
代码实现(模拟请求5个网站,请求网站通常都会产生阻塞):
from gevent import monkey; monkey.patch_all()
import requests
import gevent
def get_url(url):
print("开始请求url:", url)
res = requests.get(url)
print(url, "请求完毕")
g1 = gevent.spawn(get_url, "https://www.baidu.com/")
g2 = gevent.spawn(get_url, "https://www.csdn.net/")
g3 = gevent.spawn(get_url, "https://www.bilibili.com/")
g4 = gevent.spawn(get_url, "https://www.weibo.com/")
g5 = gevent.spawn(get_url, "https://www.dongqiudi.com/")
gevent.joinall([g1, g2, g3, g4, g5])
注意代码的第一行,由于gevent在切换IO操作(文件IO、网络IO)时是自动完成的,所以gevent需要通过修改Python自带的一些阻塞式系统调用的标准库,包括socket、ssl、threading和 select等模块,而变为协程,这一过程需要在启动时通过monkey patch完成。如果没有第一行代码,将无法实现协程的自动切换。
运行结果:
gk@gk-vm:~/python/test$ python3 gevent_test2.py
开始请求url: https://www.baidu.com/
开始请求url: https://www.csdn.net/
开始请求url: https://www.bilibili.com/
开始请求url: https://www.weibo.com/
开始请求url: https://www.dongqiudi.com/
https://www.baidu.com/ 请求完毕
https://www.bilibili.com/ 请求完毕
https://www.dongqiudi.com/ 请求完毕
https://www.weibo.com/ 请求完毕
https://www.csdn.net/ 请求完毕
运行可以发现对于这种网络IO操作,使用协程效率真的非常高,多线程也适用于这种网络IO操作,但是协程却避免了多线程的线程间相互切换的消耗,协程比多线程具有更高的效率。