3

我一直被告知要对多个线程将访问的变量加锁,我一直认为这是因为你想确保你正在使用的值在写回之前不会改变,即

mutex.lock()
int a = sharedVar
a = someComplexOperation(a)
sharedVar = a
mutex.unlock()

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

线程 A:

sharedVar = someFunction()

线程 B:

localVar = sharedVar

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

4

4 回答 4

5

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

即使不是这种情况,并且分配是原子的,如果没有内存屏障,您可能看不到最新值。

于 2012-12-13T21:08:53.367 回答
4

MSDN Magazine 对您在多线程代码中可能遇到的不同问题有很好的解释:

  • 忘记同步
  • 不正确的粒度
  • 读写撕裂
  • 无锁重新排序
  • 锁定车队
  • 两步舞
  • 优先级反转

您问题中的代码特别容易受到读/写撕裂的影响。但是您的代码既没有锁也没有内存屏障,也受到无锁重新排序(可能包括推测性写入,其中线程 B 读取线程 A 从未存储的值),其中副作用对第二个线程可见与它们在源代码中出现的顺序不同。

它继续描述了一些避免这些问题的已知设计模式:

  • 不变性
  • 纯度
  • 隔离

该文章可在此处获得

于 2012-12-13T21:17:51.483 回答
2

主要问题是赋值运算符(C++ 中的 operator=)并不总是保证是原子的(甚至对于原始的内置类型也不保证)。用简单的英语来说,这意味着分配可能需要一个以上的时钟周期才能完成。如果在此过程中线程被中断,则变量的当前值可能已损坏。

让我以您的示例为基础:

可以说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 开始运行时,ready 仍然为假。这可能意味着当线程 B 尝试将对象复制到局部变量中时,该对象仅被部分复制,或者处于某种中间的无效状态。

对于这方面的一个特别讨厌的例子,想想一个数据结构,其中一个被删除的节点被删除,然后在它被设置为 NULL 之前被中断。

(有关不需要锁的结构(又名,是原子的)的更多信息,这里是另一个问题,对此进行了更多讨论。)

于 2012-12-13T21:16:25.723 回答
0

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

线程 B:

localVar = sharedVar

线程 A:

sharedVar = someFunction()

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

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

于 2012-12-13T21:11:43.330 回答