Multithreading 为什么穿线很危险?

Multithreading 为什么穿线很危险?,multithreading,Multithreading,我一直被告知在多个线程将访问的变量周围放置锁,我一直认为这是因为您希望确保您正在使用的值在写回之前不会更改 i、 e 这是有道理的,你会锁定它。但在其他情况下,我不明白为什么我不能不使用互斥锁 线程A: sharedVar = someFunction() sharedVar = someFunction() 线程B: localVar = sharedVar localVar = sharedVar 在这种情况下可能出现什么问题?特别是如果我不在乎线程B读取线程A分配的任何特定值。这在

我一直被告知在多个线程将访问的变量周围放置锁,我一直认为这是因为您希望确保您正在使用的值在写回之前不会更改 i、 e

这是有道理的,你会锁定它。但在其他情况下,我不明白为什么我不能不使用互斥锁

线程A:

sharedVar = someFunction()
sharedVar = someFunction()
线程B:

localVar = sharedVar
localVar = sharedVar

在这种情况下可能出现什么问题?特别是如果我不在乎线程B读取线程A分配的任何特定值。

这在很大程度上取决于
sharedVar的类型、您使用的语言、任何框架和平台。在许多情况下,将单个值分配给
sharedVar
可能需要多条指令,在这种情况下,您可以读取该值的“半集”副本


即使情况并非如此,并且赋值是原子的,如果没有一个适当的值,您也可能看不到最新的值。

这可能会出错,因为线程调度程序可以挂起和恢复线程,因此您无法确定这些指令的执行顺序。也可以按以下顺序进行:

线程B:

localVar = sharedVar
localVar = sharedVar
线程A:

sharedVar = someFunction()
sharedVar = someFunction()
在这种情况下,
localvar
将为null或0(或不安全语言中完全意外的值),这可能不是您想要的


顺便说一句,互斥锁实际上不会解决这个问题。您提供的示例不适合并行化。

主要问题是赋值运算符(运算符=在C++中)并不总是保证是原子的(即使对于基本的内置类型也是如此)。简单地说,这意味着分配可能需要一个以上的时钟周期才能完成。如果在中间,线程被中断,那么变量的当前值可能被损坏。 让我以你的例子为基础:

假设
sharedVar
是一个定义了
operator=
的对象:

object& operator=(const object& other) {
    ready = false;
    doStuff(other);
    if (other.value == true) {
        value = true;
        doOtherStuff();
    } else {
        value = false;
    }
    ready = true;
    return *this;
}
如果你的例子中的线程A在这个函数的中间被中断,那么当线程B开始运行时,准备就绪仍然是错误的。这可能意味着当线程B试图将对象复制到局部变量中时,该对象仅被部分复制,或者处于某种中间的无效状态

对于这方面的一个特别糟糕的例子,请考虑一个数据结构,其中删除的节点被删除,然后在设置为NULL之前被中断


(有关不需要锁的结构(也称为原子结构)的更多信息,请参阅另一个问题。

MSDN杂志对多线程代码中可能遇到的不同问题进行了很好的解释:

  • 遗忘同步
  • 粒度不正确
  • 读写撕裂
  • 无锁重排序
  • 锁定车队
  • 两步舞
  • 优先级反转
问题中的代码特别容易被读/写撕裂。但是,您的代码既没有锁也没有内存障碍,也会受到无锁重新排序的影响(这可能包括推测性写入,其中线程B读取线程a从未存储的值),在这种情况下,副作用对第二个线程可见的顺序与它们在源代码中出现的顺序不同

接着描述了一些避免这些问题的已知设计模式:

  • 不变性
  • 纯洁
  • 隔离

这篇文章是可用的

我不是权威,但我猜这是因为你最终可能会遇到冲突。如果您试图在写入变量的同时访问该变量,则可能会遇到访问冲突。@Pow-Ian内存访问始终是可序列化的。硬件负责以合理的方式处理此问题。问题是变量可能包含您不期望的值。当你只是阅读时,这不是问题。当您开始编写和使用变量时,您将无法再根据变量的值做出正确的决策。@Jan Dvorak,谢谢,这很好。您需要了解撕裂和顺序一致性。这不仅仅是类型。它还取决于语言(有些只有引用类型,其中赋值几乎是原子的)和内存模型(可能保证某些或所有类型的原子性)。@delnan Yes-type/language/framework/Underground hardware/etc-这一切都很重要。了解观察部分更新值的术语很有用:撕裂。当试图哄骗硬件执行更新时,对齐是非常重要的。是否有一种语言不能保证对引用类型变量的原子访问?@ WayruSeLIPEP,我相信C++中的“浮点”依赖于平台/编译器;我相信,在一些(大部分是较旧的)嵌入式系统上,浮点赋值不是原子的。即使是在简单类型(如浮点)的情况下,这也是一个问题吗?(如果共享var是一个float)对于一些简单的类型,是的,这仍然是一个问题。C++11引入了std::atomic包装类,该类将提供对许多基本类型的保证原子访问,但在大多数情况下,您不能总是假设看起来原子的东西是原子的,除非编译器保证是原子的。我发现有一篇文章讨论了一些在MSVC下是或不是原子的运算符,但主要的收获是基本整数数学通常是原子的,但其他东西可能不是。还有一个问题似乎有更详细的关于浮点类型的信息。@BenVoigt是否可以刷新文章的链接?