首页 > 编程笔记 > C++笔记 阅读:15

C++ std::mutex互斥锁的用法(附带实例)

线程允许你在同一时间执行多个函数,但通常这些线程需要访问共享资源。访问共享资源需要同步,这样同一时间共享资源才只能被一个线程读写。

在本节中,我们将看到针对同步线程访问共享数据,C++ 标准定义了哪些机制以及它们是如何工作的。

本节讨论的互斥量(mutex)和锁(lock)类可以在 <mutex> 头文件里的 std 命名空间获得。

C++ mutex互斥锁的使用方式

使用以下模式来同步访问单一共享资源:

1) 在合适的上下文(类或全局域)定义 mutex:
std::mutex g_mutex;

2) 每个线程访问共享资源前,在这个 mutex 上获取 lock:
void thread_func()
{
    using namespace std::chrono_literals;
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        std::cout << "running thread " << std::this_thread::get_id() << '\n';
    }

    std::this_thread::yield();
    std::this_thread::sleep_for(2s);

    {
        std::lock_guard<std::mutex> lock(g_mutex);
        std::cout << "done in thread " << std::this_thread::get_id() << '\n';
    }
}

使用以下模式来避免死锁,同时同步访问多个共享资源:
1)在合适的上下文(类或全局域)给每个共享资源定义互斥量:
template <typename T>
struct container
{
    std::mutex    mutex;
    std::vector<T> data;
};

2)用 std::lock() 的避免死锁算法同时锁住互斥量:
template <typename T>
void move_between(container<T> & c1, container<T> & c2, T const value)
{
    std::lock(c1.mutex, c2.mutex);
    // continued at 3.
}

3) 锁住后,转移每个互斥量的所有权利到 std::lock_guard 类,以保证函数(或作用域)结束后它们可以被安全释放:
// continued from 2.
std::lock_guard<std::mutex> l1(c1.mutex, std::adopt_lock);
std::lock_guard<std::mutex> l2(c2.mutex, std::adopt_lock);

c1.data.erase(std::remove(c1.data.begin(), c1.data.end(), value),c1.data.end());
c2.data.push_back(value);
}

深度剖析C++ mutex互斥锁

互斥量是一种同步原语,保证我们在多线程中同步访问共享资源。

C++ 标准库提供了几种实现:
第一个锁住互斥量的线程获取所有权,并继续执行。包括已经获取互斥量的线程在内,后续任何尝试获取互斥量的线程都将失败,在互斥量用 unlock() 释放前,lock() 将阻塞线程。如果一个线程想要多次获取互斥量而不被阻塞和进入死锁,应该使用 recursive_mutex 类模板。

用互斥量来访问共享资源的典型做法是锁住互斥量,使用共享资源,释放互斥量:
g_mutex.lock();
// use the shared resource such as std::cout
std::cout << "accessing shared resource" << '\n';
g_mutex.unlock();
然而这种使用互斥量的方式很容易犯错。这是因为在所有的执行路径(即正常返回路径和异常返回路径上),每次 lock() 的调用都必须与 unlock() 配对。

无论函数的执行方式如何,为了安全地获取和释放互斥量,C++ 标准定义了几个锁类:
1) 在前面已经介绍过 std::lock_guard 这种锁机制,以 RAII 方式呈现的互斥量封装。当互斥量被创建时获取,被销毁时释放。

std::lock_guard 从 C++11 开始可用。以下是 lock_guard 的一种典型实现方式:
template <class M>
class lock_guard
{
public:
    typedef M mutex_type;

    explicit lock_guard(M& Mtx) : mtx(Mtx)
    {
        mtx.lock();
    }

    lock_guard(M& Mtx, std::adopt_lock_t) : mtx(Mtx)
    {
    }

    ~lock_guard() noexcept
    {
        mtx.unlock();
    }

    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
private:
    M& mtx;
};

2) std::unique_lock 是互斥量所有权的封装。它支持延时锁定、时间锁定、递归锁定和所有权转移,并且与条件变量配合使用。它从 C++11 开始可用。

3) std::shared_lock 是互斥量共享所有权的封装。它支持延时锁定、时间锁定、递归锁定和所有权转移。它从 C++14 开始可用。

4) std::scoped_lock 是以 RAII 方式实现的一种多互斥量的封装。构造时,它将在避免死锁的同时去获取互斥量的所有权,就像用 std::lock() 一样。析构时,它将以相反的顺序释放互斥量。它从 C++17 开始可用。

在前面的示例中,我们使用 std::mutex 和 std::lock_guard 来保护程序中多个线程共享的 std::cout 流的访问。以下示例将展示在多线程中 thread_func() 是如何并发执行的:
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
    threads.emplace_back(thread_func);
for (auto & t : threads)
    t.join();

这段程序可能的输出如下:

running thread 140296545582272
running thread 140296846157568
running thread 140296837764864
running thread 140296829372160
running thread 140296820979456
done in thread 140296854558272
done in thread 140296846157568
done in thread 140296837764864
done in thread 140296829379456
done in thread 140296829372160


当线程尝试获取用来保护多个共享资源的多个互斥量所有权时,逐个去获取可能导致死锁。让我们看看以下的示例:
template <typename T>
void move_between(container<T> & c1, container<T> & c2, T const value)
{
    std::lock_guard<std::mutex> l1(c1.mutex);
    std::lock_guard<std::mutex> l2(c2.mutex);

    c1.data.erase(
        std::remove(c1.data.begin(), c1.data.end(), value),
        c1.data.end());
    c2.data.push_back(value);
}

container<int> c1;
c1.data.push_back(1);
c1.data.push_back(2);
c1.data.push_back(3);

container<int> c2;
c2.data.push_back(4);
c2.data.push_back(5);
c2.data.push_back(6);

std::thread t1(move_between<int>, std::ref(c1), std::ref(c2), 3);
std::thread t2(move_between<int>, std::ref(c2), std::ref(c1), 6);

t1.join();
t2.join();
在此示例中,container 类保存了可能被不同线程同时访问的数据,因此它需要获取互斥量来保护。move_between() 是线程安全的函数,它将元素从容器删除并添加到第二个容器中。为此,它需要依序获取两个容器的互斥量,然后将元素从第一个容器中删除并添加到第二个容器的尾部。

然而此函数很容易导致死锁,因为在获取锁时可能触发竞争条件。假设两个不同线程同时执行这个函数,但使用了不同的参数:
为了避免产生这种类似情况的死锁,互斥量必须以避免死锁的方式来获取。为此标准库里提供了 std::lock() 的实用函数。move_between() 函数修改为用以下代码替换两个锁:
std::lock(c1.mutex, c2.mutex);
std::lock_guard<std::mutex> l1(c1.mutex, std::adopt_lock);
std::lock_guard<std::mutex> l2(c2.mutex, std::adopt_lock);
为了互斥量在函数执行结束后(或当作用域结束时)被适当的释放,互斥量的所有所有权必须被转移给 lock_guard 对象。

在之前的示例中,可用 C++17 中的新互斥量封装 std::scoped_lock 来简化代码。它以避免死锁方式来获取多个互斥量。当区域锁销毁时,互斥量将被释放。下面一行代码跟之前代码等同:
std::scoped_lock lock(c1.mutex, c2.mutex);
在区域代码块中,scoped_lock 类提供了简化的机制用来获取一个或多个互斥量,能帮助写出更简单健壮的代码。

相关文章