同时覆盖具有相同值的变量是不是安全?

Posted

技术标签:

【中文标题】同时覆盖具有相同值的变量是不是安全?【英文标题】:Is concurrently overwriting a variable with the same value safe?同时覆盖具有相同值的变量是否安全? 【发布时间】:2010-08-17 13:39:55 【问题描述】:

我有以下情况(代码有缺陷引起的):

有一个原始类型的共享变量(假设为int),它在程序启动期间从严格的一个线程初始化为值N(假设为0)。然后(严格在变量初始化之后)在程序运行期间启动各种线程,它们以某种随机顺序读取该变量或用相同的值N覆盖它 em>(本例中为0)。访问变量没有同步。

这种情况会导致程序出现意外行为吗?

【问题讨论】:

【参考方案1】:

根据标准,这非常不可能,但并非不可能。

没有说明整数的底层表示是什么,标准也没有说明值的加载方式。

无论多么奇怪,我可以设想一个实现,其中 0 的底层位模式是 10101010 并且该架构仅支持通过将数据移位八个周期将数据加载到内存中,但在一个周期内将其作为单个单元读取.

如果在移入位模式时另一个线程读取该值(例如,000000010000001000000101 等),您将遇到问题。

任何人设计出如此奇异的架构的可能性几乎为零,可以忽略不计。但是,不幸的是,它不是零。我想表达的是,在标准合规方面,您根本不应该依赖假设。

请在您投票反对我之前,随时引用标准中声明这是不可能的部分:-)

【讨论】:

许多人认为将已经存在的内容写入内存不会产生任何影响。对于“普通”RAM,这几乎是正确的,但某些类型的字节可升级闪存在写入后的一段时间内无法读取。我不认为我见过一种既具有写入速度慢又具有无限耐力的内存,但这是可以想象的。更值得注意的是,写入另一个线程正在读取的位置可能会导致缓存冲突。这些通常不会导致错误执行,但可能会导致延迟。 很难想象一个计算机系统会允许一个处理器从一个内存位置读取一个奇怪的中间值,而另一个处理器正在更新它。我所知道的所有系统(当然不是很多)都在硬件级别执行基本同步。如果一个处理器写入一个位置,而另一个处理器从该位置读取,则第二个处理器将在更新之前或之后看到该值,但看不到两者之间的值。 @Peter,情况并非总是如此。 GCC 必须为原子访问提供特殊的内在函数,因为假设它是不安全的。我似乎还记得 x86 有一个 LOCK 前缀来尝试同步多 CPU,但它不是在硬件级别自动完成的。但这一切与我们无关。什么是常见的做法,什么是根据标准可能的,并不总是一致的。 我同意你关于标准的观点。但我认为值得记住的是,无论标准如何规定,程序都必须在物理硬件上运行,而硬件将决定其工作方式。我还要指出,联锁内在函数 all 处理组合的读/写操作。如果简单的原子读取和原子写入需要特殊指令,那么我不得不想知道为什么相应的内在函数不存在。【参考方案2】:

由于 C++ 目前没有标准的并发模型,它完全取决于您的线程实现以及它提供的任何保证。然而,在一般情况下,这几乎肯定是不安全的,因为可能会导致读取被破坏。在某些特定情况下,它可能会“起作用”或至少“看起来起作用”。

在 C++0x(它有一个标准的并发模型)中,您的场景将正式导致未定义的行为。 C++0x Final Committee Draft §1.10 中有一个冗长、详细、难以阅读的并发模型规范,但基本上可以归结为:

如果其中一个修改内存位置而另一个访问或修改同一内存位置(第 1.10/3 节),则两个表达式计算会发生冲突。

如果程序的执行包含不同线程中的两个冲突操作,则该程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生。任何此类数据竞争都会导致未定义的行为 (§1.10/14)。

您的表达式计算显然存在冲突,因为它们修改和读取相同的内存位置,并且由于对象不是原子的并且访问未使用锁同步,因此您的行为未定义。

【讨论】:

除了因为每个线程都在写入完全相同的值,因此没有可能发生撕裂读取。这实际上是一个空操作。 @Peter:无法保证 如何 执行写入。正如 paxdiablo 在他的回答中正确解释的那样,写入内存位置可能涉及更改其值,只要在写入完成后它具有正确的值。 我想这是真的,但对我来说这似乎有点学术性。您是否知道任何有这种行为方式的系统? 是的,快闪。写入通常被重新映射。因此,如果您有一个值 17 并用值 17 覆盖它,闪存控制器将更改映射并将 17 写入先前归零的块。竞速线程可能会看到中间的归零块。【参考方案3】:

没有。当然,如果其中一个线程稍后尝试更改该值,您可能会以数据竞争告终。您最终还会遇到一点缓存争用,但我怀疑这会产生明显的影响。

【讨论】:

【参考方案4】:

你不能真正依赖它。对于原始类型,你应该没问题,如果操作是原子的(例如,在大多数平台上正确对齐的 int),那么写入和读取不同的值是安全的(注意我的意思是像 "x = 5;",not "x += 5;" 这不是原子的,也不是线程安全的)。

对于非原始类型,即使它的值相同,所有的赌注都是关闭的,因为可能有一个复制构造函数会做一些不安全的事情(比如分配内存)。

【讨论】:

【参考方案5】:

是的,在这种情况下可能会发生意外行为。考虑变量的初始值不为 0 的情况。一个线程可能会从 0 开始设置,而另一个线程只看到设置了部分字节的变量。

对于int 类型,这是不太可能的,因为大多数处理器都会对字长值进行原子分配。但是,一旦您遇到 8 位数值(在某些平台上为 long)或大型结构,这就会成为问题。

【讨论】:

他保证该值在任何人试图读取或覆盖之前由一个线程初始化。 我的问题不是很清楚,所以我更新了它。 线程开始并发工作之前,该变量不可能被可靠地设置为该值。在您描述的情况下,很明显存在竞争风险。【参考方案6】:

如果没有其他线程(包括主线程)可以在这些线程初始化时将 0 的值更改为其他任何值(比如说1),那么您将不会遇到问题。但是,如果任何其他线程有可能在启动阶段更改该值,则您可能会遇到问题。您正在玩一个危险的游戏,我建议您在读取值之前锁定。

【讨论】:

以上是关于同时覆盖具有相同值的变量是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

为啥浮点字典键可以覆盖具有相同值的整数键?

如何保存更新后与以前具有相同值的变量?

覆盖应用程序类并通过静态变量访问它是不是安全?

bugku 变量覆盖

使用相同的请求标识符同时注册 2 个本地通知不被覆盖

为啥具有 z-index 值的元素不能覆盖其子元素?