在我的项目中,我已经达到了一个需要在资源上的线程之间进行通信的点,这些资源很可能会被写入,所以同步是必须的。然而,除了基本的层面,我真的不了解同步。
考虑此链接中的最后一个示例: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++中没有这样的东西。所以我很困惑。
这里到底发生了什么?
4条答案
按热度按时间7cjasjjr1#
让我们来看看相关线路:
字符串
注意,
lock_guard
引用了 global 互斥量myMutex
。也就是说,所有三个线程的互斥量都是相同的。lock_guard
本质上是这样做的:myMutex
并保持对它的引用。myMutex
。互斥体始终是同一个,它与作用域无关。
lock_guard
的意义只是让你更容易锁定和解锁互斥体。例如,如果你手动lock
/unlock
,但你的函数在中间某处抛出异常,它永远不会到达unlock
语句。所以,手动的方式 * 你 * 必须确保互斥锁 * 总是 * 解锁。另一方面,lock_guard
对象在函数退出时自动销毁-不管它是如何退出的。fsi0uk1n2#
myMutex
是全局的,这是用来保护myList
的。guard(myMutex)
简单地接合锁,并且从块的退出导致其破坏,解除接合锁。guard
只是接合和解除接合锁的方便方式。这样一来,
mutex
就不会保护任何数据了。它只是提供了一种保护数据的方式。这是保护数据的设计模式。所以如果我编写自己的函数来修改列表,mutex
就不能保护它了。字符串
只有当所有需要访问数据的代码在访问之前都使用锁,并在访问完成后解除锁时,锁才能工作。这种在每次访问之前和之后使用和解除锁的设计模式可以保护数据(在您的情况下为
myList
)现在你会想,为什么要使用
mutex
,为什么不使用bool
呢?是的,你可以使用,但是你必须确保bool
变量会表现出某些特征,包括但不限于下面的列表。1.不能在多个线程之间缓存(易失性)。
1.读和写将是原子操作。
1.你的锁可以处理有多个执行管道(逻辑核心等)的情况。
有不同的
synchronization
机制提供了“更好的锁定”(跨进程与跨线程,多处理器与单处理器等),但代价是“性能较慢”,因此您应该始终选择一种锁定机制,它只适合您的情况。zxlwwiss3#
只是补充一下其他人所说的...
在C中有一个称为资源获取即初始化(RAII)的思想,它是将资源绑定到对象的生存期的思想:
Resource Acquisition Is Initialization或RAII,是一种C编程技术,它将在使用之前必须获取的资源(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接--任何存在于有限供应中的资源)的生命周期绑定到对象的生命周期。
C++ RAII Info
std::lock_guard<std::mutex>
类的使用遵循RAII的思想。为什么这很有用?
考虑一个不使用
std::lock_guard
的情况:字符串
在这种情况下,使用全局互斥锁,并在调用
doSomething()
之前锁定。2然后,一旦doSomething()
完成,互斥锁就被解锁。这里的一个问题是,如果出现异常,会发生什么?现在,您将面临永远无法到达
m.unlock()
行的风险,因为它会将互斥体释放给其他线程。因此,您需要考虑遇到异常的情况:型
这是可行的,但很难看,冗长,不方便。
现在,让我们编写自己的简单锁保护。
型
当lock_guard对象被销毁时,它将确保互斥体被解锁。现在我们可以使用这个lock_guard以一种更好/更干净的方式来处理前面的情况:
型
这与
std::lock_guard
背后的想法相同。这种方法同样适用于许多不同类型的资源,您可以通过RAII上的链接阅读更多信息。
cvxl0en24#
这正是锁的作用。当一个线程获取锁时,不管它在代码中的什么位置,如果另一个线程持有锁,它必须等待轮到它。当一个线程释放锁时,不管它在代码中的什么位置,另一个线程都可以获取该锁。
锁保护的是数据,而不是代码。它们通过确保所有访问受保护数据的代码在它持有锁时都能这样做,从而将其他线程排除在可能访问同一数据的任何代码之外。