前面做了一个开始,沿着路慢慢走下去。
开始之前,这里可以说说这次准备开始研究源代码的一个很大诱因了。前一段时间在生产环境遇到了一个InnoDB报错,这个错误甚至会导致InnoDB Crash:
–Thread 1222654272 has waited at ./include/btr0btr.ic line 53 for 241.00 seconds the semaphore:
S-lock on RW-latch at 0x2aaab510b818 created in file buf/buf0buf.c line 680
沿着这里的线索 buf/buf0buf.c line 680 找到了:
679
680 rw_lock_create(&block->lock, SYNC_LEVEL_VARYING);
681 ut_ad(rw_lock_validate(&(block->lock)));
继续,就开始看rw_lock_create的实现,然后感觉需要看更多基础的一点的内容,这样就有了前面一片文章,继续研究,就有了现在的这篇文章。
前文介绍了,InnoDB中如何使用mutex_struct结构来保护内存(变量)。我们可以看到,如果使用mutex_struct时,其实实现的是一个排他锁。如果每个变量可以被允许并发共享(只读)访问,写的时候才需要“排他”访问的话,再用mutex_struct的话就不合适了。所以,InnoDB在mutex_struct的基础上,又实现了一个读写锁。
本文中”共享锁”即”读锁”,”排他锁”即”写锁”。关于读写锁的基本知识这里就不再介绍了。先说说InnoDB的实现原理,再来看细节。InnoDB使用了一个整数来标记当前锁的状态。
InnoDB用了一个很简单的读写锁实现机制,使用一个整型变量lock_word标志当前锁的状态:
volatile lint lock_word; /*!< Holds the state of the lock. */
初始化时,lock_word的值为1048576(0x00100000),当每一个线程获得一次读锁时,lock_word值减一。每次获取写锁时,lock_word的值则减少1048576。这样,每次根据lock_word当前值,就可以获取当前锁的状态了。下面是关于lock_word规则的详细说明:
根据上面的规律,我们就可以推导出,lock_word取不同区间的值时,当前锁的状态。例如当0<lock_word<X_LOCK_DECR时,当前锁以读的方式被获取,下面是lock_word不同区间值锁的状态表:
这里看到,当线程需要获取该锁(读、写)的时候,根据lock_word值即可以判断当前锁的状态,也就可以知道自己是否可以获得锁。
lock_word基本上包含了锁的全部状态,但是如果细心的话,你会发现有一个例外:当一个线程A需要获得写锁的时候,lock_word的如果是写锁状态时,线程A还必须知道当前持有该锁的线程ID,如果持有锁的线程就是自己,则线程A可以获得写锁。所以,rw_lock_struct结构中还有另一个成员变量:
当锁以排他方式被持有的时候,该变量记录持有者的线程ID。
如果细心,你还可能发现,这里虽然提到了“递归写锁”,却没有提到“递归读锁”。在InnoDB的读写锁实现时,并不考虑读锁的递归,如果锁被排他方式获取,即使是同一个线程,仍然无法获取读锁。
在读写锁的实现中,lock_word的可以“举足轻重”。这就要求每次对lock_word的修改都要做到“一致”。否则,这把锁就坏了。
如果是单线程,获取读锁我们可能写出下面的代码:
local_lock_word = local_lock_word -1;
}
上面的代码,在多线多处理环境是不安全的,会导致lock_word的不一致。线程A需要获取读锁,则CPU在执行上面代码的时候,首先获取local_lock_word的值将其载入寄存器,然后运算,然后返回到内存。如果在“载入寄存器之后运算之前”,lock_word的值发生了改变(例如另一个线程B成功获取了读锁),而这时当前线程A却不知道,线程A仍然把自己的值回写到内存,则会覆盖线程B对lock_word的修改。lock_word的值就不一致了,等锁全部释放后,lock_word的值就可能不是X_LOCK_DECR。
上面这段话可能有点绕,简单的说:多线程并发时,上面简单的赋值操作是不可靠的。回想一下多线程,就不难理解上面这段话了。
介绍完了前面那么多原理,这里总算可以看看源代码的一些东西了。InnoDB封装了函数rw_lock_lock_word_decr来实现对lock_wrod的一致操作。InnoDB会根据不同的编译环境,决定使用不同的策略。简单地,InnoDB使用了前面介绍的mutex_t(排他锁)来实现lock_word的一致操作,即在操作之前获取排他锁;另外,InnoDB根据不同的CPU平台还可以使用Compare-And-Swap来实现,下面来看看源码:
这里是核心代码,详细参考sync0rw.ic文件
ibool rw_lock_lock_word_decr(rw_lock_t* lock, ulint amount) { #ifdef INNODB_RW_LOCKS_USE_ATOMICS while (local_lock_word > 0) { if (os_compare_and_swap_lint(&lock->lock_word,local_lock_word,local_lock_word - amount)) { return(TRUE); } local_lock_word = lock->lock_word; } return(FALSE); #else /* INNODB_RW_LOCKS_USE_ATOMICS */ ibool success = FALSE; mutex_enter(&(lock->mutex)); if (lock->lock_word > 0) { lock->lock_word -= amount; success = TRUE; } mutex_exit(&(lock->mutex)); return(success); #endif /* INNODB_RW_LOCKS_USE_ATOMICS */ }
有了Compare-And-Swap或者排他锁(前文的mutex_t)的保证,InnoDB就可以一致的更改锁的状态(lock_word)了。
这篇文章算是介绍了一下InnoDB读写锁的实现机制,本来想把具体实现也写了,发现作为一篇博客文是有点长了,而且已经非常的晦涩难懂了。所以暂时休息一下,欲知后事如何,且听下回分解。
广告时间:工作机会–MySQL Hacker
Leave a Reply