问题描述
下面的代码按原样使用将导致未定义的行为:
Below code results in undefined behaviour, if used as is:
vector<int> vi;
...
vi.push_back(1); // thread-1
...
vi.pop(); // thread-2
传统方法是使用std::mutex
对其进行修复:
Traditional approach is to fix it with std::mutex
:
std::lock_guard<std::mutex> lock(some_mutex_specifically_for_vi);
vi.push_back(1);
但是,随着代码的增长,这种事情开始显得很麻烦,因为每次在方法前都会有一个锁.而且,对于每个对象,我们可能都必须维护一个互斥体.
However, as the code grows, such things start looking cumbersome, as everytime there will be a lock before a method. Moreover, for every object, we may have to maintain a mutex.
在不影响访问对象和声明显式互斥量的语法的前提下,我想创建一个模板,使其完成所有样板工作.例如
Without compromising in the syntax of accessing an object and declaring an explicit mutex, I would like to create a template such that, it does all the boilerplate work. e.g.
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->
,那么事情就会按上述代码注释中的预期进行.
In current C++, it's impossible to achieve this. However, I have attempted a code where if just change vi.
to vi->
, then the things work as expected in above code comments.
// 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 ++中出现不确定的行为? - 这个小实用程序类是否在所有情况下都为封装对象提供了线程安全的目的?
- Is using the temporary object to call a function with overloaded
operator->()
leads to undefined behavior in C++? - Does this small utility class serve the purpose of thread-safety for an encapsulated object in all the cases?
Demo
Questions
对于相互依赖的语句,需要更长的锁定时间.因此,引入了一种方法:ScopeLocked()
.这等效于std::lock_guard()
.但是,给定对象的互斥锁是在内部维护的,因此在语法上仍然更好.
例如而不是低于有缺陷的设计(如答案所示):
For inter-dependent statements, one needs a longer locking. Hence, there is a method introduced: ScopeLocked()
. This is an equivalent of the std::lock_guard()
. However the mutex for a given object is maintained internally, so it's still better syntactically.
e.g. instead of below flawed design (as suggested in an answer):
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()
.
In other words, for the inter-dependent statements, one should be using the ScopeLocked()
.
推荐答案
请勿执行此操作.
几乎不可能创建一个线程安全的集合类,其中每个方法都需要一个锁.
It's almost impossible to make a thread safe collection class in which every method takes a lock.
请考虑以下您建议的Concurrent类的实例.
Consider the following instance of your proposed Concurrent class.
Concurrent<vector<int>> vi;
开发人员可能会这样做:
A developer might come along and do this:
int result = 0;
if (vi.size() > 0)
{
result = vi.at(0);
}
另一个线程可能会在调用size()
和at(0)
的第一个线程之间进行此更改.
And another thread might make this change in between the first threads call to size()
and at(0)
.
vi.clear();
所以现在,操作的同步顺序是:
So now, the synchronized order of operations is:
vi.size() // returns 1
vi.clear() // sets the vector's size back to zero
vi.at(0) // throws exception since size is zero
因此,即使您拥有线程安全的向量类,两个竞争线程也可能导致异常被抛出到意想不到的地方.
So even though you have a thread safe vector class, two competing threads can result in an exception being thrown in unexpected places.
那只是最简单的例子.还有其他方法,多个线程尝试同时进行读/写/迭代可能会无意间破坏您对线程安全的保证.
That's just the simplest example. There are other ways in which multiple threads attempting to read/write/iterate at the same time could inadvertently break your guarantee of thread safety.
您提到整个事情都是由这种麻烦的模式引起的:
You mentioned that the whole thing is motivated by this pattern being cumbersome:
vi_mutex.lock();
vi.push_back(1);
vi_mutex.unlock();
实际上,有一些帮助程序类可以使此程序更清洁,即lock_guard,它将使用一个互斥锁来锁定其构造函数并在析构函数上解锁
In fact, there are helper classes that will make this cleaner, namely lock_guard that will take a mutex to lock in its constructor and unlock on the destructor
{
lock_guard<mutex> lck(vi_mutex);
vi.push_back(1);
}
然后其他实践中的代码将变为线程安全ala:
Then other code in practice becomes thread safe ala:
{
lock_guard<mutex> lck(vi_mutex);
result = 0;
if (vi.size() > 0)
{
result = vi.at(0);
}
}
更新:
我编写了一个示例程序,使用您的Concurrent类来演示导致问题的争用条件.这是代码:
I wrote a sample program, using your Concurrent class to demonstrate the race condition that leads to a problem. Here's the code:
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;
}
在未优化的构建中,上述程序在几秒钟内崩溃. (零售有点难,但是错误仍然存在.)
In a non-optimized build, the program above crashes in a matter of seconds. (Retail is a bit harder, but the bug is still there).
这篇关于如何用自然的语法实现线程安全的容器?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!