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

對 C/C++,MySQL 提供的庫傳統上都是阻塞操做,所以適合多線程 / 進程服務器架構編程。可是若是用 C/C++ 編寫服務器,每每對性能會有極致要求,此時採用非阻塞的異步 I/O 纔是更好的框架。mysql

所幸,從 MySQL fork 出來的 MariaDB 提供了異步的 C/C++ MySQL client 接口。下面是本人對官方文檔的翻譯。後續我會在本人設計的 libcoevent 庫中添加異步 MariaDB client 的支持。git


概述

MariaDB 非阻塞 API 是基於普通的阻塞式的庫調用設計的,這就使得這些 PIA 便於學習和記憶;這也使得將使用阻塞式的代碼改寫爲非阻塞式的工做變得簡單許多(反之亦然)。同時,這也便於在同一個代碼目錄中混合使用阻塞和非阻塞調用架構。github

針對每個可能阻塞套接字 I/O 的庫函數,好比 int mysql_real_query(mysql, query, query_length),咱們會引入兩個非阻塞調用:sql

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 標誌。segmentfault

這種狀況下,應用程序能夠繼續處理其餘事件,而且按期檢查在套接字上的適當條件標誌或超時標誌。當事件發生時,應用程序能夠經過調用 mysql_real_query_cont() 來恢復操做,並在 wait_status 變量中傳入實際發生的位掩碼。api

正如 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,也是本人的博客。

相關文章
相關標籤/搜索