9

我试图让“原子与非原子”的概念在我脑海中浮现。我的第一个问题是我找不到“现实生活中的类比”。就像客户/餐厅关系通过原子操作或类似的东西。

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

在这篇博文中;http://preshing.com/20130618/atomic-vs-non-atomic-operations/ 它被称为:

一个作用于共享内存的操作是原子的,如果它相对于其他线程在一个步骤中完成。当对共享变量执行原子存储时,没有其他线程可以观察到修改 half-complete。当对共享变量执行原子加载时,它会读取在某个时刻出现的整个值。非原子加载和存储不做这些保证。

“没有其他线程可以观察到修改半完成”是什么意思?

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

此外,如果上述陈述为真,那么所有原子操作都是线程安全的吗?

4

5 回答 5

18

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

当我们讨论线程块时,我们通常会在对话中涉及互斥锁,因为受它们保护的代码可能需要相当长的时间才能执行。所以我们说线程在互斥体上等待。对于原子操作的情况也是一样,但是它们很快,我们通常不关心这里的延迟,所以不太可能同时听到“阻塞”和“原子操作”这两个词。

这意味着线程将等到原子操作完成?

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

该线程如何知道该操作是原子的?

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

附加信息:

原子操作有更多棘手的部分。例如,在现代 CPU 上,通常所有基本类型的读取和写入都是原子的。但是允许 CPU 和编译器重新排序它们。因此,您可能会更改某些结构,设置一个标志来告知它已更改,但 CPU 会在结构实际提交到内存之前重新排序写入并设置标志。当您使用原子操作时,通常会做一些额外的工作来防止不希望的重新排序。如果您想了解更多信息,您应该阅读有关内存屏障的内容。

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

于 2016-09-30T16:36:36.970 回答
8

在典型的现代 CPU 上,原子操作以这种方式成为原子操作:

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

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

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

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

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

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

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

于 2016-09-30T16:59:36.293 回答
1

您描述的原子操作是处理器内的指令,硬件将确保在原子写入完成之前不会在内存位置发生读取。这保证了线程要么读取写入之前的值,要么读取写入操作之后的值,但中间没有任何内容 - 没有机会从写入之前读取值的一半字节,而在写入之后读取另一半字节。

在处理器上运行的代码甚至不知道这个块,但这实际上与使用lock语句来确保更复杂的操作(由许多低级指令组成)是原子的没有什么不同。

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

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

此问题/答案中显示了一些示例,但您可以通过搜索找到更多示例。

于 2016-09-30T15:52:27.260 回答
1

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

因此,继续为您的问题进行类比:

“没有其他线程可以观察到修改半完成”是什么意思?

这意味着在从一个账户提款但尚未存入另一个账户的状态下,没有线程可以观察到这两个账户。

用机器术语来说,这意味着在一个线程中原子读取一个值将不会看到一个值,其中一些位来自另一个线程的原子写入之前,而一些位来自同一写入操作之后。比单个读取或写入更复杂的各种操作也可以是原子操作:例如,“比较和交换”是一种常用的原子操作,它检查变量的值,将其与第二个值进行比较,然后用另一个值替换它value 如果比较的值相等,原子地 - 例如,如果比较成功,则另一个线程不可能在操作的比较部分和交换部分之间写入不同的值。另一个线程的任何写入都将完全在原子比较和交换之前或之后执行。

你的问题的标题是:

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

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

这意味着线程将等到原子操作完成?

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

此外,如果上述陈述为真,那么所有原子操作都是线程安全的吗?

如果您组合原子操作,它们就不再是原子的。也就是说,我可以执行一个原子比较和交换操作,然后执行另一个,这两个比较和交换将单独是原子的,但它们是可整除的。因此,您仍然可能遇到并发错误。

于 2016-09-30T16:21:32.567 回答
1

原子操作是指系统完全执行或根本不执行操作。读取或写入 int64 是原子的(64 位系统和 64 位 CLR),因为系统在一次操作中读取/写入 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++ 发生原子性,您可以使用 Interlocked API:

long o = Interlocked.Increment(ref n); // other threads are blocked while the atomic operation is running
于 2018-01-27T19:29:29.920 回答