在上一篇博客《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