什么时候在C ++中使用pthreads时需要实现锁定?

时间:2009-04-08 18:06:27

标签: c++ locking pthreads

my solution发布到我自己关于内存问题的问题后nusi suggested that my solution lacks locking

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

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"
}

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

6 个答案:

答案 0 :(得分:2)

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

答案 1 :(得分:2)

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

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

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

你也应该像其他人提到的那样对地图使用锁,因为你希望线程访问地图,好像它们是唯一使用它的地图一样,并且地图需要处于一致的状态。

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

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

假设delete方法以下列方式实现:使一个临时ptr指向列表中的第二个元素,使头点为null,然后将头部作为临时ptr ......

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

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

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

希望这有帮助。

答案 2 :(得分:1)

整个想法是防止程序进入不确定/不安全状态,因为多个线程访问相同的资源和/或更新/修改资源,以便后续状态变得不确定。阅读MutexesLocking(附带示例)。

答案 3 :(得分:1)

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

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

2)当您在一个线程中写入内存时,无法确定在另一个线程中运行的代码是否或何时会看到该更改。更改可能位于编写器线程的缓存中,而不会刷新到主内存。它可能会刷新到主内存但不会进入读取器线程的缓存中。部分改变可能会通过,部分改变不是。

一般情况下,如果没有锁(或其他同步机制,如信号量),你无法说出线程A中发生的事情是否会发生在线程B中发生的“之前”或“之后”。你也没有说明是否或何时在线程A中进行的更改将在线程B中“可见”。

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

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

假设在调用firstFunctionRunFromThread1之后才开始第二个线程。如果情况可能不是这样,那么你需要与thread1写入和thread2读取相同的处理。

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

任何类型的消息传递机制也将包含必要的内存障碍,因此如果您从编写器线程向读取器线程发送消息,意味着“我已完成写入,您现在可以阅读”,那么是的。

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

答案 4 :(得分:0)

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

  

测试1

     

测试1

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

为了创建一个逻辑上原子的指令集 - 即使它们实际上产生了多个机器代码指令 - 是使用 lock mutex 。 Mutex代表“互斥”,因为它正是它所做的。它确保了对某些对象的独占访问权限或关键部分代码。

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

答案 5 :(得分:0)

最简单的答案是:只要有权访问某些不是原子的共享资源,就必须锁定。在您的情况下,myMap是共享资源,因此您必须锁定其上的所有读写操作。