返回介绍

18.3 避免阻塞型调用

发布于 2024-02-05 21:59:47 字数 3174 浏览 0 评论 0 收藏 0

Ryan Dahl(Node.js 的发明者)在介绍他的项目背后的哲学时说:“我们处理 I/O 的方式彻底错了。”5 他把执行硬盘或网络 I/O 操作的函数定义为阻塞型函数,主张不能像对待非阻塞型函数那样对待阻塞型函数。为了说明原因,他展示了表 18-1 中的前两列。

5Introduction to Node.js”视频 4:55 处。

表18-1:使用现代的电脑从不同的存储介质中读取数据的延迟情况;第三栏按比例换算成具体的时间,便于人类理解

存储介质

CPU 周期

按比例换算成“人类时间”

L1 缓存

3

3 秒

L2 缓存

14

14 秒

RAM

250

250 秒

硬盘

41 000 000

1.3 年

网络

240 000 000

7.6 年

为了理解表 18-1,请记住一点:现代的 CPU 拥有 GHz 数量级的时钟频率,每秒钟能运行几十亿个周期。假设 CPU 每秒正好运行十亿个周期,那么 CPU 可以在一秒钟内读取 L1 缓存 333 333 333 次,读取网络 4 次(只有 4 次)。表 18-1 中的第三栏是拿第二栏中的各个值乘以固定的因子得到的。因此,在另一个世界中,如果读取 L1 缓存要用 3 秒,那么读取网络要用 7.6 年!

有两种方法能避免阻塞型调用中止整个应用程序的进程:

在单独的线程中运行各个阻塞型操作

把每个阻塞型操作转换成非阻塞的异步调用使用

多个线程是可以的,但是各个操作系统线程(Python 使用的是这种线程)消耗的内存达兆字节(具体的量取决于操作系统种类)。如果要处理几千个连接,而每个连接都使用一个线程的话,我们负担不起。

为了降低内存的消耗,通常使用回调来实现异步调用。这是一种低层概念,类似于所有并发机制中最古老、最原始的那种——硬件中断。使用回调时,我们不等待响应,而是注册一个函数,在发生某件事时调用。这样,所有调用都是非阻塞的。因为回调简单,而且消耗低,所以 Ryan Dahl 拥护这种方式。

当然,只有异步应用程序底层的事件循环能依靠基础设置的中断、线程、轮询和后台进程等,确保多个并发请求能取得进展并最终完成,这样才能使用回调。6 事件循环获得响应后,会回过头来调用我们指定的回调。不过,如果做法正确,事件循环和应用代码共用的主线程绝不会阻塞。

6其实,虽然 Node.js 不支持使用 JavaScript 编写的用户级线程,但是在背后却借助 libeio 库使用 C 语言实现了线程池,以此提供基于回调的文件 API——因为从 2014 年起,大多数操作系统都不提供稳定且便携的异步文件处理 API 了。

把生成器当作协程使用是异步编程的另一种方式。对事件循环来说,调用回调与在暂停的协程上调用 .send() 方法效果差不多。各个暂停的协程是要消耗内存,但是比线程消耗的内存数量级小。而且,协程能避免可怕的“回调地狱”;这一点会在 18.5 节讨论。

现在你应该能理解为什么 flags_asyncio.py 脚本的性能比 flags.py 脚本高 5 倍了:flags.py 脚本依序下载,而每次下载都要用几十亿个 CPU 周期等待结果。其实,CPU 同时做了很多事,只是没有运行你的程序。与此相比,在 flags_asyncio.py 脚本中,在 download_many 函数中调用 loop.run_until_complete 方法时,事件循环驱动各个 download_one 协程,运行到第一个 yield from 表达式处,那个表达式又驱动各个 get_flag 协程,运行到第一个 yield from 表达式处,调用 aiohttp.request(...) 函数。这些调用都不会阻塞,因此在零点几秒内所有请求全部开始。

asyncio 的基础设施获得第一个响应后,事件循环把响应发给等待结果的 get_flag 协程。得到响应后,get_flag 向前执行到下一个 yield from 表达式处,调用 resp.read() 方法,然后把控制权还给主循环。其他响应会陆续返回(因为请求几乎同时发出)。所有 get_ flag 协程都获得结果后,委派生成器 download_one 恢复,保存图像文件。

 为了尽量提高性能,save_flag 函数应该执行异步操作,可是 asyncio 包目前没有提供异步文件系统 API(Node 有)。如果这是应用的瓶颈,可以使用 loop.run_in_executor 方法,在线程池中运行 save_flag 函数。示例 18-9 会说明做法。

因为异步操作是交叉执行的,所以并发下载多张图像所需的总时间比依序下载少得多。我使用 asyncio 包发起了 600 个 HTTP 请求,获得所有结果的时间比依序下载快 70 倍。

现在回到那个 HTTP 客户端示例,看看如何显示动态的进度条,并且恰当地处理错误。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文