c++ - 如何使用自然语法实现线程安全的容器?

标签 c++ templates thread-safety c++14 temporary-objects

前言

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

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;
};

Demo

问题

  • 使用临时对象调用具有重载的 operator->() 的函数是否会导致 C++ 中的未定义行为?
  • 这个小实用程序类是否在所有情况下都为封装对象提供线程安全的目的?

说明

对于相互依赖的语句,需要更长的锁定时间。因此,引入了一个方法: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()

最佳答案

不要这样做。

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

考虑您提议的 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

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

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

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

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

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

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

然后其他代码在实践中变成了线程安全的:

{
     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;
}

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

关于c++ - 如何使用自然语法实现线程安全的容器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54781372/

相关文章:

c++ - boost 序列化,按基类型加载归档类会给出错误的数据

javascript - 我无法输出 Handlebars 模板中的数据

在多线程环境中变量可以是静态的还是易失的

java - 同步(this)和同步(其他对象)之间有什么区别

c++ - 可以使用 std::vector 元素的地址作为指针吗?

c++ - 如何在 C++11 中将容器 std::array<type, size> 用于多维数组?

c++ - 从 GCC 得到 "no matching function"

python - 与 SciPy.optimize 的并行性

c++ - 用 defined(X) 定义一个宏

c++ - typedef Foo<> Foo 编译但它有效吗?