65.9K
CodeProject 正在变化。 阅读更多。
Home

线程安全智能指针

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.46/5 (8投票s)

2008 年 10 月 20 日

CPOL

4分钟阅读

viewsIcon

51828

downloadIcon

386

有了这个线程安全的智能指针,您就可以在多线程环境中使用任何类型的对象。

引言

通常,当我们想在多个线程中同时使用一个变量时,我们会将某个同步对象放在它旁边,在访问该变量之前锁定它,并在之后解锁它。

这会引入许多难以发现的 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 线程安全的智能指针。

© . All rights reserved.