0

在发布自己关于内存问题的解决方案后,nusi 建议我的解决方案缺少锁定

以下伪代码以非常简单的方式模糊地代表了我的解决方案。

std::map<int, MyType1> myMap;

void firstFunctionRunFromThread1()
{
    MyType1 mt1;
    mt1.Test = "Test 1";
    myMap[0] = mt1;
}

void onlyFunctionRunFromThread2()
{
    MyType1 &mt1 = myMap[0];
    std::cout << mt1.Test << endl; // Prints "Test 1"
    mt1.Test = "Test 2";
}

void secondFunctionFromThread1()
{
    MyType1 mt1 = myMap[0];
    std::cout << mt1.Test << endl; // Prints "Test 2"
}

我完全不确定如何实现锁定,我什至不确定我为什么要这样做(注意实际的解决方案要复杂得多)。有人可以解释一下在这种情况下我应该如何以及为什么要实施锁定?

4

6 回答 6

2

一个函数(即线程)修改映射,两个读取它。因此,读取可能会被写入中断,反之亦然,在这两种情况下,映射都可能被破坏。你需要锁。

于 2009-04-08T18:12:04.607 回答
2

实际上,问题甚至不仅仅是锁定...

如果您真的希望线程 2 始终打印“测试 1”,那么您需要一个条件变量。

原因是存在竞争条件。不管你是否在线程 2 之前创建线程 1,线程 2 的代码都可能在线程 1 之前执行,因此映射不会正确初始化。为了确保在映射初始化之前没有人从映射中读取,您需要使用线程 1 修改的条件变量。

正如其他人所提到的,您还应该对映射使用锁,因为您希望线程访问映射,就好像它们是唯一使用它的线程一样,并且映射需要处于一致状态。

这是一个概念性示例,可帮助您思考:

假设您有一个有 2 个线程正在访问的链表。在线程 1 中,您要求从列表中删除第一个元素(在列表的头部),在线程 2 中,您尝试读取列表的第二个元素。

假设delete方法是这样实现的:做一个临时ptr指向列表中的第二个元素,让head指向null,然后让head成为临时ptr...

如果发生以下事件序列怎么办: -T1 删除第二个元素的下一个指针 - T2 尝试读取第二个元素,但没有第二个元素,因为修改了头的下一个 ptr -T1 完成删除头并设置第二个元素作为头部

T2 读取失败,因为 T1 没有使用锁来使链表中的删除成为原子操作!

这是一个人为的例子,不一定是你将如何实现删除操作;但是,它说明了为什么需要锁定:对数据执行的操作是原子的。您不希望其他线程使用处于不一致状态的东西。

希望这可以帮助。

于 2009-04-08T18:32:57.970 回答
1

整个想法是防止程序由于多个线程访问相同的资源和/或更新/修改资源而进入不确定/不安全状态,以便后续状态变为未定义。阅读互斥锁和锁定(带有示例)。

于 2009-04-08T18:11:59.737 回答
1

通常,线程可能在不同的 CPU/内核上运行,具有不同的内存缓存。它们可能在同一个核心上运行,其中一个中断(“抢占”另一个)。这有两个后果:

1)你无法知道一个线程是否会在做某事的过程中被另一个线程中断。因此,在您的示例中,无法确保 thread1 在 thread2 写入字符串值之前不会尝试读取它,或者甚至当 thread1 读取它时,它处于“一致状态”。如果它不是处于一致的状态,那么使用它可能会做任何事情。

2)当您在一个线程中写入内存时,不知道在另一个线程中运行的代码是否或何时会看到这种变化。更改可能位于写入器线程的缓存中,并且不会刷新到主内存。它可能会被刷新到主内存,但不会进入读取器线程的缓存。部分更改可能会通过,而部分更改则不会。

一般来说,如果没有锁(或其他同步机制,如信号量),您无法确定线程 A 中发生的事情是否会发生在线程 B 中发生的事情“之前”或“之后”。您也无法说线程 A 中所做的更改是否或何时在线程 B 中“可见”。

正确使用锁定可确保所有更改都通过缓存刷新,以便代码以您认为应该看到的状态看到内存。它还允许您控制特定代码位是否可以同时运行和/或相互中断。

在这种情况下,查看上面的代码,您需要的最小锁定是有一个同步原语,它在写入字符串后由第二个线程(编写器)释放/发布,并由第一个线程获取/等待(读者)在使用该字符串之前。这将保证第一个线程看到第二个线程所做的任何更改。

这是假设在调用 firstFunctionRunFromThread1 之后才启动第二个线程。如果情况并非如此,那么您需要对 thread1 写入和 thread2 读取进行相同的处理。

实际执行此操作的最简单方法是拥有一个“保护”您的数据的互斥锁。您决定要保护哪些数据,并且任何读取或写入数据的代码在执行此操作时都必须持有互斥锁。所以首先你锁定,然后读取和/或写入数据,然后解锁。这确保了一致的状态,但它本身并不能确保线程 2 将有机会在线程 1 的两个不同函数之间做任何事情。

任何一种消息传递机制都会包含必要的内存屏障,所以如果你从写线程向读线程发送一条消息,意思是“我已经写完了,你现在可以阅读了”,那将是真的。

如果证明太慢,可以有更有效的方法来做某些事情。

于 2009-04-08T18:27:03.540 回答
0

作为编译代码的结果创建的指令集可以按任何顺序交错。这会产生不可预测和不希望的结果。例如,如果 thread1 在选择运行 thread2 之前运行,您的输出可能如下所示:

测试 1

测试 1

更糟糕的是,一个线程可能在分配过程中被抢占——如果分配不是原子操作的话。在这种情况下,让我们将atomic视为不能进一步拆分的最小工作单元。

为了创建一个逻辑原子指令集——即使它们在现实中产生多个机器代码指令——是使用互斥锁。Mutex 代表“互斥”,因为这正是它的作用。它确保对某些对象或代码的关键部分的独占访问。

处理多道程序的主要挑战之一是识别关键部分。在这种情况下,您有两个关键部分:分配给 myMap 的位置和更改 myMap[0] 的位置。由于您不想在写入 myMap 之前阅读它,因此这也是一个关键部分。

于 2009-04-08T18:38:53.153 回答
0

The simplest answer is: you have to lock whenever there is an access to some shared resources, which are not atomics. In your case myMap is shared resource, so you have to lock all reading and writing operations on it.

于 2009-04-09T09:48:54.323 回答