协程基础知识
概念
协程也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,就是通过一个线程实现代码块相互切换。
协程与线程的不同
协程不是计算机提供的,而是程序员人为创造的。线程和进程是由计算机操作系统提供的。
协程与多线程的区别:
多线程运行时:保证只有一个线程在运行,其他线程等待调度。这是python GIL锁导致的,会浪费一些资源开销。
协程运行时:在一个线程中运行多个任务,任务与任务之间来回切换,并在同一时间内只能运行一个任务。节省资源开销。
协程实现
协程实现有四种方法:
greenlet:早期第三方模块,用于实现协程代码(gevent协程就是基于greenlet实现)
yield:生成器,借助生成器的特点也可以实现协程代码。
asyncio装饰器:在Python3.4中引入的模块用于编写协程代码。
async & awiat:在Python3.5中引入的两个关键字,结合asyncio模块可以更方便的编写协程代码。
第一种:greentlet
使用greentlet来实现协程并发,是早期的实现方式。
from greenlet import greenlet
def study():
print(1)
# 3.切到g2函数中
g2.switch()
print(2)
# 3.切到g2函数中
g2.switch()
def eat():
print(3)
# 3.切到g1函数中
g1.switch()
print(4)
# 1.写入需要并发的函数名
g1 = greenlet(study)
g2 = greenlet(eat)
# 2.开启程序
g1.switch()
'''
弊端:需要自己手动去切
运行结果:
1
3
2
4
'''
gevent是对greentlet的进一步封装,实现了遇到阻塞自动切换。
import gevent
def funa(n):
for i in range(n):
print(gevent.getcurrent(),i) # 返回当前正在执行的greenlet协程
gevent.sleep(1) # 遇到io阻塞,自动切换
# 1.写入需要并发的函数名
g1 = gevent.spawn(funa,2)
g2 = gevent.spawn(funa,2)
g3 = gevent.spawn(funa,2)
# 2.开启程序
g1.join()
g2.join()
g3.join()
'''
运行结果:
<Greenlet at 0x2c0544017b0: funa(2)> 0
<Greenlet at 0x2c07580c590: funa(2)> 0
<Greenlet at 0x2c07580c7b0: funa(2)> 0
<Greenlet at 0x2c0544017b0: funa(2)> 1
<Greenlet at 0x2c07580c590: funa(2)> 1
<Greenlet at 0x2c07580c7b0: funa(2)> 1
'''
gevent当遇到gevent.sleep(1)才会识别为阻塞,遇到time.sleep(1)并不会识别为阻塞。
如果想让time模块能被识别为io阻塞,就需要打补丁。
import gevent
import time
from gevent import monkey
monkey.patch_all() # 打补丁
def task(name):
for i in range(3):
print(f'我的名字是{name},当前任务号{i}')
time.sleep(1) # 使时间模块能被gevent识别为io阻塞----实现自动切换
gevent.joinall([
gevent.spawn(task,'Wilia'),
gevent.spawn(task,'维佳')
])
'''
运行结果:
我的名字是Wilia,当前任务号0
我的名字是维佳,当前任务号0
我的名字是Wilia,当前任务号1
我的名字是维佳,当前任务号1
我的名字是Wilia,当前任务号2
我的名字是维佳,当前任务号2
'''
第二种:yield(了解)
基于Python生成器的yield和yield form关键字实现协程代码。
def func1():
yield 1
yield from func2()
yield 2
def func2():
yield 3
# yield from func1()
yield 4
f1 = func1()
for item in f1:
print(item)
'''
弊端:1.函数一切换到函数二之后不能手动的在函数二中切换到函数一 2.不能实现自动切换
运行结果:
1
3
4
2
'''
第三种:asyncio装饰器
遇到IO阻塞自动切换
import asyncio
@asyncio.coroutine
def func1():
print(1)
# 网络IO请求:下载一张图片
yield from asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(2)
@asyncio.coroutine
def func2():
print(3)
# 网络IO请求:下载一张图片
yield from asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(4)
'''
将协程对象交给future,将当前函数对象转为一个task对象。
如果asyncio遇到了task对象 则将task对象作为并发进行执行。
如果想用协程完成并发,则必须将普通协程对象转为一个task对象,
因为协程对象默认是同步执行,转为task对象才是异步执行。
'''
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
# 创建了一个容器(用来存放task任务对象) 事件循环
'''
事件循环是协程的一个重要的组成部分,将需要运行的任务由事件循环调度。
事件循环只能运行 --> task(任务)对象、asyncio对象、协程对象
因此这里要把列表转换为asyncio对象
'''
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
'''
弊端:当前的任务切换不是切换到某一个具体的任务
厉害之处:遇到IO阻塞自动切换
运行结果:
1
3
2
4
'''
第四种:async & awiat(主流)
import asyncio
# 创建协程函数
async def func1():
print(1)
# 网络IO请求:下载一张图片
await asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(2)
# 创建协程函数
async def func2():
print(3)
# 网络IO请求:下载一张图片
await asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(4)
'''
1.如果一个函数被async声明,则当前函数是一个协程函数对象
2.协程函数对象不能被直接调用
3.如果想要去运行协程函数,我们需要借助事件循环去运行当前的协程函数
4.await 是协程中的一个关键字 作用是:等待一些耗时任务并拿到这些任务的返回值后才会解堵塞
5.事件循环只能执行可以被等待的对象:协程对象、asyncio对象、task对象
'''
tasks = [
# 将协程对象转为一个task对象用来进行并发执行
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
# 创建完task对象之后需要借助事件循环去运行task对象
loop = asyncio.get_event_loop()
# asyncio.wait():把不同对象转换为asyncio对象
loop.run_until_complete(asyncio.wait(tasks))
'''
运行结果:
1
3
2
4
'''
进程,线程,协程总结
从资源消耗来看
切换需要的资源最大的:进程
切换需要的资源一般的:线程
切换需要的资源最小的:协程
从使用场景来看
多进程:CPU密集型操作(如计算密集型)
多线程:IO密集型(读写数据操作比较多的),如涉及到网络、磁盘IO的任务都是IO密集型
协程:IO阻塞且需要大量并发的场景
一般来说当遇到CPU+IO密集型时常采用:多进程+协程
从资源竞争来看
进程之间不共享全局变量,线程、协程之间共享全局变量,但要注意资源竞争的问题
联系
一个进程可以拥有多个线程
一个线程可以拥有多个协程
多线程、多进程都是同步机制,而协程则是异步机制
下篇文章我们来一起探讨一下什么是异步。
(剧透:异步是现象,协程是实现异步现象的方式)