数据库内核月报 - 2021 / 10

MySQL · 源码分析· 跟着MySQL 8.0 学 C++:scope_guard

背景简介

MySQL source code now permits and uses C++11 features. —- MySQL 8.0.0 (2016-09-12) MySQL now can be compiled using C++14. —- MySQL 8.0.16 (2019-04-25) MySQL now can be compiled using C++17. —- MySQL 8.0.27 (2021-10-19)

MySQL 8.0近段时间GA了8.0.17版本,正式支持了C++17的编译,很高兴看到官方开始逐步抛弃5.x时代的老包袱,在代码风格上开始拥抱一些新的东西。数据库作为一个复杂系统,依赖大量的协作开发和软件工程管理,代码的可读性和可维护性至关重要。MySQL 8.0在这方面向前一步,包括重构代码、使用STL容器、优化宏定义、std::thread等。本文向大家介绍一下8.0中的scope_guard功能。

scope_guard是什么

Scope_guard顾名思义,针对某个scope的一个guard。假设我们有如下一段代码逻辑:

if (⟨action⟩) {
  if (!⟨next⟩) {
    ⟨rollback⟩
  }
  ⟨cleanup⟩
}

以上代码非常健壮,包含了错误处理(rollback)和资源清理(cleanup)。不过缺点老生常谈,如果嵌套更多if条件,代码会变得臃肿。一般解法是用RAII,RAII自动管理cleanup,并用try-catch或类似方法统一处理rollback,来规避多层嵌套:

class RAII {
  RAII() { ⟨action⟩ }
  ~RAII() { ⟨cleanup⟩ }
};
...
RAII raii;
try {
  ⟨next⟩
} catch (...) {
  ⟨rollback⟩
  throw;
}

我们的需求是,如果⟨next⟩出错,调用⟨rollback⟩,当任务结束,调用⟨cleanup⟩。scope_guard是一种轻量级的RAII,实现同样功能的伪代码如下,是不是简洁很多:

⟨action⟩
auto g1 = scopeGuard([] { ⟨cleanup⟩ });
auto g2 = scopeGuard([] { ⟨rollback⟩ });
⟨next⟩
g2.dismiss();

MySQL中的scope_guard

MySQL里面scope_guard的代码很简单,以下摘抄自8.0源码中的include/scope_guard.h。

template <typename TLambda>
class Scope_guard {
 public:
  Scope_guard(const TLambda &rollback_lambda)
      : m_committed(false), m_rollback_lambda(rollback_lambda) {}
  Scope_guard(const Scope_guard<TLambda> &) = delete;
  Scope_guard(Scope_guard<TLambda> &&moved)
      : m_committed(moved.m_committed),
        m_rollback_lambda(moved.m_rollback_lambda) {
    moved.m_committed = true;
  }
  ~Scope_guard() {
    if (!m_committed) {
      m_rollback_lambda();
    }
  }

  inline void commit() { m_committed = true; }

  inline void rollback() {
    if (!m_committed) {
      m_rollback_lambda();
      m_committed = true;
    }
  }

 private:
  bool m_committed;
  const TLambda m_rollback_lambda;
};

template <typename TLambda>
Scope_guard<TLambda> create_scope_guard(const TLambda rollback_lambda) {
  return Scope_guard<TLambda>(rollback_lambda);
}

简单分析下这段代码。最下面的函数create_scope_guard是个模板函数,接受const TLambda类型的参数,创建一个Scope_guard对象。Scope_guard类中的m_committed,控制m_rollback_lambda在生命周期内最多允许调用一次。从析构函数和rollback()中可以看出,TLambda类型需要实现operator(),因此应该是一个function或者functor。Scope_guard的行为是除了显式调用commit(),最终在生命周期结束之前会执行一次rollback_lambda。commit的功能类似于前一节伪代码的dismiss,允许某些逻辑放弃lambda的回调。此外create_scope_guard函数本身就包括Scope_guard对象的一个作用域,因此Scope_guard类中实现move构造函数就显得非常必要。

案例一:资源管理

那么Scope_guard具体有什么用,我们可以从MySQL里面看出一些端倪。第一个典型的场景在sql/xa.cc中的find_trn_for_recover_and_check_its_state函数。这个函数比较短,我删除了一些无关的debug代码和注释,剩余摘录如下:

static std::shared_ptr<Transaction_ctx>
find_trn_for_recover_and_check_its_state(THD *thd,
                                         xid_t *xid_for_trn_in_recover,
                                         XID_STATE *xid_state) {
  if (!xid_state->has_state(XID_STATE::XA_NOTR)) {
    my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
    return nullptr;
  }

  mysql_mutex_lock(&LOCK_transaction_cache);
  auto grd =
      create_scope_guard([]() { mysql_mutex_unlock(&LOCK_transaction_cache); });

  auto foundit = transaction_cache.find(to_string(*xid_for_trn_in_recover));
  if (foundit == transaction_cache.end()) {
    my_error(ER_XAER_NOTA, MYF(0));
    return nullptr;
  }

  const XID_STATE *xs = foundit->second->xid_state();
  if (!xs->get_xid()->eq(xid_for_trn_in_recover) || !xs->is_in_recovery()) {
    my_error(ER_XAER_NOTA, MYF(0));
    return nullptr;
  }
  if (thd->in_active_multi_stmt_transaction()) {
    my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
    return nullptr;
  }

  return foundit->second;
}

互斥锁LOCK_transaction_cache用来保护transaction_cache的并发访问。原则上,lock执行后,代码最后4处return,都应该调用unlock释放锁。通过给create_scope_guard传一个带有unlock逻辑的lambda表达式,借助Scope_guard的析构函数,锁释放就被自动处理了。由于MySQL中的mutex都是采用内部的mysql_mutex_t类型,跨平台且集成了performance_schema的性能诊断,无法直接使用C++标准库中的std::lock_guard,为mysql_mutex_t单独实现RAII又涉及面太广,create_scope_guard就是这里的瑞士军刀。

案例二:错误处理

另一个样例来源于sql/sql_tmp_table.cc中的create_tmp_table函数。该函数超级长,简化逻辑如下:

TABLE *create_tmp_table(THD *thd, Temp_table_param *param,
                        const mem_root_deque<Item *> &fields, ORDER *group,
                        bool distinct, bool save_sum_fields,
                        ulonglong select_options, ha_rows rows_limit,
                        const char *table_alias) {
  ... // skip 73 lines of code

  table->init_tmp_table(thd, share, &own_root, param->table_charset,
                        table_alias, reg_field, blob_field, false);

  auto free_tmp_table_guard = create_scope_guard([table] {
    close_tmp_table(table);
    free_tmp_table(table);
  });

  ... // skip 641 lines of code

  free_tmp_table_guard.commit();

  return table;
}

这段代码是Scope_guard在错误处理方面的典型应用。代码需求是,当函数异常退出的时候,执行close_tmp_table和free_tmp_table的回滚操作;如果函数成功执行,则直接返回这个table对象。MySQL的很多代码由于历史演进背景,以及基础软件不可逃避的复杂性本质,很多函数称得上是“又臭又长”。我统计了一下,create_tmp_table这个函数总共有732行,在init_tmp_table和最后的return table中间,竟然有16个return nullptr的异常逻辑。感谢Scope_guard,否则这16个异常逻辑都要补上回滚操作的两行代码。更不用提未来会有其他开发者在中间的600多行中添加了新的错误处理逻辑,一不小心很容易出错。

案例三:状态维护

MySQL的InnoDB存储引擎代码也有Scope_guard的应用,不过在类定义上做了一些改动:

class bool_scope_guard_t {
  bool *m_active;
 public:
  explicit bool_scope_guard_t(bool &active) : m_active(&active) {
    *m_active = true;
  }
  ~bool_scope_guard_t() {
    if (m_active != nullptr) {
      *m_active = false;
      m_active = nullptr;
    }
  }
  bool_scope_guard_t(bool_scope_guard_t const &) = delete;
  bool_scope_guard_t &operator=(bool_scope_guard_t const &) = delete;
  bool_scope_guard_t &operator=(bool_scope_guard_t &&) = delete;
  bool_scope_guard_t(bool_scope_guard_t &&old) {
    m_active = old.m_active;
    old.m_active = nullptr;
  }
};

bool_scope_guard_t确保了一个bool类型的变量在某段作用域内始终是true(不过其实bool_scope_guard_t也可以复用Scope_guard那个大类)。这个类使用在如下代码中:

struct row_prebuilt_t {
 ... // skip some code

 private:
  /** Set to true iff we are inside read_range_first() or read_range_next() */
  bool m_is_reading_range;

 public:
  bool is_reading_range() const { return m_is_reading_range; }

  class row_is_reading_range_guard_t : private ut::bool_scope_guard_t {
   public:
    explicit row_is_reading_range_guard_t(row_prebuilt_t &prebuilt)
        : ut::bool_scope_guard_t(prebuilt.m_is_reading_range) {}
  };

  row_is_reading_range_guard_t get_is_reading_range_guard() {
    return row_is_reading_range_guard_t(*this);
  }
}

int ha_innobase::read_range_first(const key_range *start_key,
                                  const key_range *end_key, bool eq_range_arg,
                                  bool sorted) {
  auto guard = m_prebuilt->get_is_reading_range_guard();
  return handler::read_range_first(start_key, end_key, eq_range_arg, sorted);
}

int ha_innobase::read_range_next() {
  auto guard = m_prebuilt->get_is_reading_range_guard();
  return (handler::read_range_next());
}

row_prebuilt_t这个struct包含m_is_reading_range标记位,is_reading_range()可以判断当前是否在执行read_range_first或read_range_next。对应的这两个ha_innobase接口通过get_is_reading_range_guard获取private继承自bool_scope_guard_t的row_is_reading_range_guard_t,来管理m_is_reading_range的状态。至于is_reading_range具体到InnoDB中是用来做什么的,这其实是8.0修复的一个历史bug(Bug #29508068):对于SELECT…FOR UPDATE在PK/UK范围扫描遍历的过程中,会一直加next key lock包括第一个不满足条件的记录,相当于在最后一条记录上多加了没必要的锁。这段代码是这个bug修复的一部分。

对于这个“状态维护”的应用场景,我举一个更直观的例子。很多系统都会有后台GC的任务,比如单独起一个线程每隔几秒调用一次gc_work()。假设我需要一个监控项gc_running标记当前是否在GC过程中,可以这么简化实现:

// garbage collection
bool gc_running;
void gc_work() {
  gc_running = true;
  auto guard = create_scope_guard([]() { gc_running = false; });
  ... // do something
  return;
}
// invoke gc_work() periodically

scope_guard的小扩展

MySQL中的Scope_guard算是一个初级版,如果想更适配C++11风格的话,可以用另一种写法(来自参考资料[1]):

template<typename Fun>
class ScopeGuard {
  Fun f_;
  bool active_;
 public:
  ScopeGuard(Fun f) : f_(std::move(f)), active_(true) {}
  ~ScopeGuard() { if (active_) f_(); }
  void dismiss() { active_ = false; }

  ScopeGuard() = delete;
  ScopeGuard(const ScopeGuard &) = delete;
  ScopeGuard& operator=(const ScopeGuard &) = delete;
  ScopeGuard(ScopeGuard&&rhs) : f_(std::move(rhs.f_)), active_(rhs.active_) {
      rhs.dismiss();
  }
};

template<typename Fun>
ScopeGuard<Fun> scopeGuard(Fun f) {
  return ScopeGuard<Fun>(std::move(f));
}

namespace detail {
    enum class ScopeGuardOnExit {};
    template<typename Fun>
    ScopeGuard<Fun> operator+(ScopeGuardOnExit, Fun&& fn)
    {
        return ScopeGuard<Fun>(std::forward < Fun > (fn));
    }
}

// Helper macro
#define SCOPE_EXIT \
auto ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE) =::detail::ScopeGuardOnExit()+[&]()

#define CONCATENATE_IMPL(s1,s2) s1##s2
#define CONCATENATE(s1,s2) CONCATENATE_IMPL(s1,s2)

#ifdef __COUNTER__
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__COUNTER__)
#else
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__LINE__)
#endif

最上面主要的Class和MySQL中的类似,忽略变量名的差异,改动有两块:1. 模板参数从const引用变成std::move传进来,减少不必要的对象销毁;2. 通过=delete的语法禁用默认构造函数、copy构造函数和copy赋值操作,防止误用。之前MySQL的版本,创建scope_guard需要调用create_scope_guard返回一个变量,即使之后代码完全不再用到,也得专门起个名字,是比较麻烦的。新的代码新加了一些宏定义的黑科技来解决这个问题,如果要读懂的话,你要了解宏定义展开、宏定义中的##语法、operator+操作符重载、编译器的预定义宏__COUNTER__等知识,大家可以自行分析。

简化后的一种代码样例如下:

void fun() {
  char name[] = "/tmp/test.xxx";
  auto fd = mkstemp(name);
  SCOPE_EXIT { fclose(fd); unlink(name); };
  auto buf = malloc(1024 * 1024);
  SCOPE_EXIT { free(buf); };

  ...  use fd and buf ...
}

总结

本质上来讲,scope_guard相当于把很多工作交给了编译器,如错误处理的延迟回调、资源释放、提供RAII。对此还有一个新的概念叫Declarative Control Flow。如果想看更丰富的ScopeGuard写法可以参考[2][3]。如何借助scope_guard提高复杂代码的可维护性,是个很有趣的问题,感兴趣的朋友可以从参考资料[4][5]中找到更多的灵感。

参考资料

[1] ScopeGuard C++11基础版 https://github.com/joker-eph/ScopeGuard11/blob/master/ScopeGuard.hpp
[2] ScopeGuard C++11完整版 https://github.com/Neargye/scope_guard
[3] ScopeGuard folly版 https://github.com/facebook/folly/blob/main/folly/ScopeGuard.h
[4] C++ and Beyond 2012: Andrei Alexandrescu - Systematic Error Handling in C++
[5] CppCon 2015: Andrei Alexandrescu “Declarative Control Flow