c++ - 如何实现具有自然语法的线程安全容器?
问题描述
前言
如果按原样使用,下面的代码会导致未定义的行为:
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;
};
演示
问题
- 使用临时对象调用重载的函数
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
因此,即使您有一个线程安全的向量类,两个竞争线程也可能导致在意想不到的地方引发异常。
这只是最简单的例子。还有其他方式,多个线程同时尝试读/写/迭代可能会无意中破坏您对线程安全的保证。
您提到整个事情的动机是这种模式很麻烦:
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;
}
在未优化的构建中,上面的程序会在几秒钟内崩溃。(零售有点困难,但错误仍然存在)。
推荐阅读
- xml - 使用 XSLT 重定向 Visual Studio 解决方案文件
- javascript - 具有不可变列表的算术平均值
- c# - LTE ASN.1 BCCH - LD - SCH 消息解码器
- wordpress - 错误:在文件 '.\docker-compose.yml' 中,卷必须是映射,而不是字符串。- Docker 与 Wordpress
- c++ - c++17折叠表达式打印功能,可支持20000个元素高效
- sqlite - 永远不要使用 MobileServiceSQLiteStore 获取在线 Azure SQL 数据
- macos - 预期行尾等,但找到“脚本”
- c# - 转换代码以读取字节并存储数据
- c++ - PPL。如何在同一个工作线程上安排多个任务?
- c# - 编译器在不同的 PC 中为相同的代码抛出异常