专栏首页后台全栈之路在 C/C++ 异步 I/O 中使用 MariaDB 的非阻塞接口
原创

在 C/C++ 异步 I/O 中使用 MariaDB 的非阻塞接口

对 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://cloud.tencent.com/developer/article/1336510

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

登录 后参与评论
0 条评论

相关文章

  • 在 libevent 中使用 MariaDB(MySQL)

    在之前我翻译的官方文档中提到了 MariaDB 提供了对异步 I/O 的支持。那篇文章是一个比较简要的介绍。不过实际适配中,官方也提供了一个完整适配 libev...

    amc
  • 在 libevent 中使用 MariaDB(MySQL)

    [腾讯云服务器 年付3折起【新用户限量秒杀】热门云产品限量秒杀,云服务器1核2G 16.5元/月起

    赤孺
  • 数据库中间件DBLE学习(一) 基础介绍和快速搭建

    dble是上海爱可生信息技术股份有限公司基于mysql的高可用扩展性的分布式中间件。江湖人送外号MyCat Plus。开源地址

    BuddyYuan
  • socket阻塞与非阻塞,同步与异步、I/O模型

    在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:

    用户6280468
  • socket阻塞与非阻塞,同步与异步、I/O模型

    在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:

    黄规速
  • Java网络编程之BIO

    作为一个WEB程序员,网络编程模型是我们需要掌握的基础知识,在介绍具体模型之前先聊下几个基本的概念:

    心平气和
  • 面经·同步(Sync)/异步(Async)阻塞(Block)/非阻塞(Unblock)四种调用方式和(网络)IO模型

    网络应用需要处理的无非就是两大类问题,网络I/O,数据计算。相对于后者,网络I/O的延迟,给应用带来的性能瓶颈大于后者。

    陈黎栋
  • 几种服务器端IO模型的简单介绍及实现(上)

    一些概念: 同步和异步 同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪,而异步是指用户进程...

    李海彬
  • 几种服务器端IO模型的简单介绍及实现(上)

    一些概念: 同步和异步 同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪,而异步是指用户进程...

    李海彬
  • linux网络编程之socket(八):五种I/O模型和select函数简介

    一、五种I/O模型 1、阻塞I/O ? 我们在前面所说的I/O模型都是阻塞I/O,即调用recv系统调用,如果没有数据则阻塞等待,当数据到来则将数据从内核空间(...

    s1mba
  • 高性能服务端漫谈

    一、背景 进入多核时代已经很久了,大数据概念也吵得沸沸扬扬,不管你喜欢不喜欢,不管你遇到没遇到,big-data或bigger-data都必须正视. 处理大数...

    小小科
  • 高性能服务端漫谈

    我是攻城师
  • 网络I/O原理、I/O模型及Linux监控命令

    I/O是计算机的输入输出,通俗一点讲是计算机数据的流动,包括CPU、内存、磁盘、网络、外设的数据流程,是针对不同主体而言的数据的输入和输出。

    搬砖俱乐部
  • 深入浅出Node.js

    2.模块定义:提供exports对象用于导出当前模块的方法或者变量,并且是唯一导出的出口

    硬核项目经理
  • 深入浅出NodeJS随记 (一)

    yiuanli最近在研读书籍 深入浅出nodejs , 随手写下的一些笔记, 和大家分享~ 如有错误,欢迎指正~

    邱邱邱邱yf
  • 再送一波超级福利,想知道嘛?

    在分享这次面试经典题目汇总之前,预告一下,下期分享面试题目汇总,会把之前网友在群里分享的一份面试题目答案完善出来:

    用户6280468
  • 几种服务器端IO模型的简单介绍及实现

    一些概念: 同步和异步 同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪,而异步是指用户进程...

    李海彬
  • UNPv1第六章:IO复用select&poll

    有些进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(也就是说输入已准备好被读取,或者描述符已能承受更多的输出),他就通知进程...

    提莫队长
  • 【i.MX6ULL】驱动开发9——Linux IO模型分析

    前面两篇介绍按键的文章,无论是用GPIO来读取,还是用中断的方式,其应用程序通过循环读取的方式获取按键值,都会使得CPU的占用率很高。本篇先来介绍Linux中几...

    xxpcb

扫码关注云+社区

领取腾讯云代金券