std::lock_guard 示例,解释其工作原理

Posted

技术标签:

【中文标题】std::lock_guard 示例,解释其工作原理【英文标题】:std::lock_guard example, explanation on why it works 【发布时间】:2016-02-07 10:04:35 【问题描述】:

我在我的项目中遇到了一个问题,即需要线程之间就可以写入的资源进行通信,因此必须进行同步。但是,除了基本级别之外,我对同步一无所知。

考虑此链接中的最后一个示例:http://www.bogotobogo.com/cplusplus/C11/7_C11_Thread_Sharing_Memory.php

#include <iostream>
#include <thread>
#include <list>
#include <algorithm>
#include <mutex>

using namespace std;

// a global variable
std::list<int>myList;

// a global instance of std::mutex to protect global variable
std::mutex myMutex;

void addToList(int max, int interval)

    // the access to this function is mutually exclusive
    std::lock_guard<std::mutex> guard(myMutex);
    for (int i = 0; i < max; i++) 
        if( (i % interval) == 0) myList.push_back(i);
    


void printList()

    // the access to this function is mutually exclusive
    std::lock_guard<std::mutex> guard(myMutex);
    for (auto itr = myList.begin(), end_itr = myList.end(); itr != end_itr; ++itr ) 
        cout << *itr << ",";
    


int main()

    int max = 100;

    std::thread t1(addToList, max, 1);
    std::thread t2(addToList, max, 10);
    std::thread t3(printList);

    t1.join();
    t2.join();
    t3.join();

    return 0;

该示例演示了三个线程(两个写入者和一个读取者)如何访问公共资源(列表)。

使用了两个全局函数:一个由两个写入线程使用,一个由读取线程使用。这两个函数都使用 lock_guard 来锁定相同的资源,即列表。

现在这就是我无法理解的内容:阅读器在与两个编写器线程不同的范围内使用锁,但仍锁定相同的资源。这怎么行?我对互斥锁的有限理解非常适合编写器功能,您有两个线程使用完全相同的功能。我可以理解,在您即将进入保护区时进行检查,如果其他人已经在里面,您等待。

但是当范围不同时呢?这表明存在某种比进程本身更强大的机制,某种运行时环境阻止“迟到”线程的执行。但我认为 C++ 中没有这样的东西。所以我很茫然。

这里到底发生了什么?

【问题讨论】:

我想补充一点,互斥锁是atomic。希望这会有所帮助。 【参考方案1】:

让我们看一下相关行:

std::lock_guard<std::mutex> guard(myMutex);

注意lock_guard 引用了全局 互斥体myMutex。也就是说,所有三个线程都使用相同的互斥锁。 lock_guard 所做的基本上是这样的:

在构造时,它会锁定 myMutex 并保留对它的引用。 在销毁时(即当守卫的范围离开时),它会解锁myMutex

互斥体总是同一个,它与作用域无关。 lock_guard 的目的只是为了让您更轻松地锁定和解锁互斥锁。例如,如果您手动lock/unlock,但您的函数在中间某处抛出异常,它永远不会到达unlock 语句。因此,以手动方式必须确保互斥锁始终解锁。另一方面,无论何时退出函数,lock_guard 对象都会自动销毁——不管它是如何退出的。

【讨论】:

您当然是对的,但恐怕我被误解了。我真正的意思是它在低级别是如何工作的。一段数据只是内存设置的东西。如果你想保护这些数据,你必须封装它。在这里工作似乎没有任何类似的东西,您可以以相同的方式轻松锁定一个简单的 int 。那么锁所代表的保护机制究竟是如何实现的呢?由于范围无关紧要,因此必须有其他东西与后期线程相交,将其关闭,并在资源再次空闲时再次唤醒它。找我? @Daviatore:叫做操作系统调度器 这与封装无关。互斥体本质上是您在调用lock() 时获取的资源。如果无法获取资源(因为另一个线程正在持有它),会发生什么取决于具体的实现;要么使用mutual exclusion algorithms,要么由操作系统处理它,将你的线程放入等待队列(参见例如tutorial on POSIX Threads Programming,了解UNIX/Linux系统是如何做到的)。【参考方案2】:

myMutex 是全局的,用于保护myListguard(myMutex) 只需锁上锁,从块中退出会导致其破坏,解除锁。 guard 只是一种方便的方式来接合和解除锁定。

除此之外,mutex 不会保护任何数据。它只是提供了一种保护数据的方法。它是保护数据的设计模式。因此,如果我编写自己的函数来修改如下列表,mutex 将无法保护它。

void addToListUnsafe(int max, int interval)

    for (int i = 0; i < max; i++) 
        if( (i % interval) == 0) myList.push_back(i);
    

只有在需要访问数据的所有代码段在访问之前都使用锁并在完成后解除使用时,锁才会起作用。这种在每次访问之前和之后接合和解除锁定的设计模式是保护数据的原因(myList 在您的情况下)

现在你会想,为什么要使用mutex,为什么不使用bool。是的,您可以,但您必须确保 bool 变量会表现出某些特征,包括但不限于以下列表。

    不会跨多个线程缓存(易失性)。 读写将是原子操作。 您的锁可以处理存在多个执行管道(逻辑内核等)的情况。

有不同的synchronization 机制提供“更好的锁定”(跨进程与跨线程、多处理器与单处理器等),但代价是“性能较慢”,因此您应该始终选择一种锁定机制对你的情况来说已经足够了。

【讨论】:

【参考方案3】:

只是补充这里其他人所说的话......

C++ 中有一个思想叫做 Resource Acquisition Is Initialization (RAII),就是将资源绑定到对象的生命周期:

Resource Acquisition Is Initialization 或 RAII,是一种 C++ 编程技术,它绑定在使用前必须获取的资源的生命周期(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接(有限供应的任何东西)到对象的生命周期。

C++ RAII Info

std::lock_guard&lt;std::mutex&gt; 类的使用遵循 RAII 理念。

为什么有用?

考虑一个不使用std::lock_guard的情况:

std::mutex m; // global mutex
void oops() 
   m.lock();
   doSomething();
   m.unlock();

在这种情况下,使用全局互斥体并在调用doSomething() 之前被锁定。然后一旦doSomething() 完成,互斥锁就会解锁。

这里的一个问题是如果出现异常会发生什么?现在您冒着永远无法到达m.unlock() 行的风险,该行将互斥锁释放到其他线程。 因此,您需要涵盖遇到异常的情况:

std::mutex m; // global mutex
void oops() 
   try 
      m.lock();
      doSomething();
      m.unlock();
    catch(...) 
      m.unlock(); // now exception path is covered
      // throw ...
   

这可行,但丑陋、冗长且不方便。

现在让我们编写自己的简单锁守卫。

class lock_guard 
private:
   std::mutex& m;
public: 
   lock_guard(std::mutex& m_):(m(m_)) m.lock();   // lock on construction
   ~lock_guard()  t.unlock();                    // unlock on deconstruction

当 lock_guard 对象被销毁时,它会确保互斥锁被解锁。 现在我们可以使用这个 lock_guard 以更好/更清洁的方式处理之前的案例:

std::mutex m; // global mutex
void ok() 
      lock_guard lk(m); // our simple lock guard, protects against exception case 
      doSomething(); 
 // when scope is exited our lock guard object is destroyed and the mutex unlocked

这与std::lock_guard 背后的想法相同。

同样,这种方法用于许多不同类型的资源,您可以通过点击 RAII 上的链接了解更多信息。

【讨论】:

【参考方案4】:

这正是锁的作用。当一个线程获取锁时,无论它在代码中的哪个位置获取锁,如果另一个线程持有锁,它必须等待轮到它。当一个线程释放锁时,无论它在代码中的哪个位置释放锁,另一个线程都可能获取该锁。

锁保护数据,而不是代码。他们通过确保所有访问受保护数据的代码在持有锁时都这样做,从而将可能访问相同数据的任何代码中的其他线程排除在外。

【讨论】:

以上是关于std::lock_guard 示例,解释其工作原理的主要内容,如果未能解决你的问题,请参考以下文章

linux C++互斥锁std::lock_guard(轻锁)std::unique_lock(重锁)区别

C++ std::lock_guard 自动加锁释放锁 原理

std::mutex 锁定函数和 std::lock_guard<std::mutex> 的区别?

将 std::lock_guard 与 try_lock 一起使用

c++ 如何将 std::mutex 和 std::lock_guard 与仿函数一起使用?

为啥冗余的额外范围块会影响 std::lock_guard 行为?