线程安全智能指针






3.46/5 (8投票s)
有了这个线程安全的智能指针,您就可以在多线程环境中使用任何类型的对象。
引言
通常,当我们想在多个线程中同时使用一个变量时,我们会将某个同步对象放在它旁边,在访问该变量之前锁定它,并在之后解锁它。
这会引入许多难以发现的 bug。
- 我们可能会忘记在访问变量之前对同步对象进行锁定,这可能导致数据损坏。
- 如果我们忘记在变量使用后解锁同步对象,那么另一个线程可能会永远阻塞,等待那个同步对象。
- 将此类变量的引用或指针作为参数传递给现有函数,而没有其同步对象,将导致数据损坏,因为此变量可以被无保护地访问。
示例
下面的例子演示了上述情况中的第一种,即(如果我们忘记锁定)可能导致数据损坏。
/////////////////////////////////////////////////
#include<string>
class CMyClass
{
public:
. . . // constructor, destructor and other things
std::string m_data;
HANDLE m_mutex;
};
DWORD WINAPI ThreadProc( LPVOID lpParameter)
{
CMyClass* myClass = (CMyClass*)lpParameter;
// accessing without a prior lock
myClass->m_data.assign(“another thread”);
return 0;
}
int main()
{
CMyClass myClass;
// create another thread
HANDLE threadA = ::CreateThread( ..., ThreadProc, &myClass, ...);
// locking
::WaitForSingleObject(m_mutex, INFINITE);
// accessing
myClass.m_data.assign(“main thread”);
// cleanup
::ReleaseMutex(mutex);
::CloseHandle(threadA);
}
在此示例中,对象 myClass
的 m_data
成员可能会损坏,因为主线程在访问 m_data
之前已锁定 m_mutex
,但另一个线程在尝试更改 m_data
的值之前根本没有使用 m_mutex
。如果这些线程同时尝试更改 m_data
的值,那么 m_data
可能会损坏。为了避免这种情况,必须使用相同的同步对象来同步这两个线程中对 m_data
的访问。
解决方案
解决此问题的方法是采用面向对象的编程方法,将同步对象绑定到我们希望线程安全的数据,将它们两者隐藏在一个线程安全的智能指针中,并使用这个线程安全的智能指针而不是它所封装的原始数据。
新线程安全智能指针的特性
- 它可以用于线程同步,因为它包含自己的同步对象(它不需要在其周围的任何同步对象)。
- 它基于 Boost 库(它使用
boost::shared_ptr
来保存数据)。 - 它是一个 Windows 的线程安全智能指针,因为其中的互斥量是使用 WinAPI 创建的(如果有人能想出如何用 Boost 的某些互斥量替换 WinAPI 互斥量,请在下面的论坛中留言)。
- 它是一个智能指针,因为
- 它允许通过
operator->
和operator*
对数据进行同步访问。 - 它会删除它所包含的数据。
- 它可以按值传递。
- 它允许通过
它包含两个类
CThreadPtr
– 主类,包含指向任何类型数据的指针、用于同步指针访问的互斥量以及用于保护自身状态的另一个互斥量。CThreadPtr::CLocker
– 嵌套类,提供对CThreadPtr
互斥量的锁定/解锁操作。锁定在CLocker
的构造函数中完成,解锁在析构函数中完成。
如何使用它
让我们来看下面这段代码,这是上面示例使用线程安全智能指针重构后的代码。
/////////////////////////////////////////////////
#include<string>
#include "ThreadPtr.hpp" // for the thread-safe smart pointer
typedef CThreadPtr<std::string> TMyDataPtr;
class CMyClass
{
public:
// constructor
CMyClass()
: m_data(new std::string)
{. . .}
. . . // destructor and other things
TMyDataPtr m_data;
};
DWORD WINAPI ThreadProc( LPVOID lpParameter)
{
CMyClass* myClass = (CMyClass*)lpParameter;
// locking and accessing
myClass->m_data->assign(“another thread”);
return 0;
}
int main()
{
CMyClass myClass;
// create another thread
HANDLE threadA = ::CreateThread( ..., ThreadProc, &myClass, ...);
// locking and accessing
myClass.m_data->assign(“main thread”);
// cleanup
::CloseHandle(threadA);
}
在这个例子中,我们根本不可能忘记在 m_data
使用之前对其进行锁定。
事务性锁定
事务性锁定允许在不让其他线程访问任何一个变量的情况下,对多个变量进行修改,直到事务结束。这通常是保持这些变量相互一致所必需的。
事务性锁定可以通过使用 CLocker
(CThreadPtr
的嵌套类)来实现,如下面的示例所示。
typedef CThreadPtr<MyStructWithSeveralDataMembers> TMyDataPtr;
void MakeTransaction( TMyDataPtr& obj)
{
...
{
TMyDataPtr::CLocker lock(obj);
...
obj->ChangeOneDataMember();
obj->ChangeAnotherDataMember();
...
}// here the mutex is released in the lock's destructor
...
}
在此示例中,变量 lock
将在退出其定义的块时被销毁(并释放互斥量),从而允许在线程安全的方式下对 obj
执行任意数量的操作。
operator*()
要获取 CThreadPtr
对象指向的数据的引用,我们可以使用以下代码行:
typedef CThreadPtr<std::string> TMyDataPtr;
TMyDataPtr ptr = new std::string;
...
{
TMyDataPtr::CLocker lock(ptr);
...
std::string& ref = **ptr;
// ref is no more protected by a mutex so variable lock above is necessary
ref = "new value";
}
要获取 CThreadPtr
对象指向的数据的指针,我们可以使用以下代码行:
typedef CThreadPtr<std::string> TMyDataPtr;
TMyDataPtr ptr = new std::string;
...
{
TMyDataPtr::CLocker lock(ptr);
...
std::string* rawPtr = **ptr;
// rawPtr is no more protected by a mutex so variable lock above is necessary
rawPtr->assign("new value");
}
小心使用 bool 运算符 ()
在以下代码段中,CThreadPtr
的 operator bool ()
被用来确保 ptr
已被初始化,以避免访问冲突。
typedef CThreadPtr<std::string> TMyDataPtr;
TMyDataPtr ptr;
...
// this is a use of operator bool ()
if (ptr) // line A
ptr->assign("some value"); // line B
但无论如何,代码中都可能发生异常。如果另一个线程调用同一对象 ptr
的 reset
方法,则可能发生这种情况……
ptr->reset();
……当第一个线程位于 **A 行**和 **B 行**之间时。为了避免这种数据竞争,应该在 **事务性锁定 A 行**和 **B 行**周围使用 **事务性锁定**(如上所述)。但如果您不打算在代码的任何地方对对象 ptr
应用 reset
方法,那么使用 **事务性锁定**只会降低代码的性能和可读性。
现在,有了这个线程安全的智能指针,您就可以在多线程环境中使用任何新的(非现有)对象了。
延伸阅读
您可以尝试修改这个智能指针,使其能够包含 COM 接口的指针,方法是将 CThreadPtr
中的 boost::shared_ptr
替换为 CComPtr
。否则,您可以在稍后查看我的另一篇文章,该文章专门介绍 COM 线程安全的智能指针。