前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >客户端异步非阻塞 IO 新的流模式(基于 Nim 语言)

客户端异步非阻塞 IO 新的流模式(基于 Nim 语言)

作者头像
用户2870857
发布2020-04-22 12:02:47
9980
发布2020-04-22 12:02:47
举报
文章被收录于专栏:Python高效编程

Nim 编程语言是一个新型的静态类型、命令式编程语言,支持过程式、函数式、面向对象和泛型编程风格而保持简单和高效。Nim 从Lisp继承来的一个特殊特性--抽象语法树(AST)作为语言规范的一部分,可以用作创建领域特定语言的强大宏系统。

官方网址: https://nim-lang.org 与 Python 交互: https://www.oschina.net/news/114888/nimporter-1-0-0-released

客户端异步非阻塞 IO 新的流模式(基于 Nim 语言)

Mostly Single Buffer 更精确点应该是 Mostly Single Buffer one connection,意思是:大多数时候,一个连接总是使用一个缓冲区。也就是说,会存在这样的情况,即一个连接使用两个甚至更多个缓冲区。

Mostly Single Buffer 的目标是提供绝对的 IO 一致性,并尽可能保证 IO 性能和减少内存占用,而最小化对用户编程效率的影响。

在对网络 IO 进行编程时,我们通常要处理各种各样的协议封装数据。拿 HTTP 来说,这些数据是以 “请求” (Request) 为单元进行封装的。对于同一个客户端连接,其发送 HTTP 数据包的过程类似这样:

代码语言:javascript
复制
|---request 1---|---request 2---|---request 3---|

上面图中表示,客户端发送了 3 个请求。同一个客户端发送的 HTTP 请求总是连续的。站在服务器的视角,服务器会为每一个客户端创建一个 “缓冲区”,将请求数据读入缓冲区,进行处理,然后作出响应。通常,服务器对于缓冲区的态度是,对每个客户端连接创建一个独立的缓冲区。也就是说,如果同时有 2000 个客户端连接到服务器,服务器通常会有 2000 个缓冲区,分别对应每一个客户端。对于每一个客户端,服务器都使用一个完全独立的缓冲区处理,这就保证了 IO 一致性,即各个客户端的处理不会交叉在一起;同时,也意味着更多的内存占用,每一个缓冲区都要占用一块内存。

现在,把视角转回客户端。作为客户端,通常向服务器建立一个连接,然后不断发起请求。让我们谈谈 MySQL 连接,这会非常有代表性,特别是涉及到异步非阻塞 IO 时,其内部的操作过程会变得非常负载和不稳定。看看下面这段伪代码 (1):

代码语言:javascript
复制
var mysql = newMysqlClient()

await mysql.query("select * from users") # 第一个请求
await mysql.query("select * from blogs") # 第二个请求

这段代码发起了两次查询请求,需要注意的是 await 适时地等待第一个请求完成,然后才发起第二个请求。请看下面伪代码(2):

代码语言:javascript
复制
var mysql = newMysqlClient()

var req1 = mysql.query("select * from users") # 第一个请求
var req2 = mysql.query("select * from blogs") # 第二个请求

await req1
await req2

仍是发起两次查询请求,不同之处在于,第二个请求还没等第一个请求处理完成,就开始发起。相同的是,先等待第一个响应完成,然后等待第二个响应完成。再请看下面伪代码(3):

代码语言:javascript
复制
var mysql = newMysqlClient()

var req1 = mysql.query("select * from users") # 第一个请求
var req2 = mysql.query("select * from blogs") # 第二个请求

await req2
await req1

这段代码先等待第二个响应完成,然后等待第一个响应完成。

以上三段伪代码执行了相同的操作,但是其涉及的影响却远远不同。上面说过,服务器会为每一个客户端连接创建一个缓冲区,那么客户端呢?通常的方法是,客户端为每一个连接创建一个缓冲区。创建多个缓冲区是没有必要的,因为每个连接一次只能处理一个问题,额外的缓冲区通常都是浪费。

然而,上面三段伪代码会牵扯出很多问题。我们现在假设 mysql 的客户端连接只有一个缓冲区,发出两个查询请求,那么返回来的结果则是这样的:

代码语言:javascript
复制
|---response 1---|---response 2---|
  ^

结果是顺序排列在同一个缓冲区当中。

对于伪代码(1),这不会产生问题,因为总是先等待第一个响应完成,再处理第二个响应;伪代码(2),也构不成问题,因为其对响应的处理,类似伪代码(1)。然而,对于伪代码(3),却产生了大问题,因为其先等待第二个响应完成,然后再处理第一个响应。这就表示,响应 2 不处理完成,响应 1 就不会处理。看看上图,因为响应 1 和响应 2 被顺序存储到同一个缓冲区,这就导致只有响应 1 从缓冲区提取后,响应 2 才会获得操作。“死锁” 产生了!

这有点像多线程编程中常常提到的 “死锁”,锁 A 先于锁 B 锁住,然而程序却先处理锁 B,导致 “死锁”。异步非阻塞 IO 并没有锁的概念,然而在此处却也产生了 “死锁” 的问题。这是因为 await 的问题。await 将一行程序操作拆成了两行,将原来本可以一次处理变成了两次处理,“破坏了” 原子操作。然而,我们不能强制要求用户总是将 await 写作一行,而且,当处理大数据流的时候,我们还必须使用多次 await 来处理 “小块” 数据。看看这段伪代码:

代码语言:javascript
复制
var stream = mysql.queryLargeResult(...)

while stream.next():
  await stream.readRow()

然而,问题总要解决。回想起服务器对于缓冲区的态度,我们可以想要使用同样的思路,为每一次请求创建一块单独的缓冲区。好了,客户端 IO 的解决方法是,对于每一个客户端连接,不再是创建一个单独的缓冲区,而是每一个请求创建一个单独的缓冲区。看看下面的伪代码:

代码语言:javascript
复制
var mysql = newMysqlClient()

var req1 = mysql.query("select * from users") # 第一个请求
var req2 = mysql.query("select * from blogs") # 第二个请求
var req3 = mysql.query("select * from blogs") # 第三个请求
var req4 = mysql.query("select * from blogs") # 第四个请求

await req2
await req1
await req4
await req3

上面的代码创建 4 块独立的缓冲区,但是它们都是位于同一个客户端连接。

不过,这也同时带来问题,即内存被大量的占用,而且被浪费,因为在每一时刻,客户端只有一块缓冲区是有用的。特别是当你建立一个 Web Server,然后对 HTTP 请求进行一些 Mysql 查询时,你的服务器内存开始飙升。这可能不是你所想要的。

Mostly Single Buffer 期望解决这些问题。客户端建立网络 IO 时,对每一个连接,该方案尽可能只创建一块缓冲区,并使用一个标记,标记该缓冲区是处于 “忙” 状态,还是处于 “空闲” 状态。当处于 “忙”状态时,如果收到新的请求操作,则自动创建一块新的缓冲区。当一块 “忙” 缓冲区重新变为 “空闲” 的时候,自动将其回收。

倾倒:如果用户对同一连接申请了多个请求,当处理响应的时候,MSB 会查看用户提供的读操作。比如发起请求 [q1, q2, q3],MSB 收到响应数据后,查看请求队列,先查询 q1 的读操作,然后使用读操作处理数据,如果 q1 的所有读操作都工作完,q1 的响应数据仍然未完全 “读” 完,那么 MSB 就进行 “倾倒”,将 q1 剩余的数据倒入一块新的缓冲区暂存起来,以便于后续 q1 的相关读操作进行处理。然后依次处理 q2,q3,… 这自然会形成一种情况,即 q1、q2、q3 剩余的数据及其引用会一直暂存在内存,直到内存溢出。.clear(q1) 允许立刻清除 q1 相关响应数据。

使用 Mostly Single Buffer,如果你是经验丰富的程序员,总能合理安排你的程序,即在合理时刻 await 每一个请求,那么你总能最小化缓冲区的内存占用。比如 (只创建一块缓冲区) :

代码语言:javascript
复制
var req1 = mysql.query("select * from users")    # 第一个请求
await req1

var req2 = mysql.query("select * from blogs")    # 第二个请求
await req2

var req3 = mysql.query("select * from comments") # 第三个请求
await req3

而如果你对 IO 编程掌握还不够熟练,或者编程的程序比较随意,Mostly Single Buffer 总能保证你的程序正确运行,但是会消耗一些内存。比如 (创建三块缓冲区):

代码语言:javascript
复制
var req1 = mysql.query("select * from users")    # 第一个请求
var req2 = mysql.query("select * from blogs")    # 第二个请求
var req3 = mysql.query("select * from comments") # 第三个请求

await req3
await req1
await req2

这个 IO 缓冲区方案将会应用在 https://github.com/iocrate/netkit --- 一个正在积极开发的 Nim Network 工具包,以及其他的一些网络包中,比如 mysql connector。顺便一提的是,https://github.com/tulayang/asyncmysql 使用了回调函数来处理 IO 一致性问题,但是却导致 API 调用比较难以使用,未来的连接器将会获得改善。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python高效编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 客户端异步非阻塞 IO 新的流模式(基于 Nim 语言)
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档