在上一篇博客《STL空間配置器那點事》簡單介紹了空間配置器的基本實現
兩級空間配置器處理,一級相關細節問題,同時簡單描述了STL各組件之間的關系以及設計到的設計模式等。
在最後,又關於STL空間配置的效率以及空間釋放時機做了簡單的探討。
為什麼會有線程安全問題?
認真學過操作系統的同學應該都知道一個問題。
first--進程是系統資源分配和調度的基本單位,是操作系統結構的基礎,是一個程序的運行實體,同時也是一個程序執行中線程的容器
seconed--進程中作為資源分配基本單位,管理著所有線程共享資源:代碼段,數據段,堆,部分共享區(IPC中的共享內存等)。。棧則是線程私有的。
所以,由此就有:如果我們的數據存放位置處在數據段,堆這兩個地方,那麼就會有線程安全問題:
1 #include <iostream> 2 using namespace std; 3 static int * arr = new int(4); //arr作為全局變量存在於數據段,new申請所得空間存在於堆上。 4 5 void testThreadSafe(int arg) 6 { 7 *arr = arg; 8 } 9 10 int main() 11 { 12 int arg; 13 cin >> arg; 14 testThreadSafe(arg); 15 cout << (*arr)<<endl; 16 return 0; 17 }
做個簡單分析,假設進程同時運行到了第七行,因為程序執行的最小粒度是更為細致的cpu指令而不是一個代碼語句。
所以可能A線程和B線程同時執行修改*arr = arg;,但是兩個線程中cin>>arg輸入的值不一樣,那麼就有問題。
兩個線程各自執行到15行時,顯示的結果是一樣的(因為線程共享該區域),但他們本來卻不該相同。
這就是線程安全問題。
STL中,一級空間配置器簡單封裝malloc,free同時引入sethandler機制。而malloc,free作為最基本的系統調用是線程安全的,
所以問題就在二級空間配置器的實現部分了。
各位還記得二級配置器內部結構定義吧。
template <bool threads, int inst> class __DefaultAllocTemplate { //... protected: //桶結構,保存鏈表 static _Obj* _freeList[_NFREELISTS]; //..... };
這裡的核心結構,保存自由鏈表的指針數組就是各靜態數據,存在於數據段,於是就有了線程安全問題。
linux環境,互斥鎖
win環境,臨界區(臨界資源訪問問題)
對於STL的二級空間配置器中,線程安全問題的唯一存在也就是對於已組織的自由鏈表的訪問了(也就是Allocate和Deallocate了):
兩個線程同時向空間配置器申請內存塊(ps,A未完成取出該節點並將表指針指向下一個節點時,B線程來了。於是兩個線程同時得到一塊內存);
//////A執行玩1,尚未執行2,B就來申請空間。最終兩個線程都修改數組中指針指向y,且共同擁有x
兩個線程同時向空間配置器釋放內存塊;
////a釋放執行1而沒有來得及執行2,於是乎,在1。5的情況系,b釋放,進入。於是,最終結果,a塊,b塊都指向了x,但是數組中指針只是指向了後來修改他的值,於是就有了內存洩漏。
核心代碼給出:
文件Alloc.h中部分代碼
#pragma once #include "Config.h" #include "Trace.h" #include "Threads.h" #ifdef __STL_THREADS #define __NODE_ALLOCATOR_THREADS true //用於二級空間配置器翻非類型模板參數 #define __NODE_ALLOCATOR_LOCK \ { if (threads) _S_node_allocator_lock._M_acquire_lock(); } #define __NODE_ALLOCATOR_UNLOCK \ { if (threads) _S_node_allocator_lock._M_release_lock(); } #else // Thread-unsafe # define __NODE_ALLOCATOR_LOCK # define __NODE_ALLOCATOR_UNLOCK # define __NODE_ALLOCATOR_THREADS false #endif # ifdef __STL_THREADS static _STL_mutex_lock _S_node_allocator_lock; # endif template <bool threads, int inst> class __DefaultAllocTemplate { class _Lock; friend class _Lock; class _Lock { public: _Lock() { __TRACE("鎖保護\n"); __NODE_ALLOCATOR_LOCK; } ~_Lock() { __TRACE("鎖撤銷\n"); __NODE_ALLOCATOR_UNLOCK; } }; static void* Allocate(size_t n) { void * ret = 0; __TRACE("二級空間配置器申請n = %u\n",n); if(n>_MAX_BYTES) ret = MallocAlloc::Allocate(n); _Obj* volatile * __my_free_list = _freeList + _FreeListIndex(n); //利用RAII(資源獲取即初始化原則)進行封裝,保證 即使內部拋出異常,依舊執行解鎖操作 #ifdef __STL_THREADS _Lock __lock_instance; #endif _Obj* __result = *__my_free_list; if (__result == 0) ret = _Refill(RoundUp(n)); else { *__my_free_list = __result -> _freeListLink; ret = __result; } return ret; } static void Deallocate(void* p, size_t n) { if(!p) { return; } __TRACE("二級空間配置器刪除p = %p,n = %d\n",p,n); if (n > (size_t) _MAX_BYTES) MallocAlloc::Deallocate(p, n); else { _Obj* volatile* __my_free_list = _freeList + _FreeListIndex(n); _Obj* q = (_Obj*)p; #ifdef __STL_THREADS //進行資源歸還自由鏈表時的鎖操作。 _Lock __lock_instance; #endif q -> _freeListLink = *__my_free_list; *__my_free_list = q; } }
文件Threads.h
#pragma once #if defined(__STL_PTHREADS) #include <pthread.h> #endif #include "Config.h" __STLBEGIN struct _STL_mutex_lock { #if defined(__STL_PTHREADS) pthread_mutex_t _M_lock; void _M_initialize() { pthread_mutex_init(&_M_lock, NULL); } void _M_acquire_lock() { pthread_mutex_lock(&_M_lock); } void _M_release_lock() { pthread_mutex_unlock(&_M_lock); } #else /* No threads */ void _M_initialize() {} void _M_acquire_lock() {} void _M_release_lock() {} #endif }; __STLEND
簡單測試結果
其中TRACE打印的“鎖保護”,“鎖撤銷” 部分就是二級空間配置器資源分配時鎖機制的保護實現了。
其利用了C++的RAII(資源獲取即初始化方案)
同時利用C++對象特性,退出作用域即執行析構函數,將解鎖封裝,巧妙的避免了死鎖問題的產生
死鎖:簡單理解就是,因為某個線程鎖定資源進行訪問時,因為異常等原因退出執行,但是沒來的及解鎖,致使其他線程都無法訪問共享資源的現象就是死鎖。更細致的解釋請找google叔。
最後,說明的是,實際的STL源碼中因為需要考慮平台,系統兼容性等問題,對於鎖的使用通過宏編譯技術,有比較長的一段代碼,我這裡只是取出了當下linux平台可用代碼放在了自己的Threads.h
更詳細代碼請關注個人另一博客:http://www.cnblogs.com/lang5230/p/5556611.html
或者github獲取更新中的代碼:https://github.com/langya0/llhProjectFile/tree/master/STL