考虑以下课程:
class testThreads
{
private:
int var; // variable to be modified
std::mutex mtx; // mutex
public:
void set_var(int arg) // setter
{
std::lock_guard<std::mutex> lk(mtx);
var = arg;
}
int get_var() // getter
{
std::lock_guard<std::mutex> lk(mtx);
return var;
}
void hundred_adder()
{
for(int i = 0; i < 100; i++)
{
int got = get_var();
set_var(got + 1);
sleep(0.1);
}
}
};
当我在main()中创建两个线程时,每个线程都有hundred_adder
的线程函数修改相同的变量var,var的最终结果总是不同,即不是200而是其他一些数字。
从概念上讲,为什么使用带有getter和setter函数的互斥锁不是线程安全的?锁定防护装置是否无法阻止比赛状况变化?什么是替代解决方案?
答案 0 :(得分:4)
Thread a: get 0
Thread b: get 0
Thread a: set 1
Thread b: set 1
瞧,即使它应该是2,var也是1。
显然你需要锁定整个操作:
for(int i = 0; i < 100; i++){
std::lock_guard<std::mutex> lk(mtx);
var += 1;
}
或者,您可以将变量设为原子(即使是在您的情况下也可以放松)。
答案 1 :(得分:3)
int got = get_var();
set_var(got + 1);
您的get_var()
和set_var()
本身是线程安全的。但是get_var()
后跟set_var()
的组合序列不是。没有互斥体可以保护整个序列。
您有多个并发线程执行此操作。您有多个线程正在调用get_var()
。在第一个完成它并解锁互斥锁之后,另一个线程可以立即锁定互斥锁并获得与第一个线程所做的got
相同的值。绝对没有什么可以防止多个线程同时锁定并获得相同的got
。
然后两个线程都会调用set_var()
,将受互斥锁保护的int
更新为相同的值。
这只是可能发生的一种可能性。您可以轻松地让多个线程按顺序获取互斥锁,从而将var
递增几个值,然后在几秒钟之前调用get_var()
的其他一些停滞的线程,然后才转到调用set_var()
,从而将var
重置为更小的值。
答案 2 :(得分:2)
代码在线程安全中显示,它永远不会设置或获取变量的部分值。
但是您对方法的使用并不能保证值会正确更改:从多个线程读取和写入可能会相互冲突。两个线程读取值(11),都将它递增(到12)并且都设置为相同(12) - 现在你计算了2但实际上只增加了一次。
修复选项:
答案 3 :(得分:0)
为什么不直接使用std :: atomic作为共享数据(在本例中为var)?这将更安全有效。
答案 4 :(得分:0)
这绝对经典。
一个线程获取var
的值,释放mutex
,另一个线程在第一个线程有机会更新之前获得相同的值。
因此,该过程可能会失去增量。
有三个明显的解决方案:
void testThreads::inc_var(){
std::lock_guard<std::mutex> lk(mtx);
++var;
}
这是安全的,因为互斥锁一直保持到变量更新为止。
接下来:
bool testThreads::compare_and_inc_var(int val){
std::lock_guard<std::mutex> lk(mtx);
if(var!=val) return false;
++var;
return true;
}
Then write code like:
int val;
do{
val=get_var();
}while(!compare_and_inc_var(val));
这是有效的,因为循环重复,直到它确认它正在更新它读取的值。这可能会导致实时锁定,但在这种情况下它必须是瞬态的,因为线程只能因为另一个线程而无法取得进展。
最后将int var
替换为std::atomic<int> var
,然后使用++var
或var.compare_exchange(val,val+1)
或var.fetch_add(1);
进行更新。
注意:注意compare_exchange(var,var+1)
无效......
++
保证在std::atomic<>
类型上是原子的,但尽管“看起来”像一般操作,但int
不存在此类保证。
std::atomic<>
还提供了适当的内存屏障(以及提示需要何种屏障的方法),以确保正确的线程间通信。
std::atomic<>
应该是可用的无等待,无锁实现。检查您的文档和标记is_lock_free()
。