【问题标题】:Relationship of "Threadsafe" with "Atomic Operation"“线程安全”与“原子操作”的关系
【发布时间】:2014-05-04 02:20:46
【问题描述】:

从这里的 MSDN (http://msdn.microsoft.com/en-us/library/aa691278(v=vs.71).aspx) 你可以看到 int,byte……等基本类型都是可读/可写原子的。所以我想知道它们都是“原子的”:

“原子操作”和“锁”有什么关系?在我看来,如果一个操作是“原子的”,我们就不再需要锁了,因为它们必须是“线程安全的”,对吗?

无论如何,网站论坛上有人告诉我,他们是对的吗?

1) 任何引用类型都是“原子操作”。

2) i++ 或 --i 等类型都不是原子操作(那么如何检查哪个操作是“原子”,哪个不是?)

我总是对这些问题感到困惑......


@Daniel Brückner 和@keshlam:

似乎像 ++i(先读/然后写)这样的混合操作不是原子操作。但是,如果我将它们分成如下两个步骤,我会对此感兴趣:

//suppose I have "i" defined and assigned a value
void Fun()
{
   if(i==1)
   {
     i=2;
   }
}

如果上面的代码会被多个线程调用,并且“if”部分只读取(原子步骤)和“i=2”(另一个原子步骤),那么我不需要锁?

【问题讨论】:

  • 这句话:“任何引用类型都是'原子操作'”没有意义。类型不是操作。
  • 当一个类型大于本机寄存器大小时,“不保证原子读-修改-写,例如在递增或递减的情况下。”
  • @MitchWheat:“类型大于本地寄存器大小”是什么意思?

标签: c# multithreading thread-safety


【解决方案1】:

原子操作本质上是不可中断的——只有在整个操作之前或之后,其他人无法看到系统处于不一致状态。

原始值的单次读取或写入对于该核心来说是原子的。如果在其他处理器内核中运行代码可能不安全;在操作系统显式执行该操作之前,值通常不会从处理器缓存中刷新并且对其他内核可见。我对 C# 线程的了解还不够,无法知道其中存在多少风险。

我们更经常关心的读-修改-写通常不是 -- 包括 ++ 和 -- 操作。在某些体系结构中,有单个指令实现了固有的原子测试和设置,这是读取-修改-写入的特殊情况,可用于实现非常轻量级的线程安全锁定/计数形式......但是这在很大程度上取决于您在哪个处理器上运行。

涉及读取和/或更改多个原始值的操作,或者对值执行非平凡操作的操作,无论您如何对其进行切片,都不会是原子的。并且在实现原始信号量级别之上的大多数实际任务确实涉及多个必须保持彼此同步的值。因此,虽然知道某些操作是原子的对于最简单的情况是有帮助的,但是一旦超过了最低级别的子例程,您会发现这还不够。此时,您需要使用显式锁来防止其他线程中断聚合操作。信号量/锁基本上使用原始原子作为更复杂操作的围栏,利用操作系统功能使第二个(或以后)线程尝试访问锁等待它非常 (基本上,在释放锁之前进入睡眠状态,而不是进入自旋循环)。这使您可以创建更大的操作以原子方式运行,尽管它们必须相互合作才能实现。

总结:如果您不知道某些东西本质上是原子的,请始终假设它不是,并且如果要从多个线程访问它,则必须通过以下方式对其进行保护一把锁。不这样做会导致乱码值和极其烦人的调试场景。不要冒险;如果有疑问,请明确保护它。

(并且不要忘记拥有原子结构并不能确保其内容是原子的。去年我不得不调试一个案例,有人使用 Java 的一种原子集合类型但忘记了它们的结构存储到集合中也必须保护自己的内容。)

【讨论】:

  • 非常感谢!根据你所说的,似乎在 MSDN 上,你的意思是只有一个变量类型,比如上面所说的 (int, byte……) 在单线程模式下是线程安全和原子的,可以从中读取值或写入一个价值?那么微软为什么要让它们“原子化”呢?多线程环境下不能正常工作就没用了……我还是不明白MSDN为什么这么说?
【解决方案2】:

数据类型不是原子的,对它们的一些操作是或可以是原子的。分配一个 32 位整数变量

Int32 foo = 123456789;

是,例如,一个原子操作。永远不会发生另一个线程观察到foo 为 52501,即分配了最低有效 16 位但尚未分配最高有效 16 位。 Int64 的情况并非如此 - 可能会发生另一个线程看到仅部分执行的赋值操作。

许多其他操作不是原子的。例如

foo++;

需要读取、修改和写入值,因此另一个线程可能会在您读取 foo 之后但在您能够将更新后的值写回之前读取和更改它的值。您可以使用Interlocked.Increment() 以原子方式执行此操作,并且还有一些其他操作的方法。

所以本质上,你所谓的原子数据类型只保证变量赋值是原子地执行的,这对于除DoubleDecimalInt64UInt64之外的原始数据类型和引用是正确的。

void Fun()
{
  if (i == 1)
  {
     i = 2; // I may have any value here because another thread might
            // have assigned a new value after the test i == 1 but
            // before this assignment.
  }
}

【讨论】:

  • 非常感谢!我可以换一个有趣的问题:如果我写了一个可以被多个线程调用的方法,它首先读取一个带有“if”检查的值,然后当它满足在特定条件下,它将直接将新值写入变量本身。那么我们不需要锁,因为每个步骤都是原子的吗?
  • 这取决于你想做什么。如果没有锁,您可能会读取1,另一个线程写入2,然后在您的if 块内,您用3 覆盖2,您的线程永远不会注意到1 已更改为2.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-02
  • 1970-01-01
  • 2014-02-01
相关资源
最近更新 更多