程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> MYSQL數據庫 >> MySQL綜合教程 >> MySQL系列:innodb引擎分析之線程並發同步機制

MySQL系列:innodb引擎分析之線程並發同步機制

編輯:MySQL綜合教程

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處於線程讀模式下是有線程寫等待,這時候如果再有其他線程讀請求鎖的時,這個讀請求將處於等待前面寫完成。
從上面5點我們可以看出,rw_lock在被占用是會處於讀狀態和寫狀態,我們稱之為S-latch(讀共享)和X-latch(寫獨占),《MySQL技術內幕:innodb引擎》對S-latch和X_latch的描述如下:
  S-latch X-latch S-latch 兼容 不兼容 X-latch 不兼容 不兼容 innodb中的rw_lock是在建立在自定義mutex_t之上的,所有的控制是基於mutex和thread_cell的。 以下是rw_lock_t的結構定義:
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_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.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved