首页 > 解决方案 > 如何实现具有自然语法的线程安全容器?

问题描述

前言

如果按原样使用,下面的代码会导致未定义的行为:

vector<int> vi;
...
vi.push_back(1);  // thread-1
...
vi.pop(); // thread-2

传统方法是使用以下方法修复它std::mutex

std::lock_guard<std::mutex> lock(some_mutex_specifically_for_vi);
vi.push_back(1);

然而,随着代码的增长,这样的事情开始看起来很麻烦,因为每次在方法之前都会有一个锁。此外,对于每个对象,我们可能必须维护一个互斥体。

客观的

在不影响访问对象和声明显式互斥体的语法的情况下,我想创建一个模板,以便它完成所有样板工作。例如

Concurrent<vector<int>> vi;  // specific `vi` mutex is auto declared in this wrapper
...
vi.push_back(1); // thread-1: locks `vi` only until `push_back()` is performed
...
vi.pop ()  // thread-2: locks `vi` only until `pop()` is performed

在当前的 C++ 中,这是不可能实现的。但是,我尝试了一个代码,如果只是更改vi.vi->,那么事情会按上述代码注释中的预期工作。

代码

// The `Class` member is accessed via `->` instead of `.` operator
// For `const` object, it's assumed only for read purpose; hence no mutex lock
template<class Class,
         class Mutex = std::mutex>
class Concurrent : private Class
{
  public: using Class::Class;

  private: class Safe
           {
             public: Safe (Concurrent* const this_,
                           Mutex& rMutex) :
                     m_This(this_),
                     m_rMutex(rMutex)
                     { m_rMutex.lock(); }
             public: ~Safe () { m_rMutex.unlock(); }

             public: Class* operator-> () { return m_This; }
             public: const Class* operator-> () const { return m_This; }
             public: Class& operator* () { return *m_This; }
             public: const Class& operator* () const { return *m_This; }

             private: Concurrent* const m_This;
             private: Mutex& m_rMutex;
           };

  public: Safe ScopeLocked () { return Safe(this, m_Mutex); }
  public: const Class* Unsafe () const { return this; }

  public: Safe operator-> () { return ScopeLocked(); }
  public: const Class* operator-> () const { return this; }
  public: const Class& operator* () const { return *this; }

  private: Mutex m_Mutex;
};

演示

问题


澄清

对于相互依赖的语句,需要更长的锁定时间。因此,引入了一种方法:ScopeLocked(). 这相当于std::lock_guard(). 然而,给定对象的互斥体是在内部维护的,所以它在语法上仍然更好。
例如,而不是低于有缺陷的设计(如答案中所建议):

if(vi->size() > 0)
  i = vi->front(); // Bad: `vi` can change after `size()` & before `front()`

一个应该依靠以下设计:

auto viLocked = vi.ScopeLocked();
if(viLocked->size() > 0)
  i = viLocked->front();  // OK; `vi` is locked till the scope of `viLocked`

换句话说,对于相互依赖的语句,应该使用ScopeLocked().

标签: c++templatesthread-safetyc++14temporary-objects

解决方案


不要这样做。

几乎不可能创建一个线程安全的集合类,其中每个方法都需要一个锁。

考虑以下您提议的 Concurrent 类的实例。

Concurrent<vector<int>> vi;

开发人员可能会出现并执行此操作:

 int result = 0;
 if (vi.size() > 0)
 {
     result = vi.at(0);
 }

另一个线程可能会在第一个线程调用size()和之间进行此更改at(0)

vi.clear();

所以现在,操作的同步顺序是:

vi.size()  // returns 1
vi.clear() // sets the vector's size back to zero
vi.at(0)   // throws exception since size is zero

因此,即使您有一个线程安全的向量类,两个竞争线程也可能导致在意想不到的地方引发异常。

这只是最简单的例子。还有其他方式,多个线程同时尝试读/写/迭代可能会无意中破坏您对线程安全的保证。

您提到整个事情的动机是这种模式很麻烦:

vi_mutex.lock();
vi.push_back(1);
vi_mutex.unlock();

事实上,有一些帮助类可以使这个更干净,即 lock_guard 它将使用互斥锁锁定其构造函数并解锁析构函数

{
    lock_guard<mutex> lck(vi_mutex);
    vi.push_back(1);
}

然后实践中的其他代码成为线程安全ala:

{
     lock_guard<mutex> lck(vi_mutex);
     result = 0;
     if (vi.size() > 0)
     {
         result = vi.at(0);
     }
}

更新:

我编写了一个示例程序,使用您的 Concurrent 类来演示导致问题的竞争条件。这是代码:

Concurrent<list<int>> g_list;

void thread1()
{
    while (true)
    {
        if (g_list->size() > 0)
        {
            int value = g_list->front();
            cout << value << endl;
        }
    }

}

void thread2()
{
    int i = 0;
    while (true)
    {
        if (i % 2)
        {
            g_list->push_back(i);
        }
        else
        {
            g_list->clear();
        }
        i++;
    }
}

int main()
{

    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join(); // run forever

    return 0;
}

在未优化的构建中,上面的程序会在几秒钟内崩溃。(零售有点困难,但错误仍然存​​在)。


推荐阅读