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

z9gpfhce  于 12个月前  发布在  其他
关注(0)|答案(4)|浏览(127)

在我的项目中,我已经达到了一个需要在资源上的线程之间进行通信的点,这些资源很可能会被写入,所以同步是必须的。然而,除了基本的层面,我真的不了解同步。
考虑此链接中的最后一个示例:C11/C14 7. THREADS WITH SHARED MEMORY AND MUTEX - 2020

#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;
}

字符串
该示例演示了三个线程(两个writer和一个reader)如何访问公共资源(列表)。
使用了两个全局函数:一个由两个写线程使用,一个由读线程使用。两个函数都使用lock_guard来锁定同一个资源,即列表。
现在我想不通的是:读线程使用的锁与两个写线程使用的锁在不同的作用域,但仍然锁定了同一个资源。这是如何工作的?我对互斥锁的有限理解很好地适用于writer函数,在那里你有两个线程使用完全相同的函数。我可以理解,当你即将进入保护区时,会进行检查,如果已经有人在里面了你就等着
但是当作用域不同的时候呢?这就意味着有某种机制比进程本身更强大,某种运行时环境阻止了“迟到”线程的执行。但是我以为c++中没有这样的东西。所以我很困惑。
这里到底发生了什么?

7cjasjjr

7cjasjjr1#

让我们来看看相关线路:

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

字符串
注意,lock_guard引用了 global 互斥量myMutex。也就是说,所有三个线程的互斥量都是相同的。lock_guard本质上是这样做的:

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

互斥体始终是同一个,它与作用域无关。lock_guard的意义只是让你更容易锁定和解锁互斥体。例如,如果你手动lock/unlock,但你的函数在中间某处抛出异常,它永远不会到达unlock语句。所以,手动的方式 * 你 * 必须确保互斥锁 * 总是 * 解锁。另一方面,lock_guard对象在函数退出时自动销毁-不管它是如何退出的。

fsi0uk1n

fsi0uk1n2#

myMutex是全局的,这是用来保护myList的。guard(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变量会表现出某些特征,包括但不限于下面的列表。
1.不能在多个线程之间缓存(易失性)。
1.读和写将是原子操作。
1.你的锁可以处理有多个执行管道(逻辑核心等)的情况。
有不同的synchronization机制提供了“更好的锁定”(跨进程与跨线程,多处理器与单处理器等),但代价是“性能较慢”,因此您应该始终选择一种锁定机制,它只适合您的情况。

zxlwwiss

zxlwwiss3#

只是补充一下其他人所说的...
在C中有一个称为资源获取即初始化(RAII)的思想,它是将资源绑定到对象的生存期的思想:
Resource Acquisition Is Initialization或RAII,是一种C
编程技术,它将在使用之前必须获取的资源(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接--任何存在于有限供应中的资源)的生命周期绑定到对象的生命周期。
C++ RAII Info
std::lock_guard<std::mutex>类的使用遵循RAII的思想。
为什么这很有用?
考虑一个不使用std::lock_guard的情况:

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

字符串
在这种情况下,使用全局互斥锁,并在调用doSomething()之前锁定。2然后,一旦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() { m.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 the scope is exited our lock guard object is destroyed and the mutex is unlocked


这与std::lock_guard背后的想法相同。
这种方法同样适用于许多不同类型的资源,您可以通过RAII上的链接阅读更多信息。

cvxl0en2

cvxl0en24#

这正是锁的作用。当一个线程获取锁时,不管它在代码中的什么位置,如果另一个线程持有锁,它必须等待轮到它。当一个线程释放锁时,不管它在代码中的什么位置,另一个线程都可以获取该锁。
锁保护的是数据,而不是代码。它们通过确保所有访问受保护数据的代码在它持有锁时都能这样做,从而将其他线程排除在可能访问同一数据的任何代码之外。

相关问题