0.概述
在Python中,常常涉及进程、线程与协程的概念。本文档对其做基本的记录与说明。
1.概念引入
我们都熟悉函数,也称为子程序, 程序, 子进程等。函数是打包为一个单元来执行特定任务的一系列指令。当一个复杂函数的逻辑被划分为几个独立的步骤,这些步骤本身就是函数时,这些函数被称为辅助函数或子程序。
Python 中的subroutine (子例程),由负责协调使用这些子例程的 main 函数调用。子例程具有单个入口点。
协程是子例程的泛化。它们用于协作多任务处理,其中进程自愿定期或在空闲时放弃(放弃)控制权,以便同时运行多个应用程序。协程和子例程的区别是:
- 与子例程不同,协程有许多用于暂停和恢复执行的入口点。协程可以暂停其执行并将控制权转移到其他协程,并且可以从中断点重新开始执行。
- 与子例程不同,没有 main 函数来按特定顺序调用协程并协调结果。协程是协作的,这意味着它们链接在一起形成一个管道。一个协程可能会使用输入数据并将其发送给处理它的其他协程。最后,可能有一个协程来显示结果。
2.协程与线程
现在你可能会想协程与线程有什么不同,两者似乎都在做同样的工作。对于线程,它是一个操作系统(或运行时环境)根据调度程序在线程之间切换,也就是说,线程是CPU调度和分派的基本单位。而在协程的情况下,由程序员和编程语言决定何时切换协程。协程通过程序员在设定点暂停和恢复来协同多任务处理。
2.1 Python 协程
在 Python 中,协程类似于生成器,但它几乎没有额外的方法,并且我们使用yield语句的方式略有变化。生成器生成数据用于迭代,而协程也可以使用数据。
在 Python 2.5 中,对 yield 语句进行了轻微的修改,现在 yield 也可以用作表达式。例如,在赋值语句的右侧:
line = (yield)
我们发送到协程的任何值都会被 (yield) 表达式捕获并返回。
可以通过 send()方法将值发送到协程 。例如,考虑这样一个协程,它打印出带有前缀“Dear”的名称。我们可以使用 send()方法将名称发送到协程。
# Python3 program for demonstrating
# coroutine execution
def print_name(prefix):
print("Searching prefix:{}".format(prefix))
while True:
name = (yield)
if prefix in name:
print(name)
# calling coroutine, nothing will happen
corou = print_name("Dear")
# This will start execution of coroutine and
# Prints first line "Searching prefix..."
# and advance execution to the first yield expression
corou.__next__()
# sending inputs
corou.send("Atul")
corou.send("Dear Atul")
Searching prefix:Dear
Dear Atul
2.2 协程的执行
协程的执行类似于生成器。当我们调用协程时,什么也没发生,它只在响应 next()和 sends()方法时运行。这在上面的例子中可以清楚地看到,因为只有在调用 __next__() 方法后,我们的协程才开始执行。在此调用之后,程序执行将推进到第一个 yield 表达式,现在执行过程暂停并等待将值发送到 corou 对象。当第一个值发送给它时,它会检查前缀和打印名称(如果存在前缀)。打印名称后,它会经历循环,直到再次遇到 name = (yield) 表达式。
2.3 关闭协程
协程可能会无限期运行,以关闭协程close()方法。当协程关闭时,它会生成 GeneratorExit 异常,该异常可以通过常规方式捕获。关闭协程后,如果我们尝试发送值,将引发 StopIteration 异常。下面是一个简单的例子:
# Python3 program for demonstrating
# closing a coroutine
def print_name(prefix):
print("Searching prefix:{}".format(prefix))
try :
while True:
name = (yield)
if prefix in name:
print(name)
except GeneratorExit:
print("Closing coroutine!!")
corou = print_name("Dear")
corou.__next__()
corou.send("Atul")
corou.send("Dear Atul")
corou.close()
输出:
Searching prefix:Dear
Dear Atul
Closing coroutine!!
2.4 创建管道的链接协程
协程可用于设置管道。我们可以将协程链接在一起,并使用 send()方法将数据推送到管道中。管道需要:
- 初始源(生产者)派生整个管道。生产者通常不是一个协程,它只是一个简单的方法。
- 接收器,它是管道的端点。接收器可能会收集所有数据并显示这些数据。
以下是链接的简单示例
# Python3 program for demonstrating
# coroutine chaining
def producer(sentence, next_coroutine):
'''
Producer which just split strings and
feed it to pattern_filter coroutine
'''
tokens = sentence.split(" ")
for token in tokens:
next_coroutine.send(token)
next_coroutine.close()
def pattern_filter(pattern="ing", next_coroutine=None):
'''
Search for pattern in received token
and if pattern got matched, send it to
print_token() coroutine for printing
'''
print("Searching for {}".format(pattern))
try:
while True:
token = (yield)
if pattern in token:
next_coroutine.send(token)
except GeneratorExit:
print("Done with filtering!!")
next_coroutine.close()
def print_token():
'''
Act as a sink, simply print the
received tokens
'''
print("I'm sink, i'll print tokens")
try:
while True:
token = (yield)
print(token)
except GeneratorExit:
print("Done with printing!")
pt = print_token()
pt.__next__()
pf = pattern_filter(next_coroutine = pt)
pf.__next__()
sentence = "Bob is running behind a fast moving car"
producer(sentence, pf)
I'm sink, i'll print tokens
Searching for ing
running
moving
Done with filtering!!
Done with printing!
3.小结
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源最大,效率很低
- 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
- 协程切换任务资源很小,效率高
- 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发