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
? ? 在innodb引擎其中,封裝了操作系統提供的基本mutex(相互排斥量)和event(信號量)。在WINDOWS下的實現臨時不做記錄,主要還是對支持POSIX系統來做介紹。在POSIX系統的實現是os_fast_mutex_t和os_event_t。os_fast_mutex_t相對簡單,事實上就是pthread_mutex。定義例如以下:
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的兩線程信號控制的樣例流程:
對于系統的封裝,最基本的就是os_event_t接口的封裝。而在os_event_t的封裝中,os_event_set、os_event_reset、os_event_wait這三 個方法是最關鍵的。
2 CPU原子操作
在innodb的mutex(相互排斥量)的實現中,除了引用系統的os_mutex_t以外,還使用了原子操作來進行封裝一個高效的mutex實現。在
系統支持原子操作的情況下。會採用自己封裝的mutex來做相互排斥,假設不支持,就使用os_mutex_t。在gcc 4.1.2之前,編譯器是 不提供原子操作的API的,所以在MySQL-.3.23的innodb中自己實現了一個類似__sync_lock_test_and_set的實現,代碼是採用 了匯編實現: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的實現
在innodb中,帶有原子操作的mutex自己定義相互排斥量是基礎的并發和同步的機制,目的是為了降低CPU的上下文切換和提供高效率。一般mutex等待的時間不超過100微秒的條件下,這樣的mutex效率是很高的。假設等待的時間長,建議選擇os_mutex方式。盡管自己定義mutex在自旋時間超過自旋閾值會進入信號等待狀態。可是整個過程相對os_mutex來說。效率太低。這不是自己定義mutex的目的。自己定義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流程圖:
以上流程主要是在mutex_spin_wait這個函數中實現的,從其代碼中能夠看出,這個函數是盡力讓線程在自旋周期內獲得鎖。由于一旦進入cell_wait狀態,至少的耗費1 ~ 2次系統調用。在cell_add的時候有可能觸發os_mutex_t的鎖等待和一定會event_wait等待。這比系統os_mutex效率會低得多。假設在調試狀態下。獲得鎖的同一時候會向thread_levels的加入一條正在使用鎖的信息,以便死鎖檢查和調試。
3.2 mutex_exit流程圖
3.4 mutex_t的內存結構關系圖
3.4mutex獲得鎖和釋放鎖的示意圖
4 rw_lock的實現
innodb為了提高讀的性能,自己定義了read write lock。也就是讀寫鎖。其設計原則是:? ? 1、同一時刻同意多個線程同一時候讀取內存中的變量
? ? 2、同一時刻僅僅同意一個線程更改內存中的變量
? ? 3、同一時刻當有線程在讀取變量時不同意不論什么線程寫存在
? ? 4、同一時刻當有線程在更改變量時不同意不論什么線程讀,也不同意出自己以外的線程寫(線程內能夠遞歸占有鎖)。
? ? 5、當有rw_lock處于線程讀模式下是有線程寫等待,這時候假設再有其它線程讀請求鎖的時。這個讀請求將處于等待前面寫完畢。
| ? | 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四個關鍵函數。?當中rw_lock_s_lock_func和rw_lock_x_lock_func中定義了自旋函數,這兩個自旋函數的流程和mutex_t中的自旋函數實現流程是相似的。其目的是要在自旋期間就完畢鎖的獲得。詳細細節能夠查看sync0rw.c中的rw_lock_s_lock_spin/rw_lock_x_lock_func的代碼實現。從上面結構的定義和函數的實現能夠知道rw_lock有四種狀態:
? RW_LOCK_NOT_LOCKED ? ? ? ? ? ? ? ? ? ?空暇狀態
? RW_LOCK_SHARED ? ? ? ? ? ? ? ? ? ? ? ? ? ? 處于多線程并發都狀態
? RW_LOCK_WAIT_EX ? ? ? ? ? ? ? ? ? ? ? ? ? ?等待從S-latch成為X-latch狀態
? RW_LOCK_EX ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 處于單線程寫狀態
下面是這四種狀態遷移示意圖:
通過上面的遷徙示意圖我們能夠非常清楚的了解rw_lock的運作機理,除了狀態處理以外,rw_lock還為debug提供了接口。我們能夠通過內存關系圖來了解他們的關系:
5 死鎖檢測與調試
innodb除了實現自己定義mutex_t和rw_lock_t以外,還對這兩個類型的latch做了調試性死鎖檢測, 這大大簡化了innodb的latch調試,latch的狀態和信息在能夠實時查看到,但這不過在innodb的調試 版本號中才干看到。與死鎖檢測相關的模塊主要是mutex level、rw_lock level和sync_cell。latch level相關的定義:
/*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的內存結構關系:
sync_thread_level_arrays的長度是OS_THREAD_MAX_N(linux下默認是10000),也就是和最大線程個數是一樣的。
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在等線程就造成了死鎖。
了解了死鎖的概念后,我們就能夠開始分析innodb中關于死鎖檢測的流程細節,innodb的檢車死鎖的實質就是推斷 要進行鎖的latch是否會產生全部線程的閉環,這個是通過sync_array_cell_t的內容來推斷的。在開始等待cell信號的時候。 會推斷將自己的狀態信息放入sync_array_cell_t其中,在進入os event wait之前會調用sync_array_detect_deadlock來判 斷是否死鎖,假設死鎖,會觸發一個異常。死鎖檢測的關鍵在與sync_array_detect_deadlock函數。 下面是檢測死鎖的流程描寫敘述:
? ? 1、將進入等待的latch相應的cell作為參數傳入到sync_array_detect_deadlock其中,其中start的參數和依賴的cell參 數填寫的都是這個cell自己。
? ? 2、進入sync_array_detect_deadlock先推斷依賴的cell是否正在等待latch,假設沒有,表示沒有死鎖。直接返回. 假設有。先推斷等待的鎖被哪個線程占用,并獲得占用線程的id,通過占用線程的id和全局的sync_array_t ?等待cell數組狀 態信息調用sync_array_deadlock_step來推斷等待線程的鎖依賴。 3、進入sync_array_deadlock_step先找到占用線程的相應cell,假設cell和最初的須要event wait的cell是同一 個cell,表示是一個閉環,將產生死鎖。
假設沒有。繼續將查詢到的cell作為參數遞歸調用
sync_array_detect_deadlock運行第2步。這是個兩函數交叉遞歸推斷的過程。
在檢測死鎖過程latch句柄、thread id、cell句柄三者之間環環相扣和遞歸,通過latch的本身的狀態來推斷閉環死鎖。在上面的第2步會依據latch是mutex和rw_lock的差別做區分推斷。這是由于mutex和rw_lock的運作機制不同造成的。由于關系數據庫的latch使用很頻繁和復雜。檢查死鎖對于鎖的調試是很有效的,尤其是配合thread_levels狀態信息輸出來做調試,對死鎖排查是很有意義的。
6.總結
通過上面的分析能夠知道innodb除了實現對操作系統提供的latch結構封裝意外。還提供了原子操作級別的自己定義latch,那么它為什么要實現自己定義latch呢?我個人理解主要是降低操作系統上下文的切換,提高并發的效率。innodb中實現的自己定義latch僅僅適合短時間的鎖等待(最好不超過50us),假設是長時間鎖等待,不妨使用操作系統提供的。盡管自己定義鎖在等待一個自旋周期會進入操作系統的event_wait,但這無疑比系統的mutex lock耗費的資源多。最后我們還是看作者在代碼中的總結:
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.?總結
以上是生活随笔為你收集整理的MySQL系列:innodb源代码分析之线程并发同步机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图片下载器类
- 下一篇: 【ELK】ELK集群搭建(Elastic