对 C/C++,MySQL 提供的库传统上都是阻塞操作,因此适合多线程 / 进程服务器架构编程。但是如果用 C/C++ 编写服务器,往往对性能会有极致要求,此时采用非阻塞的异步 I/O 才是更好的框架。

所幸,从 MySQL fork 出来的 MariaDB 提供了异步的 C/C++ MySQL client 接口。下面是本人对官方文档的翻译。后续我会在本人设计的 libcoevent 库中添加异步 MariaDB client 的支持。


概述

MariaDB 非阻塞 API 是基于普通的阻塞式的库调用设计的,这就使得这些 PIA 便于学习和记忆;这也使得将使用阻塞式的代码改写为非阻塞式的工作变得简单许多(反之亦然)。同时,这也便于在同一个代码目录中混合使用阻塞和非阻塞调用架构。

针对每一个可能阻塞套接字 I/O 的库函数,比如 int mysql_real_query(mysql, query, query_length),我们会引入两个非阻塞调用:

int mysql_real_query_start(&status, MYSQL, query, query_length)
int mysql_real_query_cont(&status, MYSQL, query_status)

为了做到非阻塞的操作,应用程序首先调用 mysql_real_query_start() 而不是 mysql_real_query(),除了第一个参数之外,剩余参数两者相同。

如果 mysql_real_query_start() 返回 0,则表示函数操作完成了,同时 status 变量被设置为通常 mysql_real_query() 的返回值。否则如果 mysql_real_query_start() 返回非零,则返回值表示一个位掩码值,表示当前库正在等待中的标志位。这些标志可以是 MYSQL_WAIT_READ, MYSQL_WAIT_WRITE或者 MYSQL_WAIT_EXEP,对应于 select() 或者 poll() 等系统调用中的类似标志位。同时,当正在等待超时的时候,也可以包含 MYSQL_WAIT_TIMEOUT 标志。

这种情况下,应用程序可以继续处理其他事件,并且定期检查在套接字上的适当条件标志或超时标志。当事件发生时,应用程序可以通过调用 mysql_real_query_cont() 来恢复操作,并在 wait_status 变量中传入实际发生的位掩码。

正如 mysql_real_query_start() 一样,当 mysql_real_query_cont() 操作结束时,返回 0,否则返回器需要继续等待着的标志位掩码。因此,应用程序同样需要继续调用 mysql_real_query_cont(),并根据需要,混合处理其他事件,直到返回 0 为止。同样地,返回值存储在 status 变量中。

有些调用并不会做任何套接字 I/O 操作,也不会阻塞,比如 mysql_option()。对于这些接口,并不会新增独立的 _start()_cont()函数。参见 “Non-blocking API reference” 页面,查看完整的阻塞与不阻塞函数的列表。

可以使用 select()poll() 等类似机制来检查套接字或超时事件。不过实际上往往是用更高一层封装的、提供注册和处理这类事件的工具的框架中去完成这些工作(比如 libevent)。

可以通过调用 mysql_get_socket() 函数来获得需要检查的时间的套接字,超时时间则可以通过 mysql_get_timeout_value() 来获得。

下面是一个使用非阻塞 API 进行一次查询的简单(但完整)的示例。这个例子在 MariaDB 代码树中的 client/async_example.c 中;另一个比较大、但是更加贴近实际的、使用 libevent 的例子则是 tests/asyny_queries.c

static void run_query(const char *host, const char *user, const char *password)
{
  int err, status;
  MYSQL mysql, *ret;
  MYSQL_RES *res;
  MYSQL_ROW row;

  mysql_init(&mysql);
  mysql_options(&mysql, MYSQL_OPT_NONBLOCK, 0);

  status = mysql_real_connect_start(&ret, &mysql, host, user, password, NULL, 0, NULL, 0);
  while (status) {
    status = wait_for_mysql(&mysql, status);
    status = mysql_real_connect_cont(&ret, &mysql, status);
  }

  if (!ret)
    fatal(&mysql, "Failed to mysql_real_connect()");

  status = mysql_real_query_start(&err, &mysql, SL("SHOW STATUS"));
  while (status) {
    status = wait_for_mysql(&mysql, status);
    status = mysql_real_query_cont(&err, &mysql, status);
  }
  if (err)
    fatal(&mysql, "mysql_real_query() returns error");

  /* This method cannot block. */
  res= mysql_use_result(&mysql);
  if (!res)
    fatal(&mysql, "mysql_use_result() returns error");

  for (;;) {
    status= mysql_fetch_row_start(&row, res);
    while (status) {
      status= wait_for_mysql(&mysql, status);
      status= mysql_fetch_row_cont(&row, res, status);
    }
    if (!row)
      break;
    printf("%s: %s\n", row[0], row[1]);
  }
  if (mysql_errno(&mysql))
    fatal(&mysql, "Got error while retrieving rows");
  mysql_free_result(res);
  mysql_close(&mysql);
}

/* Helper function to do the waiting for events on the socket. */
static int wait_for_mysql(MYSQL *mysql, int status) {
  struct pollfd pfd;
  int timeout, res;

  pfd.fd = mysql_get_socket(mysql);
  pfd.events =
    (status & MYSQL_WAIT_READ ? POLLIN : 0) |
    (status & MYSQL_WAIT_WRITE ? POLLOUT : 0) |
    (status & MYSQL_WAIT_EXCEPT ? POLLPRI : 0);
  if (status & MYSQL_WAIT_TIMEOUT)
    timeout = 1000*mysql_get_timeout_value(mysql);
  else
    timeout = -1;
  res = poll(&pfd, 1, timeout);
  if (res == 0)
    return MYSQL_WAIT_TIMEOUT;
  else if (res < 0)
    return MYSQL_WAIT_TIMEOUT;
  else {
    int status = 0;
    if (pfd.revents & POLLIN) status |= MYSQL_WAIT_READ;
    if (pfd.revents & POLLOUT) status |= MYSQL_WAIT_WRITE;
    if (pfd.revents & POLLPRI) status |= MYSQL_WAIT_EXCEPT;
    return status;
  }
}

设置 MySQL 非阻塞标志

在使用任意一个非阻塞操作之前,有必要通过设置 MYSQL_OPT_NONBLOCK选项来启用非阻塞功能:

mysql_options(&mysql, MYSQL_OPTION_NONBLOCK, 0)

这个调用可以在任何时候调用,不过典型情况下是在最开始的时候完成,也就是在 mysql_real_connect() 之前。不过这依然可以在任何开始使用非阻塞操作的时候调用。如果在没有使用 MYSQL_OPT_NONBLOCK 的情况下尝试任何非阻塞操作,应用程序一般情况下会因为空指针异常崩溃。

MYSQL_OPTION_NONBLOCK 的参数是正在等待 I/O、并且应用程序正在做其他操作时用于保存非阻塞操作的状态(state)的栈大小。正常情况下,应用程序不需要修改这个值,可以传入 0 以使用默认值。


混合阻塞和非阻塞操作

在同一个 MYSQL 连接中混合使用阻塞和非阻塞操作是完全可行的。

因此,应用程序可以做普通的阻塞式的 mysql_real_connect(),然后依序执行一个非阻塞的 mysql_real_query_start()。反之亦然:先做一个非阻塞的 mysql_real_connect_start(),然后晚些时间执行后续的 mysql_real_query()

混合操作允许代码在发生忙等待也影响不大的地方使用较为简单的的阻塞式 API 时非常有用。比如在程序启动的时候建立连接,或者是在多个大型的、长耗时的查询中,执行短且快的小型查询。

唯一的限制是,在开始一个新的阻塞式(或非阻塞)操作之前,上一个的非阻塞式操作必须已经完成。参见下一章节:”尽早终止非阻塞操作“。


提前终止非阻塞过程

当使用 mysql_real_query_start()或其他 _start() 函数启动了一个非阻塞操作之后,它必须在启动一个新的操作之前完成。因此,应用程序必须继续调用 `mysql_real_query_cont() 直到返回 0 —— 表示目前操作已经完成。不允许在流程的中间挂起一个操作不管,然后启动一个新的。

尽管如此,允许在出列非阻塞操作的流程的中途调用通过 mysql_close() 来完全中止连接。一个新的连接在发起查询操作之前必须以 mysql_real_connect() 开始,这个连接可以使用新的 MYSQL 对象或者是复用旧的。

未来我们可能会实现一个 abort 机制,用于强制一个正在进行中的操作尽可能快地中止掉(不过疼然需要在 abort 之后调用一次 mysql_real_query_cont()),并且允许其进行清理操作并且立即返回合适的错误码。


限制

DNS

当传递一个主机名给 mysql_real_connect_start() 时(相对于一个本地 unix 套接字或者是 IP 地址),它可能会需要在 DNS 中查询这个主机名,取决于本地的配置(比如该名字不在 /etc/hosts 或缓存中)。这一个 DNS 查询并不会以非阻塞方式来完成。这就意味着 mysql_real_connect_start() 在等待 DNS 响应的时候可能不会将 CPU 控制权交还给应用程序。因此,如果 DNS 查询很慢或不可用的时候,应用程序会 “挂起” 一段时间。

如果这是一个大问题的话,应用程序可以传递一个 IP 地址给 mysql_real_connect_start()而不是主机名以避免该情况的发生。应用程序可以采用操作系统或事件框架提供的任何非阻塞的 DNS 查询机制来实现主机名的解析以实现 IP 地址的获取。又或者一个简单的解决方法是,将主机名添加到本地的主机查找文件中(在 Posix / Unix / Linux 机器中则是 /etc/hosts 文件)。

Windows 命名管道和共享内存连接

对使用 Windows 命名管道和共享内存的连接,目前没有非阻塞 API 可支持。

使用阻塞或者是非阻塞的 API,命名管道和共享内存连接依然是可用的。尽管如此,需要阻塞在命名管道的 I/O 的操作,仍然不会(想上文那样)将 CPU 控制权交回给应用程序;相反,它们会 “挂起” 并等待操作完成,就像普通的阻塞 API 一样。


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文地址为:https://segmentfault.com/a/1190000016405452/
原文最早发布于:https://cloud.tencent.com/developer/article/1336510,也是本人的博客。


amc
927 声望228 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发