C++ mutex

数据竞争现象

数据竞争(data race)。在并发编程中,多个线程如果同时访问相同的数据,就会发成data race,这时候 访问就可能失败。解决这个问题的方法是给共享的资源加锁 mutex。线程A要访问共享数据的时候获取mutex, 然后上锁,那么这个共享资源就只能被线程A获取,其他线程就只有在线程A解锁之后(即交出数据访问权限) 才能访问这个共享资源。

比如:

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex increment;

void countEven(const std::vector<int>& numbers,
               int& numEven)
{
    for (const auto n : numbers)
    {
        if (n % 2 == 0)
        {
            increment.lock();
            numEven++;
            increment.unlock();
        }
    }
}

int main()
{ ...

numEven是共享资源,其他线程可能也会访问,这时候线程在访问前increment.lock(),结束之后increment.unlock(), 这样就可以保证多个线程访问numEven而不会出现数据问题。

使用std::mutex容易出现的问题

但是在实际使用中,这种方法往往会因为一些问题导致线程无法解锁,或者由于程序员的原因忘了解锁,其他线程就无法 访问数据了。比如下面的两种情况:

  • unlock之前出现异常,是线程退出,但是无法解锁。

    std::mutex increment;
    void threadA (int& numEven) {
       ...
       increment.lock();
       ...  //处理数据,但是发生异常退出线程。
    
       increment.unlock();  // 由于前面异常退出,导致unlock没有执行到,不能解锁。
    }
    
    int main() {
       ...
    }
    
  • 重复锁定导致死锁

    之前看见过一个代码,大致如下,导致了死锁:

    std::mutex mutex;
    int s_num;
    void handle() {
        mutex.lock();
        ...
        mutex.unlock();
    }
    
    void threadA() {
        mutex.lock();
        ....
        handle();     // 因为前面已经lock了,handle里面的lock就一直阻塞,导致死锁。
        mutex.unlock();
    }
    

上面两种情况或者由于程序员的疏忽,或者由于程序处理异常的原因,都会导致死锁。那么有没有什么可以 避免这种情况呢? 当然 c++11 中的 std::lock_guard 可以避免这种情况。

自动上锁和解锁 std::lock_guard

std::lock_guard 是c++11提供的一个 class ,结合 std::mutex 使用,它在实例化的时候,在构造函数中 自动上锁,上锁之后,在作用域块内一直锁住,知道离开作用域,对象被析构时,自动解锁。这样将 lock 和 unlock 绑定在对象构造函数和析构函数中,就避免了没有解锁的问题。

比如:

std::mutex mtx;
void threadA (int& numEven) {
   ...
   std::lock_guard<std::mutex> lck(mtx)
   ...  //处理数据,即使发生异常,lck实例也会析构,同时unlock。
}

int main() {
   ...
}

std::lock_guard的局限

使用 std::lock_guard 很简单,但是不太灵活。而且在使用时还会有其他的问题,这个 class,没有其 他的接口,也就是除了在构造,析构的时候 lock 和 unlock ,我们几乎不能进行其他的操作。比如:

  • 作用域执行较长代码或者耗时较长,浪费其他线程等待的时间

    std::mutex mtx;
    void threadA (int& numEven) {
       ...
       std::lock_guard<std::mutex> lck(mtx)
       numEven++;   // 只有这里访问numEven,后面就不再使用。
       ...          // 这里需要执行比较长的代码,或者需要比较长的时间。其他线程无法获取锁。
    }
    
    void threadB (int& numEven) {
       ...
       std::lock_guard<std::mutex> lck(mtx); // 如果threadA没有执行完,这里就无法往下执行。
       numEven++;
       ...
    }
    
    int main() {
       ...
    }
    
    
  • 需要使用条件锁的时候,无法解锁。

    使用条件锁时:std::condition_variable::wait(...) 的时候需要 unlock ,当收到 std::condition_variable.notify_one() 信号之后需要 lock 。但是 guard_lock 不能提相应的接口实现这个功能。因此,使用条件锁的时候,根 本就不能使用 guard_lock。

如果在不需要 lock 的时候给我们提供一个 unlock 的接口那就好了。这样 threadB 就不用等待那么久了。 可以实现吗?当然,c++11 还提供了另一个 class: std::unique_lock ,这个 class 可以实现这个需求。

std::unique_lock()

unique_lock 和 lock_guard 一样都是在构造的时候自动 lock,在析构的时候自动 unlock,但不同的是他 提供了其他的接口可以在中间让我们unlock。

比如在执行较长代码中,我们可以在不需要lock的时候,调用接口unlock :

std::mutex mtx;
void threadA (int& numEven) {
   ...
   std::unique_lock<std::mutex> lck(mtx)
   numEven++;   // 只有这里访问numEven,后面就不再使用。
   lck.unlock();  // 调用接口unlock。
   ...
   lck.lock();    // 如果后面还需要lock,可以调用接口lock。
   ...
}

void threadB (int& numEven) {
}

int main() {
   ...
}

当然,在使用条件锁的时候,他会根据环境自动处理lock和unlock:

std::mutex mtx;
std::condition_variable cond;
bool someCheck() {
  if(...) return true;
  return false;
}

void threadA (int& numEven) {
   ...
   std::unique_lock<std::mutex> lck(mtx)
   cond.wait(lck, someCheck());
   numEven++;   // 只有这里访问numEven,后面就不再使用。
   lck.unlock();  // 调用接口unlock。
   ...
   lck.lock();    // 如果后面还需要lock,可以调用接口lock。
   ...
}

void threadB (int& numEven) {
   std::lock_guard<std::mutex> lck(mtx);
   ...
   cond.notify_one();
}

int main() {
   ...
}

总结:std::unique_lock()和std::lock_guard()的区别

两者相同的地方是,都会在构造的时候自动lock,析构的时候自动unlock。

不同的是 lock_guard 使用简单,但是没有 lock/unlock 的接口,所以使用不灵活。而 unique_lock 则提供了这些接口。所以:

  • 在执行较长时间,或者要在不需要lock的时候unlock,那就要使用 unique_lock。
  • 在使用条件锁的时候,只能使用 unique_lock。

Comments

comments powered by Disqus