【问题标题】:When would getters and setters with mutex be thread safe?带有互斥锁的 getter 和 setter 什么时候是线程安全的?
【发布时间】:2017-06-04 21:25:25
【问题描述】:

考虑以下类:

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 函数中使用互斥锁不是线程安全的? lock-guards 是否无法防止竞争条件发生变化?什么是替代解决方案?

【问题讨论】:

  • A - 获取,B - 获取,A - 设置,B - 设置。 :(

标签: c++ multithreading pthreads


【解决方案1】:
Thread a: get 0
Thread b: get 0
Thread a: set 1
Thread b: set 1

你瞧,var 是 1,尽管它应该是 2。

显然你需要锁定整个操作:

for(int i = 0; i < 100; i++){
     std::lock_guard<std::mutex> lk(mtx);
     var += 1;
}

或者,您可以将变量设为原子变量(在您的情况下,即使是轻松的变量也可以这样做)。

【讨论】:

    【解决方案2】:
      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 重置为更小的值。

    【讨论】:

    • ...这就是线程安全不是可组合的原因。即,完全由线程安全部分构建程序/库/类/任何东西并不能使整个事物成为线程安全的。
    【解决方案3】:

    代码以线程安全的方式显示,它永远不会设置或获取变量的部分值。

    但是您对方法的使用并不能保证值会正确更改:从多个线程读取和写入可能会相互冲突。两个线程都读取值 (11),都将其递增(到 12),然后都设置为相同的 (12) - 现在您数为 2,但实际上只递增了一次。

    修复选项:

    • 提供“安全增量”操作
    • 提供等效的 InterlockedCompareExchange 以确保您正在更新的值与原始值相对应,并根据需要重试
    • 将调用代码包装到单独的互斥体中或使用其他同步机制来防止操作混合。

    【讨论】:

      【解决方案4】:

      你为什么不直接使用 std::atomic 共享数据(在这种情况下为 var )?这样会更安全高效。

      【讨论】:

      • ...也许,但那样他就永远不会知道他的问题的答案。
      【解决方案5】:

      这绝对是经典。 一个线程获得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&lt;int&gt; var 并使用++varvar.compare_exchange(val,val+1)var.fetch_add(1); 来更新它。 注意:通知compare_exchange(var,var+1) 无效...

      ++ 保证在 std::atomic&lt;&gt; 类型上是原子的,但尽管“看起来”像一个单一的操作,但对于 int 不存在这样的保证。

      std::atomic&lt;&gt; 还提供适当的内存屏障(以及提示需要哪种屏障的方法)以确保正确的线程间通信。

      std::atomic&lt;&gt; 应该是一个无等待、无锁的实现(如果可用)。检查您的文档和标志is_lock_free()

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-05-20
        • 2011-09-08
        • 2014-09-26
        • 1970-01-01
        • 1970-01-01
        • 2020-02-08
        相关资源
        最近更新 更多