什么是协程?
协程(Coroutine)是一种用于并发编程的技术,它允许在单个线程内实现多任务调度。协程可以在执行过程中暂停,并在需要时恢复执行,这使得它们非常适合处理I/O密集型任务,如网络请求、文件读写等。
- 非抢占式调度:协程由程序显式地控制切换点,不像线程那样由操作系统调度。这意味着你可以精确控制何时暂停和恢复一个协程。
- 轻量级:由于不需要创建新的线程或进程,协程的开销更小。
- 单线程内并发:所有的任务都运行在一个单独的主线程中,因此避免了多线程编程中的一些复杂性,如竞争条件和死锁。
协程的实现
在 Python 中,协程的实现有多种,其中主要依赖于生成器和 asyncio
库。关于asyncio的具体用法本章仅做实现示例,下章再详细拆解。
greenlet实现
pip3 install greenlet
from greenlet import greenlet
def func1():
print(1)
gr2.switch()
print(2)
gr2.switch()
def func2():
print(3)
gr1.switch()
print(4)
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch()
结果:
1
3
2
4
- 定义两个函数
func1()
和func2()
,每个函数内部都有多个步骤,并且在某些步骤之后会调用对方的 switch 方法来进行上下文切换。 - 使用
greenlet()
函数分别将这两个函数包装成 Greenlets:gr1
和gr2
. - 最后,通过调用
gr1.switch()
开始执行第一个任务,这会导致程序在不同任务之间来回切换。
生成器实现
什么是yield关键字?
yield
关键字用于定义一个生成器(generator)函数。生成器是一种特殊的迭代器,它允许你逐个产生值,而不是一次性创建并返回一个包含所有值的列表。使用 yield
的函数在执行过程中可以多次 yield
值,每次 yield
都会返回一个值,并在下一次从该点继续执行。
yield
是 Python 中实现协程和异步编程的基础,特别是在 Python 3.3 之前,当时的 asyncio
库依赖于生成器来实现协程。在 Python 3.5 及以后版本中,引入了 async
和 await
语法,提供了更高级的异步编程能力,但 yield
仍然是 Python 编程中一个非常有用的关键字。
- 惰性计算:生成器在调用时不会立即计算所有值,而是在每次迭代时计算并产生下一个值。
- 状态保存:当生成器函数
yield
一个值时,它的所有本地变量和当前的执行状态都会被保存,以便下次从该点继续执行。 - 逐个产生:生成器一次产生一个值,这使得它们在处理大量数据时非常高效,因为不需要一次性将所有数据加载到内存中。
- 可迭代对象:生成器本身是可迭代对象,可以被
for
循环或next()
函数迭代。
def count_up_to(num):
"""
`count_up_to` 函数是一个生成器,它在每次迭代时 `yield` 当前的 `count` 值,并在下一次迭代时从上次停下的地方继续执行。
不使用yield的话,使用这段代码可以达到同样的效果:
result = list()
count = 1
while count <= num:
result.append(count)
count += 1
return result
"""
count = 1
while count <= num:
yield count
count += 1
# 使用生成器
for number in count_up_to(5):
print(number)
print("分割线".center(30, "="))
def chain_generators(*gens):
"""
yield from 允许你将一个生成器的输出委托给另一个生成器,简化了生成器的嵌套调用。
如果不使用yield from来简化:
for gen in gens:
# 迭代每个生成器对象
for value in gen:
# 逐个 yield 生成器的值
yield value
"""
for gen in gens:
yield from gen
for number in chain_generators(count_up_to(2), count_up_to(3)):
print(number)
结果输出:
1
2
3
4
5
=============分割线==============
1
2
1
2
3
实现协程示例
def func1():
yield 1
yield from func2()
yield 2
def func2():
yield 3
yield 4
f1 = func1()
for item in f1:
print(item)
结果
1
3
4
2
- 首先,
f1
开始执行,yield 1
被执行,生成器产生值1
。 - 然后,
yield from func2()
被执行。func2
产生第一个值3
,f1
将其传递给迭代器,所以3
被打印出来。 func2
继续产生第二个值4
,f1
同样将其传递给迭代器,所以4
也被打印出来。func2
没有更多的yield
语句,所以yield from
表达式完成,控制权回到f1
。f1
继续执行,yield 2
被执行,生成器产生值2
。
asyncio实现
旧版本实现
asyncio
是 Python 的一个标准库,用于编写单线程并发代码。
在python3.4及之后,asyncio
库可以使用 @asyncio.coroutine
装饰器来定义协程。这个装饰器是编写协程的早期方法,它允许你在函数定义之前添加 @asyncio.coroutine
,以便使用 yield from
来暂停和恢复协程的执行。
import asyncio
# 在3.8版本后这种装饰器的写法就废弃了 ,让你用async def来定义函数替代。运行会提示警告:
# DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
@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)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
输出:
1
3
2
4
- 首先,它将运行
func1
,直到遇到asyncio.sleep(2)
。 - 由于
asyncio.sleep(2)
是一个异步等待调用,事件循环将挂起func1
并检查是否有其他任务可以运行。 - 然后,事件循环切换到
func2
并执行它,直到遇到asyncio.sleep(2)
,同样被挂起。 - 事件循环再次检查任务队列,发现没有其他任务可以运行,将等待一段时间(由
asyncio.sleep
的参数决定)。 - 2 秒后,
func1
和func2
中的asyncio.sleep
都完成,事件循环将恢复它们的执行。
新版本实现
在3.7版本后,推荐使用 async def
定义协程,并用 await
来等待异步操作。
import asyncio
async def func1():
"""
相比上面的使用装饰器,推荐新的写法:
将装饰器@asyncio.coroutine去掉,def方法加上async前缀
yield from 改为使用await
"""
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)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
协程与多线程、多进程的区别联系
协程(Coroutines)
协程是一种程序组件,能够在等待操作完成时挂起执行,而无需阻塞整个线程。它们通常用于 I/O 密集型任务,因为 I/O 操作(如网络请求、文件读写)通常涉及等待。协程的优点包括:
- 轻量级:协程的创建和切换开销远小于线程。
- 非抢占式:协程的执行顺序由程序逻辑控制,而不是由操作系统强制抢占。
- 易于编写:使用
async/await
语法可以使异步代码的逻辑更清晰。
Python 中的 asyncio
库提供了对协程的支持。
多线程(Multithreading)
多线程允许在同一时间内在同一个进程中并行运行多个线程。线程共享进程的内存和资源,这使得线程间通信和数据共享变得容易,但也引入了同步和竞态条件的问题。多线程的优点包括:
- 资源共享:线程可以共享进程的内存和文件描述符等资源。
- 简化编程:线程的创建和管理相对简单。
- 适用性:适合计算密集型和 I/O 密集型任务。
Python 的 threading
模块提供了多线程的支持。
多进程(Multiprocessing)
多进程是指在操作系统级别上同时运行多个进程。每个进程有自己的内存空间,这意味着进程间通信需要通过明确的 IPC(进程间通信)机制。多进程的优点包括:
- 隔离性:进程间相互独立,一个进程的崩溃不会直接影响到其他进程。
- 资源分配:操作系统可以有效地管理不同进程的资源分配。
- CPU 密集型任务:由于每个进程有自己的内存空间,多进程适合处理 CPU 密集型任务。
Python 的 multiprocessing
模块提供了多进程的支持。
区别与联系
- 资源占用:协程是单线程内的并发,资源占用最少;多线程共享进程资源,资源占用适中;多进程有最大的资源占用。
- 上下文切换开销:协程上下文切换的开销最小;多线程次之;多进程的上下文切换开销最大。
- 编程复杂度:协程编程模型最简单;多线程需要处理线程安全和同步问题;多进程由于进程间隔离,编程复杂度介于协程和多线程之间。
- 适用场景:协程适合 I/O 密集型任务;多线程适合计算和 I/O 混合型任务;多进程适合 CPU 密集型任务和需要高隔离性的场景。