从不可变对象设置副本是不是可以避免线程损坏? [关闭]
Posted
技术标签:
【中文标题】从不可变对象设置副本是不是可以避免线程损坏? [关闭]【英文标题】:Is setting of copies from immutables safe from thread corruption? [closed]从不可变对象设置副本是否可以避免线程损坏? [关闭] 【发布时间】:2014-01-10 13:32:32 【问题描述】:这是一个类字段,
class someClass
Int someClassField = nil
...
(请,请(!)忽略可见性问题,这个问题与整体设计有关,而不是语言实现)如果我在网上看教程,我被告知这个字段可以安全地被多个线程使用。当教程说安全时,它们并不意味着一个线程不能干扰另一个线程可见的值。这种干扰可能是本意——场可能是反击。教程的意思是,当一个线程更改此字段时,该字段不会处于不安全状态。拿下这个领域,
class someClass
List<List> someClassField = new List<Int>()
...
据我了解,如果该字段是一个简单列表,则一个线程可能会使其处于不一致状态(即部分断开连接)。如果另一个线程使用该列表,它将失败——在像 C 这样的语言中,这将是一场灾难。连阅读都可能失败。
那么,可以要求在该领域使用的类复制它的状态(复制可以扩展到对不变性的全面辩护,但我保持讨论简单)。如果该类复制了它的状态,那么修改将在字段上的副本之外完成,在修改后的新副本中进行返回。这个新的、修改后的副本可以重新分配给该字段。但是这个赋值是线程安全的——从某种意义上说,字段的值不能处于不一致的状态——因为新对象对字段的引用分配是原子的?
我忽略了语言引擎是否可能重新排序、缓存等所有问题。请参阅下面的许多帖子(尤其是 Java,似乎),
c# question有提示 Rule of thumb answers 在 Scala 中,但似乎将线性同步与彻底的灾难混淆了? Dark information 关于 Java 的线程可见性问题。一篇文章表明,是的,参考写作是原子的 Java question 与此相关。可见性和未成形对象之间存在更多相同的 Java 混淆 immutable-objects-are-thread-safe-but-whyJava 问题。听起来是个正确的问题,但是什么样的线程安全? .net question 偏离轨道我想在较小的范围内解决这个问题...
【问题讨论】:
如果不参考特定语言,我认为这个问题没有多大意义。 似乎达成了一致——从概念上讲,是的,但语言实现会存在易变性问题或无法保证原子性。感谢大家尝试对“未完成”的事情进行推理。 这个问题已被标记为“太宽泛”。我不知道这个标记。 我标记了几种语言 - 这就是原因,它不具体吗?我是否询问过设计模式的细节,例如访客,(也是跨语言的),这个问题会被标记吗?我对正在考虑的行动非常具体。我想我没能说“忽略可见性问题”。嗯嗯,收集到的信息不错,相信这里有一个重点。 任何对这个问题(或它的背景)感兴趣的人都可能想看看, 【参考方案1】:在大多数语言中,对象分配是原子的。
在这种特定情况下,您需要小心,尽管在执行 x=new X()
时,不能保证在所有语言中 X 在分配之前完全初始化。我不确定 C# 的立场。
您还必须考虑可见性和原子性。例如,在 Java 中,您需要将变量设置为 volatile,否则在一个线程中所做的更改可能在另一个线程中根本不可见。
【讨论】:
【参考方案2】:C++ 将数据竞争定义为两个或多个线程可能同时访问同一内存位置,其中至少一个是修改。具有数据竞争的程序的行为是未定义的。所以不,如果至少有一个线程可以修改它,那么多个线程访问该字段是不安全的。
【讨论】:
【参考方案3】:在 Java 中编写引用是原子性的(仅当字段是 volatile 时才写入 long 或 double),但仅此一点对您没有任何帮助。
示例演示:
class Foo
int x;
public Foo() x = 5;
现在假设我们做了一个赋值,例如foo = new Foo()
(没有 foo 的 final 或 volatile 修饰符!)。从低层次的角度来看,这意味着我们必须做到以下几点:
-
分配内存
运行构造函数
为该字段分配内存地址。
但只要构造函数不读取我们分配给它的字段,编译器也可以执行以下操作:
-
分配内存
为该字段分配内存地址。
运行构造函数
线程安全?当然不是(如果你不设置内存屏障,你永远不能保证真正看到更新)。当涉及 final 字段时,Java 提供了更多保证,因此创建一个新的不可变对象将是线程安全的(您永远不会看到 final 字段的未初始化值)。可变字段(我们在这里讨论的是赋值而不是对象中的字段)在 java 和 c# 中也避免了这个问题。不确定 C# 和 readonly。
【讨论】:
【参考方案4】:在 Java 中,除了 64 位基本类型 long
和 double
之外,对引用和原语的赋值都是原子的。对 Java long
s 和 double
s 的赋值可以通过使用 volatile
修饰符声明它们来实现原子化。见:Are 64 bit assignments in Java atomic on a 32 bit machine?
之所以如此,是因为 Java VM 规范要求它才能使 VM 与 Java 兼容。
Scala 在标准 Java VM 之上运行,因此在分配方面也将提供与 Java 相同的保证,除非它们开始使用 JNI。
C/C++ 的一个问题(以及它的优势之一)是这两种语言都允许将数据结构非常精细地映射到内存地址。在这个级别上,对内存的写入是否是原子的在很大程度上取决于硬件平台。例如,CPU 通常无法自动读取,更不用说写入未正确对齐的变量。例如当 16 位变量未与偶数地址对齐时,或者当 32 位变量未与 4 的倍数地址对齐时,依此类推。当变量超出一个缓存行进入下一个缓存行,或者超出一个页面进入下一个时,情况会变得更糟。因此 C 不保证赋值是原子的。
【讨论】:
感谢您对 C 的注释。我希望可能有一些跨语言的贡献。以上是关于从不可变对象设置副本是不是可以避免线程损坏? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章