MYSQL INNODB 中通用雙向連結串列的實現

gaopengtttt發表於2017-07-03
原創:如果有誤請支援
原始碼在Ut0lst.h中
注意:這裡我將連結串列中的實際的串聯的資料叫做資料類比如:lock_t、mem_block_t

連結串列作為一種的非常重要的資料結構,在任何地方都會用到,這裡簡單解釋一下innodb雙向
連結串列的實現,讓我們來看看innodb連結串列設計的魅力:
經常在某些結構體中看到
UT_LIST_BASE_NODE_T(mem_block_t) base;
UT_LIST_NODE_T(mem_block_t) list; 

作為最為基本的一種的資料結構innodb中實現得比較精妙涉及到重要的4個C++知識點:
1、仿函式
2、類成員指標
3、類别範本
4、函式過載

簡單的說仿函式是呼叫的時候類似函式呼叫的形式的類,類成員指標並非一個真正意義上的
指標,而是指向特定類物件相對位置的一個偏移量。
比如如下就是一個仿函式類:

點選(此處)摺疊或開啟

  1. template <typename T>
  2. class ShowElemt
  3. {
  4. public:
  5.     ShowElemt()
  6.     {
  7.         n = 0;
  8.     }
  9.     void operator()(T &t)
  10.     {
  11.         n++;
  12.         cout << t << " ";
  13.     }
  14.     void printCount()
  15.     {
  16.         cout << n << endl;
  17.     }
  18. public:
  19.     int n;
  20. };
下面是一個簡單的類成員指標使用,他初始化分為2步在最後給出.:


點選(此處)摺疊或開啟

  1. #include<iostream>
  2. using namespace std;

  3. class T
  4. {
  5.   public:
  6.   typedef int uint;
  7.   public:
  8.           int a;
  9.           int b;
  10. };

  11. int main21(void)
  12. {
  13.         T t;
  14.         int T::* t1 = &T::b;//1、成員函式指標 初始化為指向類T成員b的一個指標(成員函式指標指向的是偏移量)
  15.         T* t2 = &t;//t2一個指向類變數t的指標
  16.         t.*t1 = 10;//2、初始化t的t1類成員指標指向的記憶體空間值為10,實際上就是t.b=10
  17.         cout<<t.*t1<<" "<<t2->*t1<<endl;//相同輸出一個採用物件一個採用物件指標
  18.         {
  19.                 T t3;
  20.                 t3.a=300;
  21.                 t.*t1 = t3.a; //他就是擁有實際記憶體空間的變數了
  22.         }
  23. }
模板和函式過載就沒有什麼好說的了。

接下來我們看看UT_LIST_BASE_NODE_T、UT_LIST_NODE_T分別代表了什麼
實際上他們都是宏定義:
#define UT_LIST_BASE_NODE_T(t) ut_list_base<t, ut_list_node t::*>
#define UT_LIST_NODE_T(t) ut_list_node
那麼他們的型別出來了實際上就是:
ut_list_base和ut_list_node,我們知道在設計連結串列的時候,通常有一個連結串列頭資料結構,用來儲存
比如連結串列中總共多少元素,連結串列頭,連結串列尾等等特徵,但是之前還是想來看看ut_list_node連結串列結構體:

點選(此處)摺疊或開啟

  1. template <typename Type>
  2. struct ut_list_node {
  3.     Type*        prev;            /*!< pointer to the previous
  4.                         node, NULL if start of list */
  5.     Type*        next;            /*!< pointer to next node,
  6.                         NULL if end of list */

  7.     void reverse()
  8.     {
  9.         Type*    tmp = prev;
  10.         prev = next;
  11.         next = tmp;
  12.     }
  13. };
非常簡單沒有包含任何固定的資料資訊,只是包含了連結串列的前後指標,同時包含了一個成員函式reverse,作為
實現連結串列反轉的基礎,這裡也注意到由於沒有包含任何資料資訊成員,做到了連結串列和具體資料類之間的剝離。
在連結串列設計的時候通常有2種方式:
1、連結串列結構包含資料類
2、資料類包含連結串列結構
這裡INNODB使用了後者,讓連結串列的通用更加方便

再來看看ut_list_base 連結串列頭結構體:

點選(此處)摺疊或開啟

  1. template <typename Type, typename NodePtr>
  2. struct ut_list_base {
  3.     typedef Type elem_type;
  4.     typedef NodePtr node_ptr;
  5.     typedef ut_list_node<Type> node_type;

  6.     ulint        count;            /*!< count of nodes in list */
  7.     elem_type*    start;            /*!< pointer to list start,
  8.                         NULL if empty */
  9.     elem_type*    end;            /*!< pointer to list end,
  10.                         NULL if empty */
  11.     node_ptr    node;            /*!< Pointer to member field
  12.                         that is used as a link node */
  13. #ifdef UNIV_DEBUG
  14.     ulint        init;            /*!< UT_LIST_INITIALISED if
  15.                         the list was initialised with
  16.                         UT_LIST_INIT() */
  17. #endif /* UNIV_DEBUG */

  18.     void reverse()
  19.     {
  20.         Type*    tmp = start;
  21.         start = end;
  22.         end = tmp;
  23.     }
  24. };
這裡再來解釋一下:
在類的內部進行了3種型別的typedef,分別是:
typedef Type elem_type;:具體的型別比如lock_t、mem_block_t。
typedef NodePtr node_ptr; :和模板宣告中的ut_list_node t::* 聯合起來看,實際上他是一個
類成員指標,他指向的會是特定資料類比如lock_t、mem_block_t中特定的一個成員,這個成員一定是
ut_list_node型別的也就是UT_LIST_NODE_T(t)型別的,其中t當然也就是資料類本身,這也是整個
設計中的精髓。
typedef ut_list_node node_type; :和前面的ut_list_node聯合起來看,就能知道
它是一個特定型別的節點型別比如lock_t、mem_block_t。
剩下的就是類成員了:
ulint count;:連結串列中的有多少元素
elem_type* start;:具體資料類元素的起始位置指標
elem_type* end;:具體資料類元素的最後位置指標
node_ptr node;:這裡使用了剛才說的typedef NodePtr node_ptr來定義成員node,那麼我們可以猜測他是指向
特定資料類物件中連結串列結構元素的成員指標,其實的確如此:
如lock_t

點選(此處)摺疊或開啟

  1. /** Lock struct; protected by lock_sys->mutex */
  2. struct lock_t {
  3.     trx_t*        trx;        /*!< transaction owning the
  4.                     lock */
  5.     UT_LIST_NODE_T(lock_t)
  6.             trx_locks;    /*!< list of the locks of the
  7.                     transaction */
  8. ..........

剩下還有一個關鍵的仿函式:

點選(此處)摺疊或開啟

  1. template <typename Type> //一元謂詞仿函式
  2. struct GenericGetNode {
  3.     typedef ut_list_node<Type> node_type;
  4.     GenericGetNode(node_type Type::* node) : m_node(node) {}
  5.     node_type& operator() (Type& elem)
  6.     {
  7.         return(elem.*m_node);
  8.     }
  9.     node_type    Type::*m_node;
  10. };
這裡解釋一下這個仿函式類:
typedef ut_list_node node_type;和前面的ut_list_node聯合起來看,就能知道
它是一個特定型別的節點型別比如lock_t、mem_block_t。
GenericGetNode(node_type Type::* node) : m_node(node) {} :有參建構函式,透過輸入一個指向特定資料節點的
ut_list_node(UT_LIST_NODE_T(t))成員的值node進行初始化元素m_node。但是這裡只是指向了類成員有了偏移量,但是並沒有初始化記憶體空間,具體的初始化
記憶體空間在實現函式中。
node_type& operator() (Type& elem) :這裡就是仿函式,過載了()運算子,接受一個特定節點型別比如lock_t、mem_block_t
的一個引用輸入,然後返回一個類成員指標的引用,如果是lock_t那麼返回的將是trx_locks,那麼我們就能夠使用它
trx_locks.prev
在連結串列實現中中包含很多方法大概如下:
UT_LIST_INIT:初始化一個連結串列、是一個宏定義
ut_list_prepend:頭插法插入連結串列
ut_list_append:尾插法插入連結串列
ut_list_insert:將某個元素插入到某個元素之後
ut_list_remove:刪除某個節點
ut_list_reverse:連結串列反向
ut_list_move_to_front:將指定的元素放到頭部
好了到這裡我們解釋了關鍵連結串列資料結構下面我們透過一段innodb程式碼來分析一下,這裡我們
我們只是關注連結串列操作所以如下,這裡涉及到初始化和尾插法加入連結串列
UT_LIST_BASE_NODE_T(lock_t) old_locks;
UT_LIST_INIT(old_locks, &lock_t::trx_locks);
lock_t* old_lock = lock_rec_copy(lock, heap);
UT_LIST_ADD_LAST(old_locks, old_lock);

我們來具體解釋一下步驟:
1、UT_LIST_BASE_NODE_T(lock_t) old_locks;定義old_locks為一個連結串列頭物件。
2、UT_LIST_INIT(old_locks, &lock_t::trx_locks);進行初始化,這裡UT_LIST_INIT是一個宏

點選(此處)摺疊或開啟

  1. #define UT_LIST_INIT(b, pmf)    \
  2. {    \
  3. (b).count = 0;    \
  4. (b).start = 0;    \
  5. (b).end = 0;    \
  6. (b).node = pmf;    \
  7. UT_LIST_INITIALISE(b);    \
  8. }


非常簡單設定全部指標都是NULL,並且初始化node類成員指標指向&lock_t::trx_locks。
3、lock_t* old_lock = lock_rec_copy(lock, heap);我們先不深究這裡面,但是他肯定是一種複製,完成後他返回一個lock_t*的指標
old_lock
4、接下來就是加入節點,這是一個重頭戲,會應用到前面全部的知識。
UT_LIST_ADD_LAST(old_locks, old_lock);
實際上他是一共宏定義
#define UT_LIST_ADD_LAST(LIST, ELEM) ut_list_append(LIST, ELEM)
在經過函式過載呼叫後實際上他會呼叫

點選(此處)摺疊或開啟

  1. template <typename List>
  2. void
  3. ut_list_append(
  4. List&    list,
  5. typename List::elem_type*    elem)
  6. {
  7. ut_list_append(
  8. list, elem,
  9. GenericGetNode<typename List::elem_type>(list.node));
  10. }


然後呼叫:

點選(此處)摺疊或開啟

  1. template <typename List, typename Functor>
  2. void
  3. ut_list_append(
  4. List&    list,
  5. typename List::elem_type*    elem,
  6. Functor    get_node)
  7. {
  8. typename List::node_type&    node = get_node(*elem);


  9. UT_LIST_IS_INITIALISED(list);


  10. node.next = 0;
  11. node.prev = list.end;


  12. if (list.end != 0) {
  13. typename List::node_type&    base_node = get_node(*list.end);


  14. ut_ad(list.end != elem);


  15. base_node.next = elem;
  16. }


  17. list.end = elem;


  18. if (list.start == 0) {
  19. list.start = elem;
  20. }
  21. ++list.count;
  22. }
詳細描述一下:
首先看一下:
template
void
ut_list_append(
List& list,
typename List::elem_type* elem)
{
ut_list_append(
list, elem,
GenericGetNode(list.node));
}
這裡list就是我們初始化的old_locks型別為UT_LIST_BASE_NODE_T(lock_t),elem就是我們copy出來的一個
指向lock_t*的指標old_lock其型別當然也就是UT_LIST_BASE_NODE_T(lock_t)::elem_type*型別實際上就是
lock_t*型別繞了一大圈。
而GenericGetNode(list.node)雖然很長但是我們可以清楚的明白他是
呼叫的建構函式,生成一個匿名物件,typename List::elem_type是用到了ut_list_base定義的型別
elem_type就是一個UT_LIST_BASE_NODE_T(lock_t)::elem_type型別其實就是lock_t型別,而list.node
實際上就是node_ptr型別,初始化已經被定義為&lock_t::trx_locks

接下來由於函式過載的函式呼叫了
ut_list_append(
List& list,
typename List::elem_type* elem,
Functor get_node)
我們來看看。
typename List::node_type& node = get_node(*elem);
將List(old_locks)中的node成員函式指標進行初始化他指向了old_lock這是結構實際連結串列結構的位置。
接下來node.next nodex.prev將是可用的了
node.next = 0;
node.prev = list.end;
將節點的後指標設定為NULL,前指標當然設定為list.end的位置
這裡也看到他確實放到末尾
if (list.end != 0) {
typename List::node_type& base_node = get_node(*list.end);
ut_ad(list.end != elem);
base_node.next = elem;
}
如果連結串列不為空,這裡再次獲得end節點的位置存放到base_node中,
當然也就要將base_node.next設定為我們新加入的節點的指標。
list.end = elem;
將連結串列頭結構的end指標放到我們新插入的elem中。
if (list.start == 0) {
list.start = elem;
}
如果list的start指標為空代表連結串列為空,那麼還需要將start指標指向elem
最後
++list.count;
不解釋了。

從整個連結串列的實現來看仿函式是其中的一個重點,他是一個橋樑其主要分為兩步:
1、初始化指向一個類的成員函式,這是指定他的型別,獲得他的偏移量
2、初始化指向某一個元素,這是獲得他的記憶體空間地址基地址
有了基地址+偏移量就能夠找到實際的元素了。

作者微信:

MYSQL INNODB 中通用雙向連結串列的實現

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/7728585/viewspace-2141547/,如需轉載,請註明出處,否則將追究法律責任。

相關文章