主题:[转帖]如何解决静态变量的线程安全问题
这是一位同事写的文章, 在此转贴, 大家讨论.
通常C++中很常见的Singleton实现,是利用一个模版类中的静态函数(或者一个直接的模版函数),在其中定义一个静态变量来实现的,例如:
template <class T>
class Singleton
{
public:
static T& Instance()
{
static T obj;
return obj;
}
};
而这里存在一个著名的隐患,就是static变量的初始化时间为函数被首次调用时。其汇编码如下(在VS2008下生成):
004115BD mov eax,dword ptr [`Singleton<SomeClass>::Instance'::`2'::`local static guard' (41A154h)]
004115C2 and eax,1
004115C5 jne Singleton<SomeClass>::Instance+79h (4115F9h)
004115C7 mov eax,dword ptr [`Singleton<SomeClass>::Instance'::`2'::`local static guard' (41A154h)]
004115CC or eax,1
004115CF mov dword ptr [`Singleton<SomeClass>::Instance'::`2'::`local static guard' (41A154h)],eax
004115D4 mov dword ptr [ebp-4],0
004115DB mov ecx,offset obj (41A148h)
004115E0 call SomeClass::SomeClass (41100Ah)
004115E5 push offset `Singleton<SomeClass>::Instance'::`2'::`dynamic atexit destructor for 'obj'' (411226h)
004115EA call @ILT+125(_atexit) (411082h)
004115EF add esp,4
004115F2 mov dword ptr [ebp-4],0FFFFFFFFh
,有一个dword类型的local static guard,初始值为0,函数每次被调用时,都检查这个数的最后一个二进制位是否为1,如果不为1,则调用构造函数004115E0 call SomeClass::SomeClass (41100Ah) 。
注:在C++0x标准中,规定了编译器必须完成static变量的线程安全问题,所以在支持0x标准的编译器下,直接使用原始的Singleton定义即可。
因此,如果存在多线程同时首次访问该函数,多个线程获取到的local static guard均为0,然后分别调用了一次构造函数。如果对象中包含内存的分配,或者包含重要资源的分配,则这些内存/资源会发生泄漏。而且更严重的问题是,如果对象的构造函数较为复杂,例如往容器中填写了某些数据,那么甚至可能会产生严重错误或崩溃,甚至即使加锁也不能解决问题!
举例:
构造函数流程为:
1、调用成员的构造函数(这通常是无法改变的)
2、初始化锁
3、加锁
4、往成员的某个容器里填写数据
5、解锁。
那么,以下情况下会导致十分严重的连锁错误,最后极度可能发生崩溃:
1、A线程调用成员的构造函数
2、A线程初始化锁
3、A线程加锁
4、A容器更改了某个成员,并还未来得及完全保存下来,发生了线程切换,或者是在另一个CPU核心上B线程开始了工作
5、B线程调用成员的构造函数
6、B线程初始化锁,并覆盖了A线程所初始化的锁,使得A线程所初始化的锁泄漏。
7、B线程加锁,不会被A线程阻塞(因为是不同的锁)
8、B线程试图更改某个成员,但是成员数据已经发生了不一致,从而导致生成错误的数据,甚至崩溃
9、B线程解锁
10、A线程试图解锁,但所初始化的锁已经被覆盖,结果使得线程锁发生异常。
从上面的流程可以看出,即使加了临界区锁也不能解决该对象被构造两次的问题(因为锁本身也需要构造)。除非采用命名锁之类的避免受静态内存影响的锁,那样既繁琐又不方便使用。
但是,利用数值等类型的静态变量构造方式的不同(直接在静态区写入数值),我们可以很方便的构造出一个安全的Singleton类:
template <typename T>
class ThreadSafeSingleton
{
public:
static T& Instance()
{
static long state = 0;
static char obj[sizeof(T)];
if (state == 0)
{
long val = InterlockedAdd(&state, 2);
if (val == 2)
{
new (obj) SomeClass();
InterlockedDecrement(&state);
}
else
{
InterlockedAdd(&state, -2);
}
}
while ( state > 1 )//也可以是while ((state & 1) == 0)
{
Sleep(0);
}
return reinterpret_cast<T&>(obj[0]);
}
};
其原理是用state变量代替local static guard,state为0表示对象尚未构造,state为1表示对象已经构造,state大于1通常表示对象正在构造中。另外定义了一个obj空间,用于盛放该对象。
当开始有多个线程同时进入时,state首先为0,所有线程均进入if之后的语句块。此时,所有线程开始InterlockedAdd,因为操作系统保证此操作的原子性,因此有且仅有一个线程会得到val == 2(第一个调用该函数的线程)。此时,这个线程调用该对象的构造函数,并将state减1。其它线程将state再减2,并退出这个语句块,假装自己没有来过。并且,之后进入此函数的线程都会获得state != 0,从而不进入这个语句块。
随后,其他线程均处在后一个循环中,等待构造的线程构造完毕。Sleep(0);主要是为了主动让出CPU控制权,避免构造的线程得不到CPU控制权。构造线程构造完毕后 ,将state减1,并退出这个语句块。此时,所有线程检测到state为1,因此将直接返回。
以上代码还存在一个BUG,就是程序退出时析构函数没有被调用。还有一个比较头疼的地方就是InterlockedAdd这个函数在32位下通常没有。因此小小的修补一下,改用了另一种思路:
template <typename T>
class ThreadSafeSingleton
{
class holder
{
char m_space[sizeof(T)];
long &m_state;
public:
holder(long &state)
: m_state(state)
{
}
~holder()
{
if ( m_state < 0) //通常静态对象的析构应在所有线程结束后,因此不考虑还有线程停留在构造函数中。
reinterpret_cast<T*>(this)->~T();
}
};
public:
static T& Instance()
{
static long state = 0;
static holder obj(state);
if (state == 0)
{
long val = InterlockedIncrement(&state);
if (val == 1)
{
new (&obj) T();
state = (1<<31);
}
}
while ( state > 0 )
{
Sleep(0);
}
return reinterpret_cast<T&>(obj);
}
};
主要就是信任不会有0x80000000个并发线程,因此在构造完成后,state应当一直是负值,不可能被并发线程突然使得state达到0。
以上版本为最终版本,欢迎大家讨论。
通常C++中很常见的Singleton实现,是利用一个模版类中的静态函数(或者一个直接的模版函数),在其中定义一个静态变量来实现的,例如:
template <class T>
class Singleton
{
public:
static T& Instance()
{
static T obj;
return obj;
}
};
而这里存在一个著名的隐患,就是static变量的初始化时间为函数被首次调用时。其汇编码如下(在VS2008下生成):
004115BD mov eax,dword ptr [`Singleton<SomeClass>::Instance'::`2'::`local static guard' (41A154h)]
004115C2 and eax,1
004115C5 jne Singleton<SomeClass>::Instance+79h (4115F9h)
004115C7 mov eax,dword ptr [`Singleton<SomeClass>::Instance'::`2'::`local static guard' (41A154h)]
004115CC or eax,1
004115CF mov dword ptr [`Singleton<SomeClass>::Instance'::`2'::`local static guard' (41A154h)],eax
004115D4 mov dword ptr [ebp-4],0
004115DB mov ecx,offset obj (41A148h)
004115E0 call SomeClass::SomeClass (41100Ah)
004115E5 push offset `Singleton<SomeClass>::Instance'::`2'::`dynamic atexit destructor for 'obj'' (411226h)
004115EA call @ILT+125(_atexit) (411082h)
004115EF add esp,4
004115F2 mov dword ptr [ebp-4],0FFFFFFFFh
,有一个dword类型的local static guard,初始值为0,函数每次被调用时,都检查这个数的最后一个二进制位是否为1,如果不为1,则调用构造函数004115E0 call SomeClass::SomeClass (41100Ah) 。
注:在C++0x标准中,规定了编译器必须完成static变量的线程安全问题,所以在支持0x标准的编译器下,直接使用原始的Singleton定义即可。
因此,如果存在多线程同时首次访问该函数,多个线程获取到的local static guard均为0,然后分别调用了一次构造函数。如果对象中包含内存的分配,或者包含重要资源的分配,则这些内存/资源会发生泄漏。而且更严重的问题是,如果对象的构造函数较为复杂,例如往容器中填写了某些数据,那么甚至可能会产生严重错误或崩溃,甚至即使加锁也不能解决问题!
举例:
构造函数流程为:
1、调用成员的构造函数(这通常是无法改变的)
2、初始化锁
3、加锁
4、往成员的某个容器里填写数据
5、解锁。
那么,以下情况下会导致十分严重的连锁错误,最后极度可能发生崩溃:
1、A线程调用成员的构造函数
2、A线程初始化锁
3、A线程加锁
4、A容器更改了某个成员,并还未来得及完全保存下来,发生了线程切换,或者是在另一个CPU核心上B线程开始了工作
5、B线程调用成员的构造函数
6、B线程初始化锁,并覆盖了A线程所初始化的锁,使得A线程所初始化的锁泄漏。
7、B线程加锁,不会被A线程阻塞(因为是不同的锁)
8、B线程试图更改某个成员,但是成员数据已经发生了不一致,从而导致生成错误的数据,甚至崩溃
9、B线程解锁
10、A线程试图解锁,但所初始化的锁已经被覆盖,结果使得线程锁发生异常。
从上面的流程可以看出,即使加了临界区锁也不能解决该对象被构造两次的问题(因为锁本身也需要构造)。除非采用命名锁之类的避免受静态内存影响的锁,那样既繁琐又不方便使用。
但是,利用数值等类型的静态变量构造方式的不同(直接在静态区写入数值),我们可以很方便的构造出一个安全的Singleton类:
template <typename T>
class ThreadSafeSingleton
{
public:
static T& Instance()
{
static long state = 0;
static char obj[sizeof(T)];
if (state == 0)
{
long val = InterlockedAdd(&state, 2);
if (val == 2)
{
new (obj) SomeClass();
InterlockedDecrement(&state);
}
else
{
InterlockedAdd(&state, -2);
}
}
while ( state > 1 )//也可以是while ((state & 1) == 0)
{
Sleep(0);
}
return reinterpret_cast<T&>(obj[0]);
}
};
其原理是用state变量代替local static guard,state为0表示对象尚未构造,state为1表示对象已经构造,state大于1通常表示对象正在构造中。另外定义了一个obj空间,用于盛放该对象。
当开始有多个线程同时进入时,state首先为0,所有线程均进入if之后的语句块。此时,所有线程开始InterlockedAdd,因为操作系统保证此操作的原子性,因此有且仅有一个线程会得到val == 2(第一个调用该函数的线程)。此时,这个线程调用该对象的构造函数,并将state减1。其它线程将state再减2,并退出这个语句块,假装自己没有来过。并且,之后进入此函数的线程都会获得state != 0,从而不进入这个语句块。
随后,其他线程均处在后一个循环中,等待构造的线程构造完毕。Sleep(0);主要是为了主动让出CPU控制权,避免构造的线程得不到CPU控制权。构造线程构造完毕后 ,将state减1,并退出这个语句块。此时,所有线程检测到state为1,因此将直接返回。
以上代码还存在一个BUG,就是程序退出时析构函数没有被调用。还有一个比较头疼的地方就是InterlockedAdd这个函数在32位下通常没有。因此小小的修补一下,改用了另一种思路:
template <typename T>
class ThreadSafeSingleton
{
class holder
{
char m_space[sizeof(T)];
long &m_state;
public:
holder(long &state)
: m_state(state)
{
}
~holder()
{
if ( m_state < 0) //通常静态对象的析构应在所有线程结束后,因此不考虑还有线程停留在构造函数中。
reinterpret_cast<T*>(this)->~T();
}
};
public:
static T& Instance()
{
static long state = 0;
static holder obj(state);
if (state == 0)
{
long val = InterlockedIncrement(&state);
if (val == 1)
{
new (&obj) T();
state = (1<<31);
}
}
while ( state > 0 )
{
Sleep(0);
}
return reinterpret_cast<T&>(obj);
}
};
主要就是信任不会有0x80000000个并发线程,因此在构造完成后,state应当一直是负值,不可能被并发线程突然使得state达到0。
以上版本为最终版本,欢迎大家讨论。