【问题标题】:Why is threading dangerous?为什么线程很危险?
【发布时间】:2012-12-13 21:05:31
【问题描述】:

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

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

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

线程 A:

sharedVar = someFunction()

线程 B:

localVar = sharedVar

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

【问题讨论】:

  • 我不是权威,但我猜这是因为你最终可能会发生冲突。如果您在写入变量的同时尝试访问变量,那么您可能会遇到访问冲突。
  • @Pow-Ian 内存访问始终是可序列化的。硬件负责以合理的方式处理此问题。
  • 问题是变量可能包含您不期望的值。当您只是阅读时,这不是问题。当您开始编写和使用变量时,您无法再根据变量的值做出正确的决定。
  • @Jan Dvorak 谢谢,很高兴知道。
  • 你会想了解撕裂顺序一致性

标签: multithreading


【解决方案1】:

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

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

【讨论】:

  • 不只是类型。它还取决于语言(有些只有引用类型,其中赋值几乎是原子的)和内存模型(可以保证某些或所有类型的原子性)。
  • @delnan 是的 - 类型/语言/框架/底层硬件/等 - 都很重要。
  • 了解观察部分更新值的术语很有用:撕裂。在尝试哄骗硬件以原子方式执行更新时,对齐非常重要。
  • 真的有一种语言不能保证对引用类型变量的原子访问吗?
【解决方案2】:

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

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

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

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

  • 不变性
  • 纯度
  • 隔离

文章可here

【讨论】:

  • @BenVoigt 可以刷新文章的链接吗?
【解决方案3】:

主要问题是赋值运算符(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 仍然为 false。这可能意味着当线程 B 尝试将对象复制到局部变量中时,该对象仅被部分复制,或者处于某种中间的无效状态。

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

(有关不需要锁的结构(也称为原子结构)的更多信息,here 是另一个对此进行更多讨论的问题。)

【讨论】:

  • 即使对于简单类型(如浮点数),这也是一个问题吗? (如果 shared var 是浮点数)
  • 对于一些简单的类型,是的,这仍然是一个问题。 C++11 引入了 std::atomic 包装类,它将保证对许多基本类型的原子访问,但在大多数情况下,你不能总是假设看起来原子的东西会是原子的,除非编译器保证是这样的。 Here 是我发现的一篇文章,其中讨论了一些在 MSVC 下是原子或非原子的运算符,但主要的结论是基本整数数学通常是原子的,但其他事情可能不是。
  • Here 是另一个问题,似乎有更详细的关于浮点类型的信息。
【解决方案4】:

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

线程 B:

localVar = sharedVar

线程 A:

sharedVar = someFunction()

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-13
    • 2012-02-15
    • 2015-03-08
    • 2014-02-28
    • 2011-03-16
    • 1970-01-01
    相关资源
    最近更新 更多