CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • MySQL开发与调试指南 说明
  • 第一章 MySQL的历史与架构
  • 第二章 使用MySQL源代码的基础操作
  • 第三章 核心类、结构、变量和API
  • 第四章 客户端/服务器通信
  • 第五章 配置变量
  • 第六章 基于线程的请求处理
    • 线程与进程
      • 使用线程的优点
      • 使用线程的缺点
      • 使用fork进程的优点
      • 使用fork进程的缺点
    • 请求处理的实现
      • 结构、变量、类和API
      • 执行过程详解
    • 线程编程问题
      • 标准C库调用
      • 互斥锁(Mutexes)
      • 读写锁
      • 同步
      • 抢占
  • 第七章 存储引擎接口
  • 第八章 并发访问与锁机制
  • 第九章 解析器与优化器
  • 第十章 存储引擎
  • 第十一章 事务
  • 第十二章 复制
目录

第六章 基于线程的请求处理

# 第六章 基于线程的请求处理

在实现服务器时,程序员会面临一个两难选择:是使用线程还是进程来处理请求。二者各有优缺点。从一开始,MySQL就选择了使用线程。在本章中,我们将讨论MySQL服务器中基于线程的请求处理的基本原理、优缺点以及具体实现。

# 线程与进程

进程和线程之间最重要的区别或许在于,子线程与父线程共享堆(全局程序数据),而子进程并不共享。这在你决定使用哪种模型时会产生诸多影响。

# 使用线程的优点

线程在编程库和操作系统中得到广泛应用,原因如下:

  • 降低内存使用。创建一个新线程的内存开销仅限于线程栈和线程管理器所需的一些簿记内存。
  • 访问服务器全局数据无需复杂技术。如果数据可能会被另一个并发运行的线程修改,只需使用互斥锁(本章后面会介绍)保护相关部分即可。如果不存在这种可能性,访问全局数据时就像不存在线程一样正常访问。
  • 创建线程比创建进程耗时少得多,因为无需复制可能非常大的堆段。
  • 内核在线程之间进行上下文切换的时间比在进程之间切换的时间少。这为负载繁重的服务器留出了更多CPU时间来处理任务。

# 使用线程的缺点

尽管线程在现代计算中很重要,但它们也存在一些缺陷:

  • 编程错误的代价高昂。如果一个线程崩溃,可能会导致整个服务器宕机。一个有问题的线程可能会损坏全局数据,致使其他线程出现故障。
  • 容易出现编程错误。程序员必须时刻考虑其他线程可能带来麻烦的情况,并思考如何避免。这需要采用额外的防御性编程方法。
  • 多线程服务器因同步错误而声名狼藉,这类错误在测试中几乎无法复现,但却会在生产环境中出现在极不合适的时间。出现这类错误的概率较高,是因为线程共享地址空间,这使得线程之间的交互程度更高。
  • 互斥锁争用可能会失去控制。如果过多线程同时尝试获取同一个互斥锁,可能会导致过度的上下文切换,大量CPU时间花费在内核调度器上,留给实际工作的时间就很少了 。
  • 32位系统每个进程的地址空间限制为4GB。由于所有线程共享相同的地址空间,理论上即使有更多的物理内存可用,整个服务器也被限制在4GB的RAM范围内。实际上,在x86 Linux系统中,地址空间在远小于4GB的某个限制(大约1.5GB)时就开始变得非常紧张。
  • 拥挤的32位地址空间还带来另一个问题。每个线程都需要一定的栈空间。当分配一个栈时,即使线程没有使用大部分分配的空间,服务器的地址空间也必须为其保留。每个新栈都会减少堆的可用空间。因此,即使可能有大量的物理内存,也可能无法同时拥有大缓冲区、大量并发线程,以及为每个线程提供充足的栈空间。

# 使用fork进程的优点

线程的缺点恰恰对应了使用多个进程的优点:

  • 编程错误不会那么致命。虽然也有可能出现问题,但一个有问题的fork服务器进程不太容易破坏整个服务器。
  • 编程错误发生的可能性要小得多。大多数时候,程序员只需考虑一个执行线程,无需担心可能的并发干扰。
  • 极少出现难以捉摸的错误。如果一个错误出现一次,通常很容易复现。由于每个fork进程都有自己的地址空间,它们之间的交互很少。
  • 在32位系统中,地址空间不足的问题通常不会那么严重。

# 使用fork进程的缺点

为了全面概述,下面列出使用多个进程存在的问题,这些问题与线程的优点形成对比:

  • 内存利用效率不高。fork子进程时,可能会不必要地复制较大的内存段。
  • 在进程之间共享数据需要特殊技术。这使得访问服务器的全局数据变得繁琐。
  • 创建进程在内核中比创建线程需要更多开销。一个很大的性能损耗是需要复制父进程的数据段。不过,Linux通过实现写时复制(copy-on-write)在一定程度上解决了这个问题。在子进程或父进程修改页面之前,不会实际复制父进程的页面,在此之前,二者使用相同的页面。
  • 进程之间的上下文切换比线程之间更耗时,因为内核需要切换页面、文件描述符表和其他额外的上下文信息。这样留给服务器实际处理工作的时间就更少了。

总之,当连接处理程序之间需要共享大量数据,且编程技术有保障时,多线程服务器是比较理想的选择。在为MySQL选择合适的模型时,答案很明确。数据库服务器需要有大量的共享缓冲区和其他共享数据。

就编程技术而言,MySQL的开发者们并不欠缺。就像优秀的骑手与马融为一体一样,蒙蒂(Monty)与计算机也配合默契。看到系统资源被浪费,他会感到痛心。他有足够的信心编写几乎没有错误的代码,处理线程带来的并发问题,甚至能应对较小的栈空间。这是多么令人兴奋的挑战!不用说,他选择了线程。

# 请求处理的实现

服务器在主线程中监听连接。对于每个连接,它会分配一个线程来处理。根据服务器的配置设置和当前状态,这个线程既可以是新创建的,也可以从线程缓存中分配。客户端发出请求,服务器响应该请求,直到客户端发送会话终止命令(COM_QUIT)或会话异常结束。在终止客户端会话时,根据服务器的配置设置和状态,线程可能会终止,也可能进入线程缓存等待处理新的请求。

# 结构、变量、类和API

对于线程来说,最重要的类或许是THD,它是一个用于线程描述符的类。解析器和优化器中的几乎每个服务器函数都接受一个THD对象作为参数,并且它通常位于参数列表的首位。第3章将详细讨论THD类。

每当创建一个线程,其描述符就会被放入一个全局线程列表I_List<THD> threads中(I_List<>是一个链表类模板,详见sql/sql_list.h和sql/sql_list.cc)。这个列表主要用于以下三个目的:

  • 为SHOW PROCESSLIST命令提供数据。
  • 在执行KILL命令时定位目标线程。
  • 在关机期间向所有线程发送终止信号。

另一个I_List<THD>列表也起着重要作用:thread_cache。它的实际使用方式有些出人意料:它作为一种手段,将主线程实例化的THD对象传递给在线程缓存中等待、被分配来处理当前请求的线程。详细内容可查看sql/mysqld.cc中的create_new_thread()、start_cached_thread()和end_thread()函数 。

所有与创建、终止或跟踪线程相关的操作都由互斥锁LOCK_thread_count保护。有三个POSIX线程条件变量与线程配合使用。COND_thread_count在关机期间用于同步,确保所有线程在主线程终止前完成工作并退出。当主线程决定唤醒一个缓存线程并分配它来处理当前客户端会话时,会广播COND_thread_cache。缓存线程在关机期间或处理SIGHUP信号时,使用COND_flush_thread_cache来表明它们即将退出。

此外,还有一些与线程相关的全局状态变量,总结在表6-1中。 表6-1. 与线程相关的全局变量

描述 变量定义
一个用于向所有线程发出信号,表明是时候清理并退出的标志。服务器从不强制线程立即退出,因为不给线程清理的机会就强制退出可能会导致严重的数据损坏。相反,每个线程的代码都被编写为关注环境并在收到请求时退出。 int abort_loop
一个状态变量,用于跟踪已终止并等待被分配来处理新请求的线程数量。可以在SHOW STATUS的输出中Threads_connected这一项查看。 int cached_thread_count
一个标志,表明所有缓存线程都应该退出。缓存线程在end_thread()函数中等待COND_thread_cache。如果它们看到这个标志被设置,就会退出。 int kill_cached_threads
表6-1. 与线程相关的全局变量(续)
变量定义 描述
--- ---
int max_connections 一个服务器配置变量,用于设置服务器愿意接受的非管理客户端连接的最大数量限制。一旦达到这个限制,仍允许一个额外的管理连接,以便数据库管理员(DBA)有机会解决因达到限制而引发的问题。
设置这个限制的目的是让服务器在因使用过多资源导致系统崩溃之前自我限制。
该限制由配置变量max_connections控制,默认值为100。
int max_used_connections 一个状态变量,用于跟踪服务器启动以来经历的最大并发连接数。可以在SHOW STATUS的输出中Max_used_connections这一项查看其值。
int query_id 一个用于生成唯一查询ID号的变量。每次有查询发送到服务器时,都会被分配该变量的当前值,然后该变量自增1。
int thread_cache_size 一个服务器配置变量,用于指定线程缓存中的最大线程数。如果设置为0(默认值),则禁用线程缓存。由配置变量thread_cache_size控制。
int thread_count 一个状态变量,用于跟踪当前存在的线程数量。可以在SHOW STATUS的输出中Threads_cached这一项查看。
int thread_created 一个状态变量,用于跟踪服务器启动以来创建的线程数量。可以在SHOW STATUS的输出中Threads_created这一项查看。
int thread_id 一个用于生成唯一线程ID号的变量。每次启动一个线程时,都会被分配该变量的当前值,然后该变量自增1。可以在SHOW STATUS的输出中Connections这一项查看。
int thread_running 一个状态变量,用于跟踪当前正在处理查询的线程数量。在sql/sql_parse.cc中的dispatch_command()函数开始时自增1,在函数接近结束时自减1。可以在SHOW STATUS的输出中Threads_running这一项查看。

# 执行过程详解

标准的select()/accept()请求分发循环位于sql/mysqld.cc中的handle_connections_sockets()函数中。在对各种平台上accept()函数可能出现的问题进行了相当复杂的测试组合之后,我们最终得到以下代码段:

if (!(thd = new THD)) {
    (void)shutdown(new_sock, 2);
    VOID(closesocket(new_sock));
    continue;
}
1
2
3
4
5

这创建了一个THD实例。在对THD对象进行一些额外操作后,执行进入同一文件sql/mysqld.cc中的create_new_thread()函数。经过一些额外的检查和初始化后,我们到达一个条件判断,该判断决定如何获取请求处理线程。有两种可能:使用缓存线程或创建新线程。

启用线程缓存后,一个处理完客户端请求的旧线程不会退出,而是进入睡眠状态。当有新客户端连接时,服务器不会直接创建新线程,而是首先检查缓存中是否有睡眠线程。如果有,它会唤醒其中一个,并将THD实例作为参数传递给它。

虽然缓存线程可以在负载较重的系统上提高性能,但该功能最初的动机是为了解决Alpha系统上Linux的一个计时问题。

或者,如果禁用了线程缓存,或者没有可用的缓存线程,则必须创建一个新线程来处理请求。

通过以下测试来做出决定:

if (cached_thread_count > wake_thread) {
    start_cached_thread(thd);
}
1
2
3

sql/mysqld.cc中的start_cached_thread()函数会唤醒当前未处理请求的线程(如果存在这样的线程)。cached_thread_count > wake_thread这个条件确保存在睡眠线程,所以如果没有缓存线程处于睡眠状态,该函数永远不会被调用。这也涵盖了线程缓存被禁用的情况。

如果缓存线程可用性测试结果为否,代码会进入else部分,在下面这行代码中完成创建新线程的工作:

if ((error = pthread_create(&thd->real_id, &connection_attrib, handle_one_connection, (void*)thd)))
1

新线程从sql/sql_parse.cc中的handle_one_connection()函数开始执行。

handle_one_connection()函数在进行一些检查和初始化后,开始处理业务:

while (!net->error && net->vio != 0 &&!thd->killed) {
    if (do_command(thd))
        break;
}
1
2
3
4

只要没有遇到循环退出条件,就会持续接受并处理命令。可能的退出条件包括:

  • 网络错误。
  • 数据库管理员使用KILL命令终止线程,或者服务器在关机期间自行终止线程。
  • 客户端发送COM_QUIT请求,告知服务器会话结束,在这种情况下,sql/sql_parse.cc中的do_command()函数会返回非零值。
  • do_command()由于其他原因返回非零值。目前,唯一的其他可能性是复制主服务器决定中止从服务器(或伪装成从服务器的客户端)通过COM_BINLOG_DUMP请求的更新数据传输。

之后,handle_one_connection()函数进入线程终止/清理阶段。这段代码的关键部分是调用sql/mysqld.cc中的end_thread()函数。

end_thread()函数首先进行一些额外的清理工作,但随后进入有趣的部分:决定是否将当前执行的线程放入线程缓存。通过测试以下条件来做出决定:

if (put_in_cache && cached_thread_count < thread_cache_size &&!abort_loop &&!kill_cached_threads)
1

如果end_thread()函数决定缓存这个线程,会执行以下循环:

while (!abort_loop &&!wake_thread &&!kill_cached_threads)
    (void)pthread_cond_wait(&COND_thread_cache, &LOCK_thread_count);
1
2

这个循环会一直等待,直到被start_cached_thread()函数、SIGHUP信号处理程序或关机例程唤醒。代码可以通过wake_thread的非零设置判断唤醒信号来自start_cached_thread()函数。在这种情况下,它会从thread_cache列表中获取start_cached_thread()函数传递的THD对象,然后返回(注意DBUG_VOID_RETURN宏)到handle_one_connection()函数,开始为新客户端提供服务。

如果线程没有机会进入线程缓存,它将通过pthread_exit()函数终止。

# 线程编程问题

MySQL面临着与其他依赖线程的程序相同的许多复杂情况。

# 标准C库调用

在编写可由多个线程并发执行的代码时,调用外部库中的函数需要格外小心。被调用的代码总是有可能使用全局变量、写入共享文件描述符,或者在未确保互斥的情况下使用其他共享资源。如果是这种情况,我们必须使用互斥锁(mutex)来保护调用。

在谨慎行事的同时,MySQL也必须避免不必要的保护,否则会导致性能下降。例如,我们通常认为malloc()函数是线程安全的。其他可能存在线程不安全问题的函数,如gethostbyname(),通常都有线程安全的替代函数。MySQL的构建配置脚本会测试这些替代函数是否可用,并尽可能使用它们。如果未检测到合适的线程安全替代函数,作为最后手段,会启用保护性互斥锁。

总体而言,MySQL通过在mysys中的可移植性包装器和strings下的字符串库中实现许多标准C库的等效功能,减少了很多线程安全方面的担忧。即使最终确实调用了C库函数,大多数情况下也是通过包装器进行的。如果在某个系统上,某个调用意外地缺乏线程安全性,只需在包装器中添加一个保护性互斥锁,就可以轻松解决问题。

# 互斥锁(Mutexes)

在多线程服务器中,多个线程可能会访问共享数据。如果是这样,每个线程都必须确保访问是互斥的。这是通过互斥锁(也称为mutex)来实现的。

随着应用程序复杂程度的增加,在使用多少个互斥锁以及哪些互斥锁应该保护哪些数据方面,你会面临两难选择。在一个极端情况下,你可以为每个变量单独设置一个互斥锁。这样做的优点是将互斥锁争用降至最低,但也存在一些问题。如果你需要原子性地访问一组变量会怎样呢?你必须为每个单独的变量获取一个互斥锁。如果这样做,你必须确保始终以相同的顺序获取它们,以避免死锁。频繁调用pthread_mutex_lock()和pthread_mutex_unlock()会导致性能下降,而且程序员很可能会在调用顺序上出错,从而导致死锁。

在另一个极端情况下,是对所有内容使用单个互斥锁。这对程序员来说非常简单 —— 访问全局变量时获取锁,完成后释放锁。不幸的是,这种方法对性能有非常负面的影响。当一个线程访问其他线程不需要保护的某个变量时,许多线程会不必要地等待。

解决方案是对全局变量进行某种平衡的分组,并为每个组设置一个互斥锁。这就是MySQL解决此问题的方法。

表6 - 2列出了MySQL中的全局互斥锁,并对它们所保护的相应变量组进行了描述。 表6 - 2:全局互斥锁

互斥锁名称 互斥锁描述
LOCK_Acl 在代码中已初始化但目前未使用。未来可能会被移除。
LOCK_active_mi 保护active_mi指针,该指针指向活动的复制从服务器描述符。目前,这种保护是多余的,因为active_mi的值永远不会被并发更改。但是,当添加多主服务器支持时,这种保护将变得必要。
LOCK_bytes_received 保护bytes_received状态变量,该变量跟踪服务器自启动以来从所有客户端接收的字节数。在5.0及更高版本中未使用。
LOCK_bytes_sent 保护bytes_received状态变量(此处描述与上文LOCK_bytes_received保护的变量描述重复,疑为原文档错误,推测应为保护bytes_sent状态变量,用于跟踪服务器向所有客户端发送的字节数 ),在5.0及更高版本中未使用。
LOCK_crypt 保护对Unix C库函数crypt()的调用,该函数不是线程安全的。
LOCK_delayed_create 保护创建处理延迟插入线程时涉及的变量和结构。即使表被锁定,延迟插入也会立即返回给客户端,在这种情况下,它们会在后台由一个延迟插入线程处理。
LOCK_delayed_insert 保护I_List<delayed_insert> delayed_threads,这是一个延迟插入线程列表。
LOCK_delayed_status 保护用于跟踪延迟插入操作的状态变量。
LOCK_error_log 保护对错误日志的写入操作。
LOCK_gethostbyname_r 在没有原生C库函数gethostbyname_r()的系统中,保护mysys/my_gethostbyname.c中my_gethostbyname_r()内对gethostbyname()的调用。
LOCK_global_system_variables 保护客户端线程对全局配置变量的修改操作。
LOCK_localtime_r 在没有原生C库函数localtime_r()的系统中,保护mysys/my_pthread.c中localtime_r()内对localtime()的调用。
LOCK_manager 保护管理器线程使用的数据结构,管理器线程目前负责定期刷新表(如果flush_time设置不为0),以及清理Berkeley DB日志。
LOCK_mapped_file 保护与内存映射文件操作相关的数据结构和变量。目前代码中存在对此功能的内部支持,但似乎在任何地方都未使用。
LOCK_open 保护与表缓存以及表的打开和关闭相关的数据结构和变量。
LOCK_rpl_status 保护rpl_status变量,该变量旨在用于故障安全自动恢复复制。目前,这部分代码大多已不再使用。
LOCK_status 保护SHOW STATUS输出中显示的变量。
LOCK_thread_count 保护与线程创建或销毁相关的变量和数据结构。
LOCK_uuid_generator 保护UUID() SQL函数使用的变量和数据结构。
THR_LOCK_charset 保护与字符集操作相关的变量和数据结构。
THR_LOCK_heap 保护与内存(MEMORY)存储引擎相关的变量和数据结构。
THR_LOCK_isam 保护与ISAM存储引擎相关的变量和数据结构。
THR_LOCK_lock 保护与表锁管理器相关的变量和数据结构。
THR_LOCK_malloc 保护与malloc()函数族调用包装器相关的变量和数据结构。主要用于malloc()的调试模式版本(见mysys/safemalloc.c)。
THR_LOCK_myisam 保护与MyISAM存储引擎相关的变量和数据结构。
THR_LOCK_net 目前用于保护mysys/my_net.c中my_inet_ntoa()内对inet_ntoa()的调用。
THR_LOCK_open 保护跟踪打开文件的变量和数据结构。

除了全局互斥锁,还有许多封装在类/结构中的互斥锁,用于保护该特定结构或类的部分内容。mysys库中也有几个文件作用域的全局(静态)互斥锁。

# 读写锁

互斥锁并不总是保护并发敏感操作的最佳解决方案。想象这样一种情况:某个变量仅由一个线程偶尔修改,但经常被许多其他线程读取。如果我们使用互斥锁,大多数时候,一个读者线程最终会等待另一个读者线程完成读取,即使它本可以并发执行。

还有另一种类型的锁更适合这种情况:读写锁。读锁可以共享,而写锁是排他的。因此,只要没有写操作,多个读操作可以并发进行。

显然,读写锁能够完成互斥锁能做的所有事情,甚至更多。为什么不一直使用读写锁呢?俗话说,天下没有免费的午餐,在这种情况下也是如此。额外的功能是以更高的实现复杂度为代价的。因此,即使立即获取到锁,读写锁也需要更多的CPU周期。

因此,在选择使用哪种类型的锁时,必须考虑首次尝试获取锁失败的概率,以及从互斥锁改为读写锁后,这种概率会如何降低。例如,如果典型的使用场景是每1000次尝试中有1次失败,那么读写锁在每999次浪费CPU资源的同时,仅有1次能帮助提高并发性。即使将读写锁的失败概率降低到几乎为零,也可能不值得这样做。

然而,如果首次尝试失败的概率仅为十分之一,那么尝试读写锁时额外的9次CPU周期,可能会被第10次实际获取到锁时,相比使用互斥锁无需等待那么长时间这一事实所抵消。另一方面,如果在这种特定情况下使用读写锁并不能显著降低首次尝试失败的概率,那么CPU开销可能仍然不值得。

MySQL的大多数关键区域都相当短,这导致首次尝试失败的概率较低。因此,在大多数情况下,互斥锁比读写锁更受青睐。不过,也有少数情况下会使用读写锁。表6 - 3总结了MySQL中使用的读写锁。 表6 - 3:MySQL使用的读写锁

读写锁名称 读写锁描述
LOCK_grant 保护与访问控制相关的变量和数据结构。
LOCK_sys_init_connect 在执行sys_init_connect系统变量描述符存储的命令时,保护该描述符不被修改。sys_init_connect系统变量描述符存储了每次新客户端连接时,根据init-connect配置设置要执行的命令。
LOCK_sys_init_slave 在执行sys_init_slave系统变量描述符存储的命令时,保护该描述符不被修改。sys_init_slave系统变量描述符存储了每次从服务器连接到主服务器时,根据init-slave配置设置要在主服务器上执行的命令。

# 同步

多线程应用程序经常面临线程同步问题。一个线程需要知道另一个线程已经达到了某个状态。POSIX线程提供了一种实现此目的的机制:条件变量(condition variables)。等待某个条件的线程可以调用pthread_cond_wait(),并传入条件变量和在给定上下文中使用的互斥锁。该调用也必须由同一个互斥锁保护。认为自己已达到给定条件的线程可以使用pthread_cond_signal()发出信号,或者使用pthread_cond_broadcast()进行广播。发出信号或广播操作也必须由等待线程在调用pthread_cond_wait()时使用的同一个互斥锁保护。发出信号的条件只会唤醒一个等待该条件的线程,而广播则会唤醒所有等待线程。

MySQL使用了几个POSIX条件变量。它们总结在表6 - 4中。 表6 - 4:MySQL使用的条件变量

条件变量名称 条件变量描述
COND_flush_thread_cache 在清除线程缓存期间,由sql/mysqld.cc中的end_thread()发出信号,用于通知flush_thread_cache()(也在sql/mysqld.cc中)有一个线程已退出。这让flush_thread_cache()有机会唤醒并检查是否还有其他线程需要终止。与LOCK_thread_count互斥锁一起使用。
COND_manager 发出信号以强制管理器线程(见sql/sql_manager.cc)唤醒并执行预定的一组维护任务。目前只有两个可能的任务:清理Berkeley DB日志和刷新表。与LOCK_manager互斥锁一起使用。
COND_refresh 当表缓存中的数据更新时发出信号。与LOCK_open互斥锁一起使用。
COND_thread_count 当创建或销毁线程时发出信号。与LOCK_thread_count互斥锁一起使用。
COND_thread_cache 发出信号以唤醒在线程缓存中等待的线程。与LOCK_thread_count互斥锁一起使用。

除了这些条件变量,许多结构和类使用局部条件来同步对该类或结构的操作。mysys库中也存在几个文件作用域的全局(静态)条件变量。

# 抢占

抢占(Preemption)是指中断一个线程,以便让CPU执行其他任务。MySQL通常采用 “尽责” 的抢占方式。抢占线程会设置适当的标志,通知被抢占的线程需要进行清理、终止或让步。此时,被抢占的线程有责任注意到该消息并遵守。

大多数情况下,这种方法效果很好,但有一个例外。如果被抢占的线程陷入阻塞式I/O操作,它将没有机会检查抢占消息标志。为了解决这个问题,MySQL使用了一种在MySQL开发者术语中称为线程警报(thread alarm)的技术。

即将进入阻塞式I/O的线程会调用thr_alarm(),请求在超时时间后接收警报信号。如果I/O操作在超时之前完成,则使用end_thr_alarm()取消警报。在大多数系统中,警报信号会中断阻塞式I/O,从而使可能被抢占的线程能够检查标志和I/O的错误代码,并采取适当的行动。如果被抢占,通常的行动是清理并退出I/O循环,否则会重试I/O操作。

thr_alarm()和end_thr_alarm()都接受一个警报描述符参数,该参数在首次使用前必须通过调用init_thr_alarm()进行初始化。线程警报例程在mysys/thr_alarm.c中实现。

上次更新: 2025/04/08, 19:40:35
第五章 配置变量
第七章 存储引擎接口

← 第五章 配置变量 第七章 存储引擎接口→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式