MySQL系列:innodb引擎分析之线程并发同步机制
innodb是一个多线程并发的存储引擎,内部的读写都是用多线程来实现的,所以innodb内部实现了一个比较高效的并发同步机制。innodb并没有直接使用系统提供的锁(latch)同步结构,而是对其进行自己的封装和实现优化,但是也兼容系统的锁。我们先看一段innodb内部的注释(MySQL-3.23):
Semaphore operations in operating systems are slow: Solaris on a 1993 Sparc takes 3 microseconds (us) for a lock-unlock pair and Windows NT on a 1995 Pentium takes 20 microseconds for a lock-unlock pair. Therefore, we have toimplement
our own efficient spin lock mutex. Future operating systems mayprovide efficient spin locks, but we cannot count on that.
大概意思是说1995年的时候,一个Windows NT的 lock-unlock所需要耗费20us,即使是在Solaris 下也需要3us,这也就是他为什么要实现自定义latch的目的,在innodb中作者实现了系统latch的封装、自定义mutex和自定义rw_lock。下面我们来一一做分析。
1 系统的mutex和event
typedef pthread_mutex os_fast_mutex_t;而os_event_t相对复杂,它是通过os_fast_mutex_t和一个pthread_cond_t来实现的,定义如下:
typedef struct os_event_struct { os_fast_mutex_t os_mutex; ibool is_set; pthread_cond_t cond_var; }os_event_t;以下是os_event_t的两线程信号控制的例子流程:
2 CPU原子操作
asm volatile("movl $1, %%eax; xchgl (%%ecx), %%eax" : "=eax" (res), "=m" (*lw) : "ecx" (lw));这段代码是什么意思呢?其实就是将lw的值设置成1,并且返回设置lw之前的值(res),这个过程都是CPU需要回写内存的,也就是CPU和内存是完全一致的。除了上面设置1以外,还有一个复位的实现,如下:
asm volatile("movl $0, %%eax; xchgl (%%ecx), %%eax" : "=m" (*lw) : "ecx" (lw) : "eax");这两个函数交叉起来使用,就是gcc-4.1.2以后的__sync_lock_test_and_set的基本实现了。在MySQL-5.6的Innodb引擎当中,将以上汇编代码采用了__sync_lock_test_and_set代替,我们可以采用原子操作实现一个简单的mutex.
#define LOCK() while(__sync_lock_test_and_set(&lock, 1)){} #define UNLOCK() __sync_lock_release(&lock)以上就是一个基本的无锁结构的mutex,在linux下测试确实比pthread_mutex效率要高出不少。当然在innodb之中的mutex实现不会仅仅这么简单,需要考虑的因素还是比较多的,例如:同线程多次lock、lock自旋的周期、死锁检测等。
3 mutex的实现
struct mutex_struct { ulint lock_word; /*mutex原子控制变量*/ os_fast_mutex_t os_fast_mutex; /*在编译器或者系统部支持原子操作的时候采用的系统os_mutex来替代mutex*/ ulint waiters; /*是否有线程在等待锁*/ UT_LIST_NODE_T(mutex_t) list; /*mutex list node*/ os_thread_id_t thread_id; /*获得mutex的线程ID*/ char* file_name; /*mutex lock操作的文件/ ulint line; /*mutex lock操作的文件的行数*/ ulint level; /*锁层ID*/ char* cfile_name; /*mute创建的文件*/ ulint cline; /*mutex创建的文件行数*/ ulint magic_n; /*魔法字*/ };在自定义mute_t的接口方法中,最核心的两个方法是:mutex_enter_func和mutex_exit方法
mutex_enter_func 获得mutex锁,如果mutex被其他线程占用,先会自旋SYNC_SPIN_ROUNDS,然后
mutex_exit 释放mutex锁,并向等待线程发送可以抢占mutex的信号量
3.1 mutex_enter_func流程图:
3.2 mutex_exit流程图
3.4 mutex_t的内存结构关系图
4 rw_lock的实现
1、同一时刻允许多个线程同时读取内存中的变量
2、同一时刻只允许一个线程更改内存中的变量
3、同一时刻当有线程在读取变量时不允许任何线程写存在
4、同一时刻当有线程在更改变量时不允许任何线程读,也不允许出自己以外的线程写(线程内可以递归占有锁)。
5、当有rw_lock处于线程读模式下是有线程写等待,这时候如果再有其他线程读请求锁的时,这个读请求将处于等待前面写完成。
S-latch | X-latch | |
S-latch | 兼容 | 不兼容 |
X-latch | 不兼容 | 不兼容 |
struct rw_lock_struct { ulint reader_count; /*获得S-LATCH的读者个数,一旦不为0,表示是S-LATCH锁*/ ulint writer; /*获得X-LATCH的状态,主要有RW_LOCK_EX、RW_LOCK_WAIT_EX、 RW_LOCK_NOT_LOCKED, 处于RW_LOCK_EX表示是一个x-latch 锁,RW_LOCK_WAIT_EX的状态表示是一个S-LATCH锁*/ os_thread_id_t writer_thread; /*获得X-LATCH的线程ID或者第一个等待成为x-latch的线程ID*/ ulint writer_count; /*同一线程中X-latch lock次数*/ mutex_t mutex; /*保护rw_lock结构中数据的互斥量*/ ulint pass; /*默认为0,如果是非0,表示线程可以将latch控制权转移给其他线程, 在insert buffer有相关的调用*/ ulint waiters; /*有读或者写在等待获得latch*/ ibool writer_is_wait_ex; UT_LIST_NODE_T(rw_lock_t) list; UT_LIST_BASE_NODE_T(rw_lock_debug_t) debug_list; ulint level; /*level标示,用于检测死锁*/ /*用于调试的信息*/ char* cfile_name; /*rw_lock创建时的文件*/ ulint cline; /*rw_lock创建是的文件行位置*/ char* last_s_file_name; /*最后获得S-latch时的文件*/ char* last_x_file_name; /*最后获得X-latch时的文件*/ ulint last_s_line; /*最后获得S-latch时的文件行位置*/ ulint last_x_line; /*最后获得X-latch时的文件行位置*/ ulint magic_n; /*魔法字*/ };
RW_LOCK_NOT_LOCKED 空闲状态
RW_LOCK_SHARED 处于多线程并发都状态
RW_LOCK_WAIT_EX 等待从S-latch成为X-latch状态
RW_LOCK_EX 处于单线程写状态
5 死锁检测与调试
/*sync_thread_t*/ struct sync_thread_struct { os_thread_id_t id; /*占用latch的thread的id*/ sync_level_t* levels; /*latch的信息,sync_level_t结构内容*/ }; /*sync_level_t*/ struct sync_level_struct { void* latch; /*latch句柄,是mute_t或者rw_lock_t的结构指针*/ ulint level; /*latch的level标识ID*/ };
在latch获得的时候,innodb会调用mutex_set_debug_info函数向sync_thread_t中加入一个latch被获得的状态信息,其实就是包括获得latch的线程id、获得latch的文件位置和latch的层标识(具体的细节可以查看mutex_enter_func和mutex_spin_wait)。只有占用了latch才会体现在sync_thread_t中,如果只是在等待获得latch是不会加入到sync_thread_t当中的。innodb可以通过sync_thread_levels_empty_gen函数来输出所有latch等待依赖的cell_t序列,追踪线程等待的位置。
5.1sync_thread_t与sync_level_t的内存结构关系:
levels的长度是SYNC_THREAD_N_LEVELS(10000)。
5.2死锁与死锁检测
线程A 线程B
mutex1 enter mutex2 enter
mutex2 enter mutex1 enter
执行任务 执行任务
mutex2 release mutex1 release
mutex1 release mutex2 release
上面两个线程同时运行的时候,可能产生死锁的情况,就是A线程获得了mutex1正在等待mutex2的锁,同时线程2获得了mutex2正在等待mutex1的锁。在这种情况下,线程1在等线程2,线程2在等线程就造成了死锁。
1、将进入等待的latch对应的cell作为参数传入到sync_array_detect_deadlock当中,其中start的参数和依赖的cell参
2、进入sync_array_detect_deadlock先判断依赖的cell是否正在等待latch,如果没有,表示没有死锁,直接返回.
6.总结
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。