原子操作会阻塞其他线程吗?

Will atomic operations block other threads?

我正在努力让 "atomic vs non atomic" 的概念在我的脑海中安定下来。我的第一个问题是我找不到 "real-life analogy"。就像 customer/restaurant 原子操作或类似的关系。

我还想了解原子操作如何将自己置于线程安全编程中。

在此博客中post; http://preshing.com/20130618/atomic-vs-non-atomic-operations/ 它被称为:

An operation acting on shared memory is atomic if it completes in a single step relative to other threads. When an atomic store is performed on a shared variable, no other thread can observe the modification half-complete. When an atomic load is performed on a shared variable, it reads the entire value as it appeared at a single moment in time. Non-atomic loads and stores do not make those guarantees.

"no other thread can observe the modification half-complete"是什么意思?

这意味着线程将等待直到原子操作完成?该线程如何知道该操作是原子的?例如在 .NET 中,我可以理解,如果锁定对象,则可以设置一个标志来阻止其他线程。但是原子呢?其他线程如何知道原子操作和非原子操作之间的区别?

另外,如果上面的说法是正确的,那么所有的原子操作都是线程安全的吗?

您描述的原子操作是处理器内的指令,硬件将确保在原子写入完成之前不会对内存位置进行读取。这保证了线程要么读取 before write 的值,要么读取 after write 操作的值,但中间没有任何东西 - 没有机会读取值的一半字节来自写入前,另一半来自写入后。

针对处理器的代码 运行 甚至不知道这个块,但这与使用 lock 语句确保更复杂的操作(由许多低-级指令)是原子的。

单个原子操作始终是线程安全的 - 硬件保证操作的效果是原子的 - 它永远不会在中间中断。

在绝大多数情况下,一组原子操作不是原子的(我不是专家,所以我不想做出明确的陈述,但我想不出这样的情况different) - 这就是复杂操作需要锁定的原因:整个操作可能由多个原子指令组成,但整个操作仍可能在这两个指令中的任何一个之间中断,从而导致另一个线程看到一半的可能性烤的结果。锁定确保在其他操作完成(可能通过多个线程切换)之前,对共享数据进行操作的代码无法访问该数据。

this question / answer 中显示了一些示例,但您可以通过搜索找到更多示例。

Being "atomic" 是一个属性,适用于由实现(通常是硬件或编译器)强制执行的操作。对于 real-life 类比,请查看需要交易的系统,例如银行账户。从一个账户到另一个账户的转账涉及从一个账户提款和向另一个账户存款,但通常这些应该自动执行 - 没有时间钱已经被提取但还没有存入,反之亦然。

所以,继续类比你的问题:

What is the meaning of "no other thread can observe the modification half-complete"?

这意味着没有线程可以观察到两个账户处于一个账户提款但没有存入另一个账户的状态。

在机器术语中,这意味着在一个线程中对值的原子读取将看不到具有来自另一个线程的原子写入之前的某些位的值,以及来自同一写入操作之后的某些位的值。比单个读取或写入更复杂的各种操作也可以是原子的:例如,"compare and swap" 是一种通常实现的原子操作,它检查变量的值,将其与第二个值进行比较,并将其替换为另一个如果比较的值原子地相等,则为值 - 例如,如果比较成功,则另一个线程不可能在操作的比较和交换部分之间写入不同的值。另一个线程的任何写入都将完全在原子 compare-and-swap.

之前或之后执行

您的问题的标题是:

Will atomic operations block other threads?

在"block"通常的意思中,答案是否定的;一个线程中的原子操作本身不会导致另一个线程中的执行停止,尽管它可能导致活锁情况或以其他方式阻止进度。

That means thread will wait until atomic operation is done?

从概念上讲,这意味着他们永远不需要等待。操作要么完成,要么不完成;它永远不会完成一半。在实践中,原子操作可以使用互斥量来实现,但性能成本很高。许多(如果不是大多数)现代处理器在硬件级别支持各种原子原语。

Also if above statement is true, do all atomic operations are thread-safe?

如果你组合原子操作,它们就不再是原子的。也就是说,我可以先执行一个原子 compare-and-swap 操作,然后执行另一个原子操作,这两个 compare-and-swap 将分别是原子的,但它们是可整除的。因此,您仍然会遇到并发错误。

让我们澄清一下什么是原子,什么是块。原子性意味着操作要么完全执行并且它的所有副作用都是可见的,要么根本不执行。所以所有其他线程都可以看到操作之前或之后的状态。由互斥量保护的代码块也是原子的,我们只是不称它为操作。原子操作是特殊的 CPU 指令,在概念上类似于由互斥锁保护的通常操作(你知道互斥锁是什么,所以我会使用它,尽管它是使用原子操作实现的)。 CPU 有一组有限的操作可以自动执行,但由于硬件支持,它们非常快。

当我们讨论线程块时,我们通常会在对话中涉及互斥量,因为由它们保护的代码可能需要很长时间才能执行。所以我们说线程等待互斥锁。对于原子操作,情况是一样的,但是它们很快,我们通常不关心这里的延迟,所以不太可能听到单词 "block" 和 "atomic operation" 在一起。

That means thread will wait until atomic operation is done?

是的,它会等待。 CPU 将限制访问变量所在的内存块,其他 CPU 核心将等待。请注意,出于性能原因,块仅在原子操作本身之间保留。 CPU 允许内核缓存变量以供读取。

How that thread know about that operation is atomic?

使用了特殊的 CPU 指令。它只是在你的程序中写明特定​​操作应该以原子方式执行。

附加信息:

原子操作有更多棘手的部分。例如,在现代 CPUs 上,通常所有原始类型的读写都是原子的。但是 CPU 和编译器可以对它们重新排序。所以有可能你改变了一些结构,设置了一个标志,告诉它被改变了,但是 CPU 在结构实际提交到内存之前重新排序写入并设置标志。当您使用原子操作时,通常会做一些额外的工作来防止不希望的重新排序。如果你想了解更多,你应该阅读内存障碍。

简单的原子存储和写入不是那么有用。为了最大限度地利用原子操作,您需要更复杂的东西。最常见的是 CAS——比较和交换。您将变量与值进行比较,并仅在比较成功时才更改它。

在典型的现代 CPU 上,原子操作是这样实现的:

当发出访问内存的指令时,内核的逻辑会尝试将内核的高速缓存置于正确的状态以访问该内存。通常,此状态将在必须发生内存访问之前实现,因此没有延迟。

当另一个核心正在对一块内存执行原子操作时,它将那块内存锁定在自己的缓存中。这可以防止任何其他核心在原子操作完成之前获得访问该内存的权利。

除非两个内核碰巧正在执行对许多相同内存区域的访问,并且其中许多访问都是写入,否则这通常根本不会涉及任何延迟。那是因为原子操作非常快,而且通常内核会提前知道它需要访问哪些内存。

因此,假设一块内存上次在核心 1 上被访问,现在核心 2 想要进行原子增量。当内核的预取逻辑在指令流中看到对该内存的修改时,它将指示缓存获取该内存。缓存将使用核心间总线从核心 1 的缓存中获取该内存区域的所有权,并将该区域锁定在自己的缓存中。

此时,如果另一个内核试图读取或修改该内存区域,它将无法在其缓存中获取该区域,直到锁被释放。这种通信发生在连接缓存的总线上,具体发生的位置取决于内存所在的缓存。(如果根本不在缓存中,则必须转到主内存。)

缓存锁通常不会被描述为阻塞线程,因为它是如此之快,而且因为内核在尝试获取锁定在另一个缓存中的内存区域时通常能够做其他事情。从更高层代码的角度来看,原子的实现通常被认为是一个实现细节。

所有原子操作都保证不会看到中间结果。这就是使它们成为原子的原因。

原子操作是指系统完整地执行一个操作或根本不执行。读取或写入 int64 是原子的(64 位系统和 64 位 CLR),因为系统 read/write 单个操作中的 8 个字节,读者看不到存储的新值的一半和旧值的一半。但要小心 :

long n = 0; // writing 'n' is atomic, 64bits OS & 64bits CLR
long m = n; // reading 'n' is atomic
....// some code
long o = n++; // is not atomic : n = n + 1 is doing a read then a write in 2 separate operations

要使 n++ 发生原子性,您可以使用互锁 API :

long o = Interlocked.Increment(ref n); // other threads are blocked while the atomic operation is running