asyncio
是异步的,因为协程自愿地合作。所有的asyncio
代码都必须考虑到合作,这完全是重点。否则,您可能只能使用线程来实现并发。
您不能在执行器中运行“阻塞”函数(不是协程函数或不会合作的方法),因为您不能仅仅假设该代码可以在单独的执行器线程中运行。或者即使它需要在执行器中运行。
Python标准库中充满了非常有用的代码,asyncio
项目将希望利用这些代码。大多数标准库由常规的“阻塞”函数和类定义组成。它们快速完成工作,因此即使它们“阻塞”,也会在合理的时间内返回。
但是,大多数代码也不是线程安全的,通常不需要。但是,一旦asyncio
自动在执行器中运行所有这些代码,那么您就不能再使用非线程安全的函数了。此外,在其中创建线程以运行同步代码并不是免费的,创建线程对象需要时间,您的操作系统也不会让您无限制地运行线程。许多标准库函数和方法都非常快速,为什么要在单独的线程中运行str.splitlines()
或urllib.parse.quote()
,当执行代码并完成它会更快呢?
你可能会认为这些函数不是按照你的标准阻塞的。你没有定义“阻塞”在这里的含义,但“阻塞”只意味着:不会自愿放弃。如果我们将其缩小到当它必须等待某些东西并且计算机可以做其他事情时不会自愿放弃,那么下一个问题就是如何检测它应该已经放弃了?对此的答案是你无法知道。time.sleep()是一个阻塞函数,你希望将其放弃给循环,但这是一个C函数调用。Python不能知道time.sleep()将会阻塞多长时间,因为调用time.sleep()的函数将在全局命名空间中查找名称time,然后在执行time.sleep()表达式时查找结果上的属性sleep。由于Python的命名空间可以在执行期间的任何时刻被更改,因此你无法知道time.sleep()实际执行的操作。
你可以认为
time.sleep()
实现在被调用时应该自动执行yield,但是那么你就必须开始识别所有这样的函数。而且你需要修补的地方没有限制,你永远也不可能知道所有的地方,特别是对于第三方库来说更是如此。例如,
python-adb项目使用
libusb1
库为你提供了一个同步USB连接到Android设备。这不是标准的I/O代码路径,那么Python怎么知道创建和使用这些连接是好的yield位置呢?
所以你不能仅仅假设代码需要在执行器中运行,因为并不是所有的代码都可以在执行器中运行,因为它不是线程安全的,而且Python无法检测到代码何时会阻塞,并真正需要yield。
那么,在
asyncio
下,协程是如何协作的呢?通过使用
task 对象 来处理需要与其他任务同时运行的逻辑代码块,并使用
future 对象 向任务发出信号,表明当前逻辑代码块想要放弃控制权以便让其他任务执行。这就是异步
asyncio
代码异步的原因——自愿放弃控制权。当循环将控制权交给多个任务中的一个任务时,该任务会执行协程调用链的一个“步骤”,直到该调用链生成一个 future 对象,此时该任务会向 future 对象的“完成”回调列表中添加一个唤醒回调并将控制权返回给循环。在稍后的某个时间点,当 future 被标记为完成时,唤醒回调会被运行,任务将执行另一个协程调用链步骤。
有另外一些东西负责标记未来的对象完成。当使用 asyncio.sleep()
时,回调会在特定时间给予循环运行,该回调会标记 asyncio.sleep()
未来为已完成。当使用 stream object 执行 I/O 时,(在 UNIX 上),循环使用 select
calls 检测何时应唤醒未来对象,以便在 I/O 操作完成时唤醒它。当您使用 lock or other synchronisation primitive 时,同步原语将维护一组未来对象,以在适当时候将其标记为“完成”(等待锁? 将未来添加到堆栈中。释放保持的锁?从堆栈中选择下一个未来并将其标记为已完成,以便等待锁的下一个任务可以唤醒并获取锁,等等)。
将阻塞同步代码放入执行器中只是这里合作的另一种形式。在项目中使用 asyncio
时,开发人员需要确保使用提供给您的工具来确保您的协程相互配合。您可以自由地使用阻塞的 open()
调用文件而不使用流,并且在知道代码需要在单独的线程中运行以避免阻塞太长时间时,可以使用执行器。
最后但并非最不重要的,使用{{asyncio}}的整个目的是尽可能避免使用线程。使用线程有缺点;代码需要保持线程安全(控制可以在任何地方切换线程,因此两个访问共享数据的线程应该小心处理,并且“小心处理”可能会使代码变慢)。线程无论是否有任务都会执行;在固定数量的线程之间切换控制,而所有线程都等待I/O发生是浪费CPU时间的,而{{asyncio}}循环则可以自由查找未等待的任务。
asyncio
和协程的整个目的是在没有线程的情况下运行非阻塞代码。aiohttp
的第一个示例展示了它使用asyncio
来运行非阻塞代码。如果您决定在asyncio
中运行阻塞代码,那不是asyncio
的错。您也可以问为什么int(“我的银行账户”)
不能返回您当前的账户余额。 - MisterMiyagi