Python 基于生成器的协程的看似无限递归
以下是大卫·比兹利(David Beazley)关于发电机的幻灯片(供感兴趣的人观看) 定义了一个Python 基于生成器的协程的看似无限递归,python,python-3.x,recursion,generator,coroutine,Python,Python 3.x,Recursion,Generator,Coroutine,以下是大卫·比兹利(David Beazley)关于发电机的幻灯片(供感兴趣的人观看) 定义了一个任务类,该类将生成未来的生成器完整地封装在任务类中(不包含错误处理),如下所示: class Task: def __init__(self, gen): self._gen = gen def step(self, value=None): try: fut = self._gen.send(value)
任务
类,该类将生成未来的生成器完整地封装在任务
类中(不包含错误处理),如下所示:
class Task:
def __init__(self, gen):
self._gen = gen
def step(self, value=None):
try:
fut = self._gen.send(value)
fut.add_done_callback(self._wakeup)
except StopIteration as exc:
pass
def _wakeup(self, fut):
result = fut.result()
self.step(result)
在示例中,还定义了以下递归函数:
from concurrent.futures import ThreadPoolExecutor
import time
pool = ThreadPoolExecutor(max_workers=8)
def recursive(n):
yield pool.submit(time.sleep, 0.001)
print("Tick :", n)
Task(recursive(n+1)).step()
以下是两种情况:
Task(recursive(0)).step()
它开始打印,似乎已经超过了递归限制。但它显然没有超过它,打印堆栈级别表明它在整个执行过程中保持不变。还有别的事情我不太明白
注意:如果这样执行python进程,则需要终止它任务
,递归
)与以下内容一起放入文件:
if __name__ == "__main__":
Task(recursive(0)).step()
然后用python myfile.py
运行它,它在7
处停止滴答(似乎是max_workers
的数量)我的问题是,它似乎是如何超越递归限制的,以及为什么它的行为会根据您执行它的方式而有所不同
该行为同时出现在Python 3.6.2和Python 3.5.4上(我猜
3.6
和3.5
家族中的其他成员也会出现)。让我们从第7个开始。这是您已经提到的工人数量,标记为[0..7]。任务类需要以函数标识符的形式传递递归
Task(recursive).step(n)
而不是
Task(recursive(n)).step()
这是因为,recursive函数需要在pool
环境中调用,而在当前情况下,recursive
是在主线程本身中计算的<代码>时间。睡眠是当前代码中唯一在任务池中计算的函数
代码的一个主要问题是递归。池中的每个线程都依赖于内部函数,该函数对可用工作线程数的执行设置上限。该函数无法完成,因此无法执行新函数。因此,它在达到递归限制之前就终止了。您显示的
递归
生成器实际上不是递归的,不会导致系统递归限制出现问题
了解为什么需要注意递归
生成器的代码何时运行。与普通函数不同,仅调用recursive(0)
不会导致它立即运行代码并进行其他递归调用。相反,调用recursive(0)
会立即返回生成器对象。只有当您send()
发送到生成器时,代码才会运行,并且只有在您send()
第二次发送到生成器后,它才会启动另一个调用
让我们在代码运行时检查调用堆栈。在顶层,我们运行任务(递归(0)).step()
。按顺序做三件事:
recursive(0)
此调用立即返回生成器对象Task()
将创建Task
对象,其\uuuu init\uuuu
方法存储对在第一步中创建的生成器对象的引用\uuu.step()
调用任务上的方法。这就是行动真正开始的地方!让我们看看通话中发生了什么:
在这里,我们通过发送一个值来启动发电机运行。让我们深入了解生成器代码的运行情况:fut=self.\u gen.send(value)
此命令计划在另一个线程中执行某些操作。但我们不会等它发生。相反,我们会得到一个yield pool.submit(time.sleep,0.001)
,当它完成时,我们可以使用它来获得通知。我们让未来立即回到以前的代码级别未来的
这里我们要求在将来准备好时调用我们的fut.add\u done\u callback(self.\u wakeup)
方法。这总是立即返回李>\u wakeup()
方法现在结束。没错,我们(暂时)完成了!这对于问题的第二部分很重要,我将在后面进一步讨论步骤
time.sleep
)完成运行后,它所在的线程将调用我们在Future
对象上设置的回调。也就是说,它将调用前面创建的Task
对象上的Task.\u wakup()
(我们在顶层不再对其进行引用,但Future
保留了一个引用,因此它仍然处于活动状态)。让我们看看这个方法:
存储延迟调用的结果。在这种情况下,这是不相关的,因为我们从不查看结果(反正是result=fut.result()
)None
再次执行步骤!现在我们回到我们关心的代码。让我们看看这次它做了什么:self.step(结果)
send再次发送到生成器,以便它接管。它已经产生了一次,所以这次我们在fut=self.\u gen.send(value)
产生之后开始:
这很简单打印(“勾选:”,n)
这就是事情变得有趣的地方。这一行和我们开始时一样。所以,我Task(递归(n+1)).step()
# create the pool below the definition of recursive() with ThreadPoolExecutor(max_workers=8) as pool: Task(recursive(0)).step()
exception calling callback for <Future at 0x22313bd2a20 state=finished returned NoneType> Traceback (most recent call last): File "S:\python36\lib\concurrent\futures\_base.py", line 324, in _invoke_callbacks callback(self) File ".\task_coroutines.py", line 21, in _wakeup self.step(result) File ".\task_coroutines.py", line 14, in step fut = self._gen.send(value) File ".\task_coroutines.py", line 30, in recursive Task(recursive(n+1)).step() File ".\task_coroutines.py", line 14, in step fut = self._gen.send(value) File ".\task_coroutines.py", line 28, in recursive yield pool.submit(time.sleep, 1) File "S:\python36\lib\concurrent\futures\thread.py", line 117, in submit raise RuntimeError('cannot schedule new futures after shutdown') RuntimeError: cannot schedule new futures after shutdown