关于c ++:std :: lock_guard示例,说明其工作原理

std::lock_guard example, explanation on why it works

在我的项目中,我已经达到了一个要求,即在资源上的线程之间进行通信,而这些线程很可能被写入,所以同步是必须的。但是,除了基本级别之外,我不太了解同步。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <thread>
#include <list>
#include
#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;
}

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

使用两个全局函数:一个由两个编写器线程使用,另一个由读线程使用。两个函数都使用锁保护来锁定同一个资源,即列表。

下面是我无法理解的地方:读卡器在不同于两个编写器线程的范围内使用锁,但仍然锁定相同的资源。这怎么办?我对互斥体的有限理解很适合编写函数,在这里有两个线程使用完全相同的函数。我可以理解,当你即将进入保护区时,检查是正确的,如果其他人已经在里面,你就等着。

但当范围不同时?这表明有某种机制比进程本身更强大,某种运行时环境阻止了"延迟"线程的执行。但我认为C++中没有这样的东西。所以我不知所措。

引擎盖下面到底发生了什么?


让我们看一下相关的行:

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

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

  • 施工时,锁定myMutex并保持参考。
  • 一旦被摧毁(即当守卫的范围被留下时),它就会解锁myMutex

互斥体总是相同的,与作用域无关。lock_guard的目的只是为了让您更容易锁定和解锁互斥体。例如,如果您手动执行lock/unlock,但是您的函数在中间的某个地方抛出了一个异常,它将永远不会到达unlock语句。所以,用手动的方式操作,你必须确保互斥锁始终是解锁的。另一方面,无论函数如何退出,只要函数退出,lock_guard对象就会自动销毁。


myMutex是全球性的,是用来保护myList的。guard(myMutex)简单地接合锁,并且从块的出口导致其破坏,从而断开锁。guard只是一种方便的方式来接合和分离锁。

在这种情况下,mutex不会保护任何数据。它只是提供了一种保护数据的方法。它是保护数据的设计模式。因此,如果我编写自己的函数来修改下面的列表,那么mutex就不能保护它。

1
2
3
4
5
6
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机制以"较慢的性能"为代价提供"更好的锁定"(跨进程、跨线程、多处理器、单处理器等),因此您应该始终选择一种适合您情况的锁定机制。


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

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