MySQL系列:innodb源代码分析之线程并发同步机制
innodb是一個(gè)多線程并發(fā)的存儲(chǔ)引擎,內(nèi)部的讀寫(xiě)都是用多線程來(lái)實(shí)現(xiàn)的,所以innodb內(nèi)部實(shí)現(xiàn)了一個(gè)比較高效的并發(fā)同步機(jī)制。
innodb并沒(méi)有直接使用系統(tǒng)提供的鎖(latch)同步結(jié)構(gòu),而是對(duì)其進(jìn)行自己的封裝和實(shí)現(xiàn)優(yōu)化。可是也兼容系統(tǒng)的鎖。我們先看一段innodb內(nèi)部的凝視(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.
大概意思是說(shuō)1995年的時(shí)候。一個(gè)Windows NT的?lock-unlock所須要耗費(fèi)20us,即使是在Solaris 下也須要3us,這也就是他為什么要實(shí)現(xiàn)自己定義latch的目的,在innodb中作者實(shí)現(xiàn)了系統(tǒng)latch的封裝、自己定義mutex和自己定義rw_lock。以下我們來(lái)一一做分析。
1?系統(tǒng)的mutex和event
? ? 在innodb引擎其中,封裝了操作系統(tǒng)提供的基本mutex(相互排斥量)和event(信號(hào)量)。在WINDOWS下的實(shí)現(xiàn)臨時(shí)不做記錄,主要還是對(duì)支持POSIX系統(tǒng)來(lái)做介紹。在POSIX系統(tǒng)的實(shí)現(xiàn)是os_fast_mutex_t和os_event_t。os_fast_mutex_t相對(duì)簡(jiǎn)單,事實(shí)上就是pthread_mutex。定義例如以下:
typedef pthread_mutex os_fast_mutex_t;而os_event_t相對(duì)復(fù)雜,它是通過(guò)os_fast_mutex_t和一個(gè)pthread_cond_t來(lái)實(shí)現(xiàn)的,定義例如以下: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的兩線程信號(hào)控制的樣例流程:
對(duì)于系統(tǒng)的封裝,最基本的就是os_event_t接口的封裝。而在os_event_t的封裝中,os_event_set、os_event_reset、os_event_wait這三 個(gè)方法是最關(guān)鍵的。
2 CPU原子操作
在innodb的mutex(相互排斥量)的實(shí)現(xiàn)中,除了引用系統(tǒng)的os_mutex_t以外,還使用了原子操作來(lái)進(jìn)行封裝一個(gè)高效的mutex實(shí)現(xiàn)。在
系統(tǒng)支持原子操作的情況下。會(huì)採(cǎi)用自己封裝的mutex來(lái)做相互排斥,假設(shè)不支持,就使用os_mutex_t。在gcc 4.1.2之前,編譯器是 不提供原子操作的API的,所以在MySQL-.3.23的innodb中自己實(shí)現(xiàn)了一個(gè)類(lèi)似__sync_lock_test_and_set的實(shí)現(xiàn),代碼是採(cǎi)用 了匯編實(shí)現(xiàn):asm volatile("movl $1, %%eax; xchgl (%%ecx), %%eax" :"=eax" (res), "=m" (*lw) :"ecx" (lw));這段代碼是什么意思呢?
事實(shí)上就是將lw的值設(shè)置成1,而且返回設(shè)置lw之前的值(res),這個(gè)過(guò)程都是CPU須要回寫(xiě)內(nèi)存的,也就是CPU和內(nèi)存是全然一致的。
除了上面設(shè)置1以外。另一個(gè)復(fù)位的實(shí)現(xiàn),例如以下:
asm volatile("movl $0, %%eax; xchgl (%%ecx), %%eax" :"=m" (*lw) : "ecx" (lw) : "eax"); 這兩個(gè)函數(shù)交叉起來(lái)使用,就是gcc-4.1.2以后的__sync_lock_test_and_set的基本實(shí)現(xiàn)了。在MySQL-5.6的Innodb引擎其中,將以上匯編代碼採(cǎi)用了__sync_lock_test_and_set取代。我們能夠採(cǎi)用原子操作實(shí)現(xiàn)一個(gè)簡(jiǎn)單的mutex.#define LOCK() while(__sync_lock_test_and_set(&lock, 1)){} #define UNLOCK() __sync_lock_release(&lock)以上就是一個(gè)主要的無(wú)鎖結(jié)構(gòu)的mutex,在linux下測(cè)試確實(shí)比pthread_mutex效率要高出不少。當(dāng)然在innodb之中的mutex實(shí)現(xiàn)不會(huì)只這么簡(jiǎn)單,須要考慮的因素還是比較多的,比如:同線程多次lock、lock自旋的周期、死鎖檢測(cè)等。
3?mutex的實(shí)現(xiàn)
在innodb中,帶有原子操作的mutex自己定義相互排斥量是基礎(chǔ)的并發(fā)和同步的機(jī)制,目的是為了降低CPU的上下文切換和提供高效率。一般mutex等待的時(shí)間不超過(guò)100微秒的條件下,這樣的mutex效率是很高的。假設(shè)等待的時(shí)間長(zhǎng),建議選擇os_mutex方式。盡管自己定義mutex在自旋時(shí)間超過(guò)自旋閾值會(huì)進(jìn)入信號(hào)等待狀態(tài)。可是整個(gè)過(guò)程相對(duì)os_mutex來(lái)說(shuō)。效率太低。這不是自己定義mutex的目的。自己定義mutex的定義例如以下:struct mutex_struct {ulint lock_word; /*mutex原子控制變量*/os_fast_mutex_t os_fast_mutex; /*在編譯器或者系統(tǒng)部支持原子操作的時(shí)候採(cǎi)用的系統(tǒng)os_mutex來(lái)替代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操作的文件的行數(shù)*/ulint level; /*鎖層ID*/char* cfile_name; /*mute創(chuàng)建的文件*/ulint cline; /*mutex創(chuàng)建的文件行數(shù)*/ulint magic_n; /*魔法字*/ };在自己定義mute_t的接口方法中,最核心的兩個(gè)方法是:mutex_enter_func和mutex_exit方法
? ? mutex_enter_func ? ? ? ? ? ? ? ? ? ?獲得mutex鎖,假設(shè)mutex被其它線程占用。先會(huì)自旋SYNC_SPIN_ROUNDS,然后 再等待占用鎖的線程的信號(hào)
? ? mutex_exit ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 釋放mutex鎖。并向等待線程發(fā)送能夠搶占mutex的信號(hào)量
3.1 mutex_enter_func流程圖:
以上流程主要是在mutex_spin_wait這個(gè)函數(shù)中實(shí)現(xiàn)的,從其代碼中能夠看出,這個(gè)函數(shù)是盡力讓線程在自旋周期內(nèi)獲得鎖。由于一旦進(jìn)入cell_wait狀態(tài),至少的耗費(fèi)1 ~ 2次系統(tǒng)調(diào)用。在cell_add的時(shí)候有可能觸發(fā)os_mutex_t的鎖等待和一定會(huì)event_wait等待。這比系統(tǒng)os_mutex效率會(huì)低得多。假設(shè)在調(diào)試狀態(tài)下。獲得鎖的同一時(shí)候會(huì)向thread_levels的加入一條正在使用鎖的信息,以便死鎖檢查和調(diào)試。
3.2 mutex_exit流程圖
3.4 mutex_t的內(nèi)存結(jié)構(gòu)關(guān)系圖
3.4mutex獲得鎖和釋放鎖的示意圖
4 rw_lock的實(shí)現(xiàn)
innodb為了提高讀的性能,自己定義了read write lock。也就是讀寫(xiě)鎖。其設(shè)計(jì)原則是:? ? 1、同一時(shí)刻同意多個(gè)線程同一時(shí)候讀取內(nèi)存中的變量
? ? 2、同一時(shí)刻僅僅同意一個(gè)線程更改內(nèi)存中的變量
? ? 3、同一時(shí)刻當(dāng)有線程在讀取變量時(shí)不同意不論什么線程寫(xiě)存在
? ? 4、同一時(shí)刻當(dāng)有線程在更改變量時(shí)不同意不論什么線程讀,也不同意出自己以外的線程寫(xiě)(線程內(nèi)能夠遞歸占有鎖)。
? ? 5、當(dāng)有rw_lock處于線程讀模式下是有線程寫(xiě)等待,這時(shí)候假設(shè)再有其它線程讀請(qǐng)求鎖的時(shí)。這個(gè)讀請(qǐng)求將處于等待前面寫(xiě)完畢。
| ? | S-latch | X-latch |
| S-latch | 兼容 | 不兼容 |
| X-latch | 不兼容 | 不兼容 |
?在rw_lock_t獲得鎖和釋放鎖的主要接口是:rw_lock_s_lock_func、rw_lock_x_lock_func、rw_lock_s_unlock_func、rw_lock_x_unlock_func四個(gè)關(guān)鍵函數(shù)。?當(dāng)中rw_lock_s_lock_func和rw_lock_x_lock_func中定義了自旋函數(shù),這兩個(gè)自旋函數(shù)的流程和mutex_t中的自旋函數(shù)實(shí)現(xiàn)流程是相似的。其目的是要在自旋期間就完畢鎖的獲得。詳細(xì)細(xì)節(jié)能夠查看sync0rw.c中的rw_lock_s_lock_spin/rw_lock_x_lock_func的代碼實(shí)現(xiàn)。從上面結(jié)構(gòu)的定義和函數(shù)的實(shí)現(xiàn)能夠知道rw_lock有四種狀態(tài):
? RW_LOCK_NOT_LOCKED ? ? ? ? ? ? ? ? ? ?空暇狀態(tài)
? RW_LOCK_SHARED ? ? ? ? ? ? ? ? ? ? ? ? ? ? 處于多線程并發(fā)都狀態(tài)
? RW_LOCK_WAIT_EX ? ? ? ? ? ? ? ? ? ? ? ? ? ?等待從S-latch成為X-latch狀態(tài)
? RW_LOCK_EX ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 處于單線程寫(xiě)狀態(tài)
下面是這四種狀態(tài)遷移示意圖:
通過(guò)上面的遷徙示意圖我們能夠非常清楚的了解rw_lock的運(yùn)作機(jī)理,除了狀態(tài)處理以外,rw_lock還為debug提供了接口。我們能夠通過(guò)內(nèi)存關(guān)系圖來(lái)了解他們的關(guān)系:
5 死鎖檢測(cè)與調(diào)試
innodb除了實(shí)現(xiàn)自己定義mutex_t和rw_lock_t以外,還對(duì)這兩個(gè)類(lèi)型的latch做了調(diào)試性死鎖檢測(cè), 這大大簡(jiǎn)化了innodb的latch調(diào)試,latch的狀態(tài)和信息在能夠?qū)崟r(shí)查看到,但這不過(guò)在innodb的調(diào)試 版本號(hào)中才干看到。與死鎖檢測(cè)相關(guān)的模塊主要是mutex level、rw_lock level和sync_cell。latch level相關(guān)的定義:
/*sync_thread_t*/struct sync_thread_struct{os_thread_id_t id; /*占用latch的thread的id*/sync_level_t* levels; /*latch的信息,sync_level_t結(jié)構(gòu)內(nèi)容*/};/*sync_level_t*/struct sync_level_struct{void* latch; /*latch句柄,是mute_t或者rw_lock_t的結(jié)構(gòu)指針*/ulint level; /*latch的level標(biāo)識(shí)ID*/};在latch獲得的時(shí)候,innodb會(huì)調(diào)用mutex_set_debug_info函數(shù)向sync_thread_t中增加一個(gè)latch被獲得的狀態(tài)信息。事實(shí)上就是包含獲得latch的線程id、獲得latch的文件位置和latch的層標(biāo)識(shí)(詳細(xì)的細(xì)節(jié)能夠查看mutex_enter_func和mutex_spin_wait)。僅僅有占用了latch才會(huì)體如今sync_thread_t中,假設(shè)僅僅是在等待獲得latch是不會(huì)增加到sync_thread_t其中的。innodb能夠通過(guò)sync_thread_levels_empty_gen函數(shù)來(lái)輸出全部latch等待依賴(lài)的cell_t序列。追蹤線程等待的位置。
5.1sync_thread_t與sync_level_t的內(nèi)存結(jié)構(gòu)關(guān)系:
sync_thread_level_arrays的長(zhǎng)度是OS_THREAD_MAX_N(linux下默認(rèn)是10000),也就是和最大線程個(gè)數(shù)是一樣的。
levels的長(zhǎng)度是SYNC_THREAD_N_LEVELS(10000)。
5.2死鎖與死鎖檢測(cè)
什么是死鎖,通過(guò)下面的樣例我們能夠做個(gè)簡(jiǎn)單的描寫(xiě)敘述:? ? 線程A ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?線程B
? ? mutex1 ? ?enter ? ? ? ? ? ? ? ?mutex2 ? ? ? ?enter
? ? mutex2 ? ?enter ? ? ? ? ? ? ? ?mutex1 ? ? ? ?enter
? ? 運(yùn)行任務(wù) ? ? ? ? ? ? ? ? ? ? ? ? ? 運(yùn)行任務(wù)
? ? mutex2 ? ?release ? ? ? ? ? ? mutex1 ? ? ? ? ?release
? ? mutex1 ? ?release ? ? ? ? ? ?mutex2 ? ? ? ? ? release
? ?上面兩個(gè)線程同一時(shí)候執(zhí)行的時(shí)候。可能產(chǎn)生死鎖的情況。就是A線程獲得了mutex1正在等待mutex2的鎖。同一時(shí)候線程2獲得了mutex2正在等待mutex1的鎖。在這樣的情況下,線程1在等線程2,線程2在等線程就造成了死鎖。
了解了死鎖的概念后,我們就能夠開(kāi)始分析innodb中關(guān)于死鎖檢測(cè)的流程細(xì)節(jié),innodb的檢車(chē)死鎖的實(shí)質(zhì)就是推斷 要進(jìn)行鎖的latch是否會(huì)產(chǎn)生全部線程的閉環(huán),這個(gè)是通過(guò)sync_array_cell_t的內(nèi)容來(lái)推斷的。在開(kāi)始等待cell信號(hào)的時(shí)候。 會(huì)推斷將自己的狀態(tài)信息放入sync_array_cell_t其中,在進(jìn)入os event wait之前會(huì)調(diào)用sync_array_detect_deadlock來(lái)判 斷是否死鎖,假設(shè)死鎖,會(huì)觸發(fā)一個(gè)異常。死鎖檢測(cè)的關(guān)鍵在與sync_array_detect_deadlock函數(shù)。 下面是檢測(cè)死鎖的流程描寫(xiě)敘述:
? ? 1、將進(jìn)入等待的latch相應(yīng)的cell作為參數(shù)傳入到sync_array_detect_deadlock其中,其中start的參數(shù)和依賴(lài)的cell參 數(shù)填寫(xiě)的都是這個(gè)cell自己。
? ? 2、進(jìn)入sync_array_detect_deadlock先推斷依賴(lài)的cell是否正在等待latch,假設(shè)沒(méi)有,表示沒(méi)有死鎖。直接返回. 假設(shè)有。先推斷等待的鎖被哪個(gè)線程占用,并獲得占用線程的id,通過(guò)占用線程的id和全局的sync_array_t ?等待cell數(shù)組狀 態(tài)信息調(diào)用sync_array_deadlock_step來(lái)推斷等待線程的鎖依賴(lài)。 3、進(jìn)入sync_array_deadlock_step先找到占用線程的相應(yīng)cell,假設(shè)cell和最初的須要event wait的cell是同一 個(gè)cell,表示是一個(gè)閉環(huán),將產(chǎn)生死鎖。
假設(shè)沒(méi)有。繼續(xù)將查詢(xún)到的cell作為參數(shù)遞歸調(diào)用
sync_array_detect_deadlock運(yùn)行第2步。這是個(gè)兩函數(shù)交叉遞歸推斷的過(guò)程。
在檢測(cè)死鎖過(guò)程latch句柄、thread id、cell句柄三者之間環(huán)環(huán)相扣和遞歸,通過(guò)latch的本身的狀態(tài)來(lái)推斷閉環(huán)死鎖。在上面的第2步會(huì)依據(jù)latch是mutex和rw_lock的差別做區(qū)分推斷。這是由于mutex和rw_lock的運(yùn)作機(jī)制不同造成的。由于關(guān)系數(shù)據(jù)庫(kù)的latch使用很頻繁和復(fù)雜。檢查死鎖對(duì)于鎖的調(diào)試是很有效的,尤其是配合thread_levels狀態(tài)信息輸出來(lái)做調(diào)試,對(duì)死鎖排查是很有意義的。
6.總結(jié)
通過(guò)上面的分析能夠知道innodb除了實(shí)現(xiàn)對(duì)操作系統(tǒng)提供的latch結(jié)構(gòu)封裝意外。還提供了原子操作級(jí)別的自己定義latch,那么它為什么要實(shí)現(xiàn)自己定義latch呢?我個(gè)人理解主要是降低操作系統(tǒng)上下文的切換,提高并發(fā)的效率。innodb中實(shí)現(xiàn)的自己定義latch僅僅適合短時(shí)間的鎖等待(最好不超過(guò)50us),假設(shè)是長(zhǎng)時(shí)間鎖等待,不妨使用操作系統(tǒng)提供的。盡管自己定義鎖在等待一個(gè)自旋周期會(huì)進(jìn)入操作系統(tǒng)的event_wait,但這無(wú)疑比系統(tǒng)的mutex lock耗費(fèi)的資源多。最后我們還是看作者在代碼中的總結(jié):
We conclude that the best choice is to set the spin time at 20 us. Then the?system should work well on a multiprocessor. On a uniprocessor we have to?make sure that thread swithches due to mutex collisions are not frequent,?i.e., they do not happen every 100 us or so, because that wastes too much?resources. If the thread switches are not frequent, the 20 us wasted in spin?loop is not too much.?總結(jié)
以上是生活随笔為你收集整理的MySQL系列:innodb源代码分析之线程并发同步机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 图片下载器类
- 下一篇: linux cmake编译源码,linu