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。假设我们有如下一段代码逻辑:

  1. if (⟨action⟩) {
  2. if (!⟨next⟩) {
  3. rollback
  4. }
  5. cleanup
  6. }

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

  1. class RAII {
  2. RAII() { action }
  3. ~RAII() { cleanup }
  4. };
  5. ...
  6. RAII raii;
  7. try {
  8. next
  9. } catch (...) {
  10. rollback
  11. throw;
  12. }

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

  1. action
  2. auto g1 = scopeGuard([] { cleanup });
  3. auto g2 = scopeGuard([] { rollback });
  4. next
  5. g2.dismiss();

MySQL中的scope_guard

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

  1. template <typename TLambda>
  2. class Scope_guard {
  3. public:
  4. Scope_guard(const TLambda &rollback_lambda)
  5. : m_committed(false), m_rollback_lambda(rollback_lambda) {}
  6. Scope_guard(const Scope_guard<TLambda> &) = delete;
  7. Scope_guard(Scope_guard<TLambda> &&moved)
  8. : m_committed(moved.m_committed),
  9. m_rollback_lambda(moved.m_rollback_lambda) {
  10. moved.m_committed = true;
  11. }
  12. ~Scope_guard() {
  13. if (!m_committed) {
  14. m_rollback_lambda();
  15. }
  16. }
  17. inline void commit() { m_committed = true; }
  18. inline void rollback() {
  19. if (!m_committed) {
  20. m_rollback_lambda();
  21. m_committed = true;
  22. }
  23. }
  24. private:
  25. bool m_committed;
  26. const TLambda m_rollback_lambda;
  27. };
  28. template <typename TLambda>
  29. Scope_guard<TLambda> create_scope_guard(const TLambda rollback_lambda) {
  30. return Scope_guard<TLambda>(rollback_lambda);
  31. }

简单分析下这段代码。最下面的函数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代码和注释,剩余摘录如下:

  1. static std::shared_ptr<Transaction_ctx>
  2. find_trn_for_recover_and_check_its_state(THD *thd,
  3. xid_t *xid_for_trn_in_recover,
  4. XID_STATE *xid_state) {
  5. if (!xid_state->has_state(XID_STATE::XA_NOTR)) {
  6. my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
  7. return nullptr;
  8. }
  9. mysql_mutex_lock(&LOCK_transaction_cache);
  10. auto grd =
  11. create_scope_guard([]() { mysql_mutex_unlock(&LOCK_transaction_cache); });
  12. auto foundit = transaction_cache.find(to_string(*xid_for_trn_in_recover));
  13. if (foundit == transaction_cache.end()) {
  14. my_error(ER_XAER_NOTA, MYF(0));
  15. return nullptr;
  16. }
  17. const XID_STATE *xs = foundit->second->xid_state();
  18. if (!xs->get_xid()->eq(xid_for_trn_in_recover) || !xs->is_in_recovery()) {
  19. my_error(ER_XAER_NOTA, MYF(0));
  20. return nullptr;
  21. }
  22. if (thd->in_active_multi_stmt_transaction()) {
  23. my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
  24. return nullptr;
  25. }
  26. return foundit->second;
  27. }

互斥锁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函数。该函数超级长,简化逻辑如下:

  1. TABLE *create_tmp_table(THD *thd, Temp_table_param *param,
  2. const mem_root_deque<Item *> &fields, ORDER *group,
  3. bool distinct, bool save_sum_fields,
  4. ulonglong select_options, ha_rows rows_limit,
  5. const char *table_alias) {
  6. ... // skip 73 lines of code
  7. table->init_tmp_table(thd, share, &own_root, param->table_charset,
  8. table_alias, reg_field, blob_field, false);
  9. auto free_tmp_table_guard = create_scope_guard([table] {
  10. close_tmp_table(table);
  11. free_tmp_table(table);
  12. });
  13. ... // skip 641 lines of code
  14. free_tmp_table_guard.commit();
  15. return table;
  16. }

这段代码是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的应用,不过在类定义上做了一些改动:

  1. class bool_scope_guard_t {
  2. bool *m_active;
  3. public:
  4. explicit bool_scope_guard_t(bool &active) : m_active(&active) {
  5. *m_active = true;
  6. }
  7. ~bool_scope_guard_t() {
  8. if (m_active != nullptr) {
  9. *m_active = false;
  10. m_active = nullptr;
  11. }
  12. }
  13. bool_scope_guard_t(bool_scope_guard_t const &) = delete;
  14. bool_scope_guard_t &operator=(bool_scope_guard_t const &) = delete;
  15. bool_scope_guard_t &operator=(bool_scope_guard_t &&) = delete;
  16. bool_scope_guard_t(bool_scope_guard_t &&old) {
  17. m_active = old.m_active;
  18. old.m_active = nullptr;
  19. }
  20. };

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

  1. struct row_prebuilt_t {
  2. ... // skip some code
  3. private:
  4. /** Set to true iff we are inside read_range_first() or read_range_next() */
  5. bool m_is_reading_range;
  6. public:
  7. bool is_reading_range() const { return m_is_reading_range; }
  8. class row_is_reading_range_guard_t : private ut::bool_scope_guard_t {
  9. public:
  10. explicit row_is_reading_range_guard_t(row_prebuilt_t &prebuilt)
  11. : ut::bool_scope_guard_t(prebuilt.m_is_reading_range) {}
  12. };
  13. row_is_reading_range_guard_t get_is_reading_range_guard() {
  14. return row_is_reading_range_guard_t(*this);
  15. }
  16. }
  17. int ha_innobase::read_range_first(const key_range *start_key,
  18. const key_range *end_key, bool eq_range_arg,
  19. bool sorted) {
  20. auto guard = m_prebuilt->get_is_reading_range_guard();
  21. return handler::read_range_first(start_key, end_key, eq_range_arg, sorted);
  22. }
  23. int ha_innobase::read_range_next() {
  24. auto guard = m_prebuilt->get_is_reading_range_guard();
  25. return (handler::read_range_next());
  26. }

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过程中,可以这么简化实现:

  1. // garbage collection
  2. bool gc_running;
  3. void gc_work() {
  4. gc_running = true;
  5. auto guard = create_scope_guard([]() { gc_running = false; });
  6. ... // do something
  7. return;
  8. }
  9. // invoke gc_work() periodically

scope_guard的小扩展

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

  1. template<typename Fun>
  2. class ScopeGuard {
  3. Fun f_;
  4. bool active_;
  5. public:
  6. ScopeGuard(Fun f) : f_(std::move(f)), active_(true) {}
  7. ~ScopeGuard() { if (active_) f_(); }
  8. void dismiss() { active_ = false; }
  9. ScopeGuard() = delete;
  10. ScopeGuard(const ScopeGuard &) = delete;
  11. ScopeGuard& operator=(const ScopeGuard &) = delete;
  12. ScopeGuard(ScopeGuard&&rhs) : f_(std::move(rhs.f_)), active_(rhs.active_) {
  13. rhs.dismiss();
  14. }
  15. };
  16. template<typename Fun>
  17. ScopeGuard<Fun> scopeGuard(Fun f) {
  18. return ScopeGuard<Fun>(std::move(f));
  19. }
  20. namespace detail {
  21. enum class ScopeGuardOnExit {};
  22. template<typename Fun>
  23. ScopeGuard<Fun> operator+(ScopeGuardOnExit, Fun&& fn)
  24. {
  25. return ScopeGuard<Fun>(std::forward < Fun > (fn));
  26. }
  27. }
  28. // Helper macro
  29. #define SCOPE_EXIT \
  30. auto ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE) =::detail::ScopeGuardOnExit()+[&]()
  31. #define CONCATENATE_IMPL(s1,s2) s1##s2
  32. #define CONCATENATE(s1,s2) CONCATENATE_IMPL(s1,s2)
  33. #ifdef __COUNTER__
  34. #define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__COUNTER__)
  35. #else
  36. #define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__LINE__)
  37. #endif

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

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

  1. void fun() {
  2. char name[] = "/tmp/test.xxx";
  3. auto fd = mkstemp(name);
  4. SCOPE_EXIT { fclose(fd); unlink(name); };
  5. auto buf = malloc(1024 * 1024);
  6. SCOPE_EXIT { free(buf); };
  7. ... use fd and buf ...
  8. }

总结

本质上来讲,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