C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++11 互斥锁

C++11中互斥锁的使用

作者:泽林阿

本文主要介绍了C++11中互斥锁的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

我们现在有一个需求,我们需要对 g_exceptions 这个 vector 的访问进行同步处理,确保同一时刻只有一个线程能向它插入新的元素。为此我使用了一个 mutex 和一个锁(lock)。mutex 是同步操作的主体,在 C++ 11 的 <mutex> 头文件中,有四种风格的实现:

下面是一个使用 std::mutex 的例子(注意 get_id() 和 sleep_for() 两个辅助方法的使用,上文已有提及)。

 #include <iostream>
 #include <thread>
 #include <mutex>
 #include <chrono>
std::mutex g_lock;
void func()
{
    g_lock.lock();
    std::cout << "entered thread " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(rand() % 10));
    std::cout << "leaving thread " << std::this_thread::get_id() << std::endl;
    g_lock.unlock();
}
int main()
{
    srand((unsigned int)time(0));
    std::thread t1(func);
    std::thread t2(func);
    std::thread t3(func);
    t1.join();
    t2.join();
    t3.join();
    return 0;
   } 

输出如下:

entered thread 10144 
leaving thread 10144 
entered thread 4188 
leaving thread 4188 
entered thread 3424 
leaving thread 3424 

lock() unlock() 两个方法应该很好懂,前者锁住 mutex,如果该 mutex 不可用,则阻塞线程;稍后,后者解锁线程。

下面一个例子展示了一个简单的线程安全的容器(内部使用了 std::vector)。该容器提供用于添加单一元素的 add()方法,以及添加多个元素的 addrange() 方法(内部调用 add() 实现)。

注意:尽管如此,下面会指出,由于 va_args 的使用等原因,这个容器并非真正线程安全。此外,dump() 方法不应属于容器,在实际实现中它应该作为一个独立的辅助函数。这个例子的目的仅仅是展示 mutex 的相关概念,而非实现一个完整的线程安全的容器。

template <typename T>
class container 
{
    std::mutex _lock;
    std::vector<T> _elements;
public:
    void add(T element) 
    {
        _lock.lock();
        _elements.push_back(element);
        _lock.unlock();
    }
    void addrange(int num, ...)
    {
        va_list arguments;
        va_start(arguments, num);
        for (int i = 0; i < num; i++)
        {
            _lock.lock();
            add(va_arg(arguments, T));
            _lock.unlock();
        }
        va_end(arguments); 
    }
    void dump()
    {
        _lock.lock();
        for(auto e : _elements)
            std::cout << e << std::endl;
        _lock.unlock();
    }
};
void func(container<int>& cont)
{
    cont.addrange(3, rand(), rand(), rand());
}
int main()
{
    srand((unsigned int)time(0));
    container<int> cont;
    std::thread t1(func, std::ref(cont));
    std::thread t2(func, std::ref(cont));
    std::thread t3(func, std::ref(cont));
    t1.join();
    t2.join();
    t3.join();
    cont.dump();
    return 0;
}

当你运行这个程序时,会进入死锁。原因:在 mutex 被释放前,容器尝试多次持有它,这显然不可能。这就是为什么引入 std::recursive_mutex ,它允许一个线程对 mutex 多重持有。允许的最大持有次数并不确定,但当达到上限时,线程锁会抛出 std::system_error 错误。因此,要解决上面例子的错误,除了修改 addrange 令其不再调用 lock 和 unlock 之外,可以用 std::recursive_mutex 代替 mutex

template <typename T> 
   class container  
{     
   std::recursive_mutex _lock;     
       // ... 
  }; 

成功输出:

6334 
18467 
41 
6334 
18467 
41 
6334 
18467 
41 

敏锐的读者可能注意到,每次调用 func() 输出的都是相同的数字。这是因为,seed 是线程局部量,调用 srand() 只会在主线程中初始化 seed,在其他工作线程中 seed 并未被初始化,所以每次得到的数字都是一样的。

手动加锁和解锁可能造成问题,比如忘记解锁或锁的次序出错,都会造成死锁。C++ 11 标准提供了若干类和函数来解决这个问题。封装类允许以 RAII 风格使用 mutex,在一个锁的生存周期内自动加锁和解锁。这些封装类包括:

  • lock_guard:当一个实例被创建时,会尝试持有 mutex (通过调用 lock());当实例销毁时,自动释放 mutex (通过调用 unlock())。不允许拷贝。
  • unique_lock:通用 mutex 封装类,与 lock_guard 不同,还支持延迟锁、计时锁、递归锁、移交锁的持有权,以及使用条件变量。不允许拷贝,但允许转移(move)。

借助这些封装类,可以把容器改写为:

template <typename T>
class container 
{
    std::recursive_mutex _lock;
    std::vector<T> _elements;
public:
    void add(T element) 
    {
        std::lock_guard<std::recursive_mutex> locker(_lock);
        _elements.push_back(element);
    }
    void addrange(int num, ...)
    {
        va_list arguments;
        va_start(arguments, num);
        for (int i = 0; i < num; i++)
        {
            std::lock_guard<std::recursive_mutex> locker(_lock);
            add(va_arg(arguments, T));
        }
        va_end(arguments); 
    }
    void dump()
    {
        std::lock_guard<std::recursive_mutex> locker(_lock);
        for(auto e : _elements)
            std::cout << e << std::endl;
    }
}

读者可能会提出, dump() 方法不更改容器的状态,应该设为 const。但如果你添加 const 关键字,会得到如下编译错误:

‘std::lock_guard<_Mutex>::lock_guard(_Mutex &)' : cannot convert parameter 1 from ‘const std::recursive_mutex' to ‘std::recursive_mutex &' 

一个 mutex (不管何种风格)必须被持有和释放,这意味着 lock() unlock 方法必被调用,这两个方法是 non-const 的。所以,逻辑上 lock_guard 的声明不能是 const (若该方法 为 const,则 mutex 也为 const)。这个问题的解决办法是,将 mutex 设为 mutablemutable 允许由 const 方法更改 mutex 状态。不过,这种用法仅限于隐式的,或「元(meta)」状态——譬如,运算过的高速缓存、检索完成的数据,使得下次调用能瞬间完成;或者,改变像 mutex 之类的位元,仅仅作为一个对象的实际状态的补充。

template <typename T>
class container 
{
   mutable std::recursive_mutex _lock;
   std::vector<T> _elements;
public:
   void dump() const
   {
      std::lock_guard<std::recursive_mutex> locker(_lock);
      for(auto e : _elements)
         std::cout << e << std::endl;
   }
};

这些封装类锁的构造函数可以通过重载的声明来指定锁的策略。可用的策略有:

这些策略的声明方式如下:

struct defer_lock_t { };  
struct try_to_lock_t { };  
struct adopt_lock_t { };  
constexpr std::defer_lock_t defer_lock = std::defer_lock_t();  
constexpr std::try_to_lock_t try_to_lock = std::try_to_lock_t();  
constexpr std::adopt_lock_t adopt_lock = std::adopt_lock_t();

除了这些 mutex 封装类之外,标准库还提供了两个方法用于锁住一个或多个 mutex:

下面是一个死锁案例:有一个元素容器,以及一个 exchange() 函数用于互换两个容器里的某个元素。为了实现线程安全,这个函数通过一个和容器关联的 mutex,对这两个容器的访问进行同步。

template <typename T>
class container 
{
public:
    std::mutex _lock;
    std::set<T> _elements;
    void add(T element) 
    {
        _elements.insert(element);
    }
    void remove(T element) 
    {
        _elements.erase(element);
    }
};
void exchange(container<int>& cont1, container<int>& cont2, int value)
{
    cont1._lock.lock();
    std::this_thread::sleep_for(std::chrono::seconds(1)); // <-- forces context switch to simulate the deadlock
    cont2._lock.lock();    
    cont1.remove(value);
    cont2.add(value);
    cont1._lock.unlock();
    cont2._lock.unlock();
}

假如这个函数在两个线程中被调用,在其中一个线程中,一个元素被移出容器 1 而加到容器 2;在另一个线程中,它被移出容器 2 而加到容器 1。这可能导致死锁——当一个线程刚持有第一个锁,程序马上切入另一个线程的时候。

int main()
{
    srand((unsigned int)time(NULL));
    container<int> cont1; 
    cont1.add(1);
    cont1.add(2);
    cont1.add(3);
    container<int> cont2; 
    cont2.add(4);
    cont2.add(5);
    cont2.add(6);
    std::thread t1(exchange, std::ref(cont1), std::ref(cont2), 3);
    std::thread t2(exchange, std::ref(cont2), std::ref(cont1), 6);
    t1.join();
    t2.join();
    return 0;
}

要解决这个问题,可以使用 std::lock,保证所有的锁都以不会死锁的方式被持有:

void exchange(container<int>& cont1, container<int>& cont2, int value)
{
    std::lock(cont1._lock, cont2._lock); 
    cont1.remove(value);
    cont2.add(value);
    cont1._lock.unlock();
    cont2._lock.unlock();
}

总结

到此这篇关于C++11中互斥锁的使用的文章就介绍到这了,更多相关C++11 互斥锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文