代码之家  ›  专栏  ›  技术社区  ›  Nick Bolton

当在C++中使用p螺纹时,何时需要实现锁定?

  •  0
  • Nick Bolton  · 技术社区  · 16 年前

    张贴后 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 回复  |  直到 16 年前
        1
  •  1
  •   Steve Jessop    16 年前

    一般来说,线程可能在不同的CPU/内核上运行,具有不同的内存缓存。它们可能在同一个内核上运行,其中一个中断(“先发制人”另一个)。这有两个后果:

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

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

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

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

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

    假设在调用firstFunctionRunFromThread1之后才启动第二个线程。如果情况并非如此,那么您需要同样处理线程1写入和线程2读取。

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

    任何类型的消息传递机制都会包含必要的内存屏障,因此,如果您从编写线程向读线程发送消息,意思是“我已经完成了写,您现在可以阅读”,那么这是正确的。

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

        2
  •  2
  •   anon    16 年前

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

        3
  •  2
  •   Tom    16 年前

    事实上,不仅仅是锁定问题…

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

    原因是有一个种族条件。无论您是否在线程2之前创建线程1,线程2的代码都有可能在线程1之前执行,因此映射不会正确初始化。为了确保在初始化映射之前没有人读取它,您需要使用线程1修改的条件变量。

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

    下面是一个概念性的例子来帮助您思考它:

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

    假设delete方法是通过以下方式实现的:使一个临时ptr指向列表中的第二个元素,使head指向空值,然后使head成为临时ptr…

    如果发生以下事件序列会怎样? -T1删除第二个元素的ptr旁边的heads -t2尝试读取第二个元素,但没有第二个元素,因为头部的下一个ptr已被修改。 -T1完成移除头部并将第二个元件设置为头部

    t2的读取失败,因为t1没有使用锁使从链接列表中删除的内容成为原子!

    这是一个做作的示例,不一定是如何实现删除操作;但是,它说明了为什么需要锁定:必须这样才能对数据执行原子操作。您不希望其他线程使用不一致的状态。

    希望这有帮助。

        4
  •  1
  •   dirkgently    16 年前

    整个想法是防止程序由于多个线程访问同一个资源和/或更新/修改资源而进入不确定/不安全状态,从而使后续状态变为未定义状态。读起来 Mutexes Locking (举例说明)。

        5
  •  0
  •   FreeMemory    16 年前

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

    试验1

    试验1

    更糟糕的是,如果分配不是一个 原子的 操作。在这种情况下,让我们想想 原子的 作为最小的工作单位,不能进一步拆分。

    为了创建逻辑上原子化的指令集——即使它们实际上产生了多个机器代码指令——就是使用 互斥 . mutex代表“互斥”,因为它就是这样做的。它确保对某些对象的独占访问,或者 关键部分 代码的。

    处理多道程序设计的主要挑战之一是确定 关键部分。 在本例中,有两个关键部分:分配给mymap的位置和更改mymap的位置[0]。既然你不想 阅读 我的地图在写之前,就是 关键部分。

        6
  •  0
  •   oo_olo_oo    16 年前

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