__gnu_cxx::lock
OK,現在是時候研究lock了。它的定義在GCC源碼的“libstdc++-v3/include/bits/concurrence.h”文件裏,以下簡稱concurrence.h。
<concurrence.h>
79 /// @brief Scoped lock idiom.
80 // Acquire the mutex here with a constructor call, then release with
81 // the destructor call in accordance with RAII style.
82 class lock
使用構造函數加鎖,析構函數解鎖的輔助類,相信讀者應該不會陌生。
83 {
84 // Externally defined and initialized.
85 mutex_type& device;
mutex_type是一個平臺相關的類型,在我的環境裏,它是pthread_mutex_t。
87 public:
88 explicit lock(mutex_type& name) : device(name)
89 { __glibcxx_mutex_lock(device); }
在構造函數里加鎖。__glibcxx_mutex_lock是平臺相關的宏定義,在我的環境裏有:
<concurrence.h>
48 # define __glibcxx_mutex_lock(NAME) /
49 __gthread_mutex_lock(&NAME)
符號__gthread_mutex_lock是函數pthread_mutex_lock的弱引用。
<concurrence.h>
91 ~lock() throw()
92 { __glibcxx_mutex_unlock(device); }
在析構函數裏解鎖。宏__glibcxx_mutex_unlock在我的環境裏定義爲:
<concurrence.h>
64 # define __glibcxx_mutex_unlock(NAME) __gthread_mutex_unlock(&NAME)
符號__gthread_mutex_unlock是函數pthread_mutex_unlock的弱引用。
94 private:
95 lock(const lock&);
96 lock& operator=(const lock&);
禁止對lock對象進行復制。
97 };
總結一下,__gnu_cxx::lock對象在構造的時候,把一個pthread_mutex_t對象加鎖,直到析構的時候再把這個pthread_mutex_t對象解鎖。於是可以利用__gnu_cxx::lock對象的生命週期,對一個代碼範圍進行自動的鎖定,而這正是前面函數_M_destroy_thread_key的代碼和下面的代碼使用__gnu_cxx::lock的原因。
回到__pool<true>::_M_initialize
好了,我們介紹完了lock,freelist_mutex和freelist,現在回頭再到當初中斷的地方。
<mt_allocator.cc>
466 if (__gthread_active_p())
467 {
468 {
這個“{”括號的作用是限定下面的__gnu_cxx::lock的加鎖區域。
469 __gnu_cxx::lock sentry(__gnu_internal::freelist_mutex);
對線程id鏈表freelist進行加鎖。
471 if (!__gnu_internal::freelist._M_thread_freelist_array
472 || __gnu_internal::freelist._M_max_threads
473 < _M_options._M_max_threads)
仔細研究這2個判斷條件,你會發現很多東西。
第一個條件是_M_thread_freelist_array是否爲0,這說明了freelist是否被初始化過。這個條件實際上隱含了:函數__pool<true>::_M_initialize會被運行多次?是的!不過先不要迷惑,且看後面的代碼再說。
第二個條件是:freelist鏈表當前的長度是否小於內存池需要的線程id個數。既然_M_initialize會被多次運行,那麼使用一個新的_M_max_threads 值來初始化一個已經在使用的freelist鏈表也就成爲可能了。所以,如果新的_M_max_threads值比freelist鏈表長度小,自然什麼都不許要改變;如果新的_M_max_threads值更大,那我們就需要重新初始化freelist,使它擁有更多的線程id節點數,以滿足我們的最大需求。
究竟什麼情況下會多次運行_M_initialize呢?答案是多線程下使用__per_type_pool_policy的時候。也許讀者應該回頭翻翻多線程下__per_type_pool_policy的代碼(mt_allocator.h第546行),和_S_get_pool的實現(mt_allocator.h第470行)。每一個_Tp類型模板參數,都會實例化出一個新的__per_type_pool_policy類型,從而使得從_S_get_pool裏得到的內存池對象(類型爲__pool<true>)各不相同。但是,這些__pool<true>對象都會運行自己的_M_initialize函數,使用的還是同一個全局線程id鏈表freelist,於是問題來了:怎麼樣合理的初始化freelist?
請讀者先想想自己會怎麼做:
1)如果freelist還沒有被初始化過(第一個條件),那麼自然就用當前__pool<true>對象的參數初始化它;
2)如果freelist已經初始化了,那麼我們應該判斷線程id個數是否足夠,如果不足(第二個條件),那麼需要重新初始化它,並使用當前__pool<true>對象的參數。
這2種情況都導致同樣的“下一步”:對freelist進行初始化。但情況2)比情況1)會有更多的“後續工作”需要完成,這在後面的代碼裏我們會看到。
<mt_allocator.cc>
474 {
475 const size_t __k = sizeof(_Thread_record)
476 * _M_options._M_max_threads;
477 __v = ::operator new(__k);
478 _Thread_record* _M_thread_freelist
479 = static_cast<_Thread_record*>(__v);
計算線程id節點數組的總字節大小__k,然後向OS申請內存,並最後存儲在局部變量_M_thread_freelist裏。這裏我不得不抱怨一下,_M_xxxx的命名方式向來是類的成員變量和函數專用的,爲什麼要用在一個局部變量上?更糟糕的是,_M_thread_freelist這個名字與成員變量__pool<true>::_M_thread_freelist竟然重名!這在開始的時候給我造成了不少的麻煩!
實際上,__pool<true>::_M_thread_freelist根本就沒有被用到過,下面代碼中出現的所有_M_thread_freelist都是指局部變量_M_thread_freelist,希望讀者不要被代碼作者“迷惑”。
481 // NOTE! The first assignable thread id is 1 since the
482 // global pool uses id 0
483 size_t __i;
484 for (__i = 1; __i < _M_options._M_max_threads; ++__i)
485 {
486 _Thread_record& __tr = _M_thread_freelist[__i - 1];
487 __tr._M_next = &_M_thread_freelist[__i];
488 __tr._M_id = __i;
489 }
把新的線程id節點數組“串聯”成鏈表的形式,_M_id值被設置成下標值加1。
491 // Set last record.
492 _M_thread_freelist[__i - 1]._M_next = NULL;
493 _M_thread_freelist[__i - 1]._M_id = __i;
設置最後一個節點。到這裏爲止,情況1)和2)的“共同工作”做完了。
495 if (!__gnu_internal::freelist._M_thread_freelist_array)
區分情況1)和情況2),即“freelist還沒有被初始化過”還是“線程id個數是否足夠”。
496 {
下面是情況1)的“後續工作”。
497 // Initialize per thread key to hold pointer to
498 // _M_thread_freelist.
499 __gthread_key_create(&__gnu_internal::freelist._M_key,
500 __gnu_internal::_M_destroy_thread_key);
創建線程私有數據空間,並把前面介紹的_M_destroy_thread_key函數作爲線程退出時的“清理函數”。符號__gthread_key_create是函數pthread_key_create的弱引用。讀者可以證明一下,這個部分的代碼在整個程序裏只會被運行一次。
501 __gnu_internal::freelist._M_thread_freelist
502 = _M_thread_freelist;
把新的線程id鏈表交給freelist管理。
503 }
504 else
505 {
下面是情況2)的“後續工作”。
也許我需要先解釋一下這些“後續工作”是什麼,以便讀者更容易看懂下面的代碼。前面已經介紹過,不同的_Tp類型模板參數,會實例化出不同的__per_type_pool_policy類型,從而使得從_S_get_pool裏得到的__pool<true>對象各不相同。那麼這些不同的__pool<true>對象會在什麼時候調用_M_initialize初始化呢?答案是任何時候。比如,針對int的__pool<true>對象可能在某個使用std::vector<int>的代碼點進行了初始化,但是你仍不知道針對double的__pool<true>對象會在什麼時候進行初始化,因爲你不知道程序會在何時第一次調用函數__mt_alloc<double>:: allocate。
所以,當程序最後運行到這裏來的時候,freelist裏面可能已經“面目全非”了:有些線程id被分配出去了,有些又被歸還回來了。新的線程id鏈表必須完整的記錄下這些信息,才能保證整個程序的正確運行。下面的代碼就是這個信息的複製過程。
506 _Thread_record* _M_old_freelist
507 = __gnu_internal::freelist._M_thread_freelist;
508 _Thread_record* _M_old_array
509 = __gnu_internal::freelist._M_thread_freelist_array;
先把舊鏈表保存起來。_M_thread_freelist_array與_M_thread_freelist的關係在前面介紹過了。其實這裏還可以想一下,爲什麼不能在已有的線程id基礎上,加上不足的那部分線程id?讀者一定馬上想到,所有的線程id節點都在一個數組裏,是_M_thread_freelist_array存在的基礎,也是__freelist能正常工作的前提假設,所以不能簡單的追加線程id節點。
下面的代碼把舊鏈表的結構複製到新鏈表裏。
510 __gnu_internal::freelist._M_thread_freelist
511 = &_M_thread_freelist[_M_old_freelist - _M_old_array];
這又是一個用_M_xxxx給局部變量命名的例子!_M_old_freelist所指的節點,是舊鏈表裏的第一個節點,於是“_M_old_freelist - _M_old_array”得到的就是這個節點的下標。所以上面的代碼其實是把新鏈表的第一個節點設置成與舊鏈表對應的那個節點。下圖描述這句代碼的作用。
假設舊鏈表_M_old_array長度爲8,其中1,2,3,6號線程id已經分配出去,只有4,5,7,8還是空閒的,_M_old_freelist指向節點4。圖中還給出了每個節點的next指針的指向,最後一個節點8的next顯然是NULL。新鏈表由局部變量_M_thread_freelist指向,首先我們應該把新鏈表的_M_thread_freelist指針指向對應的4號節點,這正是上面的代碼做的事情。
512 while (_M_old_freelist)
513 {
514 size_t next_id;
515 if (_M_old_freelist->_M_next)
516 next_id = _M_old_freelist->_M_next - _M_old_array;
517 else
518 next_id = __gnu_internal::freelist._M_max_threads;
519 _M_thread_freelist[_M_old_freelist->_M_id - 1]._M_next
520 = &_M_thread_freelist[next_id];
521 _M_old_freelist = _M_old_freelist->_M_next;
522 }
這段代碼所做的事情是:遍歷舊鏈表的每個節點,獲得它們的next指針的指向,然後把新鏈表對應的節點也設置成同樣的指向。比如第一次循環之後,上圖的例子會變成這樣:
再經過2此循環,新鏈表的節點5的next會指向7,而節點7的next會指向8,_M_old_freelist這時也指向了舊鏈表的節點8,如下圖所示:
接下來,新鏈表裏節點8的next應該指向何方?因爲舊鏈表裏,節點8的next是NULL,所以不能作爲參考了。第518行代碼告訴了我們答案:指向第freelist._M_max_threads個節點(節點9)。而我們從前面對新鏈表的“串聯”操作裏知道,新鏈表本來就是像糖葫蘆一樣串好了的,於是9,10,11,12自然就加入了空閒鏈表的行列:
523 ::operator delete(static_cast<void*>(_M_old_array));
釋放掉舊的鏈表。
524 }
525 __gnu_internal::freelist._M_thread_freelist_array
526 = _M_thread_freelist;
527 __gnu_internal::freelist._M_max_threads
528 = _M_options._M_max_threads;
好了,這4行代碼是情況1)和情況2)的最後的共同點,因爲無論哪種情況,最後都要把新的空閒鏈表交給freelist來管理。
529 }
530 }
至止,我們研究完了_M_initialize函數裏最難的代碼部分,因爲這部分代碼需要考慮__per_type_pool_policy策略下任何時間都可能發生的初始化工作,而且是對同一個全局變量的初始化。如果讀者讀懂了這部分代碼,相信也會有同樣的感覺。
532 const size_t __max_threads = _M_options._M_max_threads + 1;
533 for (size_t __n = 0; __n < _M_bin_size; ++__n)
初始化每個bin的內部數據。
534 {
535 _Bin_record& __bin = _M_bin[__n];
536 __v = ::operator new(sizeof(_Block_record*) * __max_threads);
537 __bin._M_first = static_cast<_Block_record**>(__v);
計算_M_first數組的字節大小,然後分配空間。_M_first的角色我們在前面已經介紹過了。
539 __bin._M_address = NULL;
540
541 __v = ::operator new(sizeof(size_t) * __max_threads);
542 __bin._M_free = static_cast<size_t*>(__v);
543
544 __v = ::operator new(sizeof(size_t) * __max_threads);
545 __bin._M_used = static_cast<size_t*>(__v);
初始化free和used計數器數組。free計數器用來記錄每個線程當前空閒的內存塊個數,used記錄分配出去的內存塊個數。多線程內存分配器容易出現的一個問題是,某個線程的空閒塊鏈表變得很長,但是這個線程卻再也不需要這些內存;同時其他線程卻總是內存緊缺,於是不得不從全局鏈表裏不斷申請內存。如果這種現象重複下去,最終程序可能因內存不足而退出,而實際上這個程序任何時刻只需要50M不到的內存就足夠了。mt allocator爲了防止這種現象,採用了一種把線程裏的空閒內存歸還給全局鏈表的算法,而這個算法需要每個線程有自己的free和used計數器。
547 __v = ::operator new(sizeof(__gthread_mutex_t));
548 __bin._M_mutex = static_cast<__gthread_mutex_t*>(__v);
549
550 #ifdef __GTHREAD_MUTEX_INIT
551 {
552 // Do not copy a POSIX/gthr mutex once in use.
553 __gthread_mutex_t __tmp = __GTHREAD_MUTEX_INIT;
554 *__bin._M_mutex = __tmp;
555 }
556 #else
557 { __GTHREAD_MUTEX_INIT_FUNCTION(__bin._M_mutex); }
558 #endif
初始化bin的鎖成員。
559 for (size_t __threadn = 0; __threadn < __max_threads; ++__threadn)
560 {
561 __bin._M_first[__threadn] = NULL;
562 __bin._M_free[__threadn] = 0;
563 __bin._M_used[__threadn] = 0;
564 }
初始化3個數組的值。
565 }
對每個bin的初始化工作完成了。
566 }
567 else
這個else對應的是第588行的“if (__gthread_active_p())”。所以下面的代碼是在沒有鏈接多線程庫的時候纔會運行。
568 {
569 for (size_t __n = 0; __n < _M_bin_size; ++__n)
570 {
571 _Bin_record& __bin = _M_bin[__n];
572 __v = ::operator new(sizeof(_Block_record*));
573 __bin._M_first = static_cast<_Block_record**>(__v);
574 __bin._M_first[0] = NULL;
575 __bin._M_address = NULL;
576 }
把內存池當成單線程下的__pool<false>處理。
577 }
578 _M_init = true;
最後設置_M_init,說明全部初始化工作完成。
579 }
終於研究完了多線程下的__pool<true>::_M_initialize函數,現在我建議讀者再回頭溫習一下上面所有的代碼,以確認自己是否真的對它們瞭如指掌了。同時我還有一點需要說明,在<mt_allocator.cc>的620行,還有一個
<mt_allocator.cc>
620 void
621 __pool<true>::_M_initialize(__destroy_handler)
這個重載函數的代碼和前面無參數_M_initialize函數的一模一樣,實際上__destroy_handler只是一個函數指針類型,參數連名字也沒有。這個重載函數從沒有被調用過,所以我也不確定它的作用是什麼。