我试图让“原子与非原子”概念在我的脑海中解决。我的第一个问题是我找不到“现实生活中的类比”。就像原子操作上的客户/餐馆关系或类似的东西。
此外,我想了解原子操作如何将自己置于线程安全编程中。
在这篇博文中; http://preshing.com/20130618/atomic-vs-non-atomic-operations/ 它被提到:
作用于共享内存的操作如果在a中完成则是原子操作 相对于其他线程的单步。 当原子商店是 在共享变量上执行,没有其他线程可以观察到 修改半完成。当对a执行原子加载时 共享变量,它读取单个出现的整个值 这一刻。非原子载荷和商店不会产生这些 保证。
“没有其他线程可以观察修改半完成”是什么意思?
这意味着线程会等到原子操作完成?该线程如何知道该操作是原子的?例如在.NET中我可以理解,如果你锁定对象,你设置一个标志来阻止其他线程。但原子怎么样?其他线程如何知道原子操作和非原子操作之间的区别?
如果上面的语句为真,那么所有原子操作都是线程安全的吗?
答案 0 :(得分:8)
让我们澄清什么是原子,什么是块。原子性意味着操作要么完全执行,所有的副作用都是可见的,要么根本不执行。因此,所有其他线程可以在操作之前或之后查看状态。由互斥锁保护的代码块也是原子的,我们只是不称它为操作。原子操作是特殊的CPU指令,在概念上类似于互斥锁保护的常规操作(你知道互斥锁是什么,所以我会使用它,尽管它是使用原子操作实现的)。 CPU具有一组有限的操作,可以自动执行,但由于硬件支持,它们非常快。
当我们讨论线程块时,我们通常会在对话中涉及互斥,因为它们保护的代码可能需要相当长的时间来执行。所以我们说线程在互斥上等待。对于原子操作情况是一样的,但它们很快,我们通常不关心这里的延迟,因此不太可能一起听到“阻塞”和“原子操作”这两个词。
这意味着线程会等到原子操作完成?
是的,它会等待。 CPU将限制对变量所在的内存块的访问,并且其他CPU内核将等待。请注意,出于性能原因,块仅保留在原子操作本身之间。允许CPU核心缓存变量以供读取。
该线程如何知道该操作是原子的?
使用特殊的CPU指令。只是在程序中写入了特定的操作应该以原子方式执行。
其他信息:
原子操作还有更棘手的部分。例如,在现代CPU上,通常所有原始类型的读写都是原子的。但是CPU和编译器可以重新排序。因此,您可能更改某个结构,设置一个标志,告知它已更改,但CPU重新排序在结构实际提交到内存之前写入并设置标志。当您使用原子操作时,通常会做一些额外的工作来防止意外的重新排序。如果你想了解更多,你应该阅读内存障碍。
简单的原子存储和写入没有用处。要最大限度地利用原子操作,您需要更复杂的东西。最常见的是CAS - 比较和交换。您将变量与值进行比较,并仅在比较成功时更改它。
答案 1 :(得分:4)
在典型的现代CPU中,原子操作以这种方式原子化:
当发出访问内存的指令时,内核的逻辑会尝试将内核的缓存置于正确的状态以访问该内存。通常,这种状态将在内存访问必须发生之前实现,因此没有延迟。
虽然另一个核心正在对一块内存执行原子操作,但它会将该内存锁定在自己的缓存中。这可以防止任何其他内核在原子操作完成之前获取访问该内存的权限。
除非两个内核碰巧执行对许多相同内存区域的访问,并且许多访问都是写入,否则这通常不会涉及任何延迟。这是因为原子操作非常快,通常核心事先知道它需要访问哪些内存。
所以,假设最后一次在核心1上访问了一块内存,现在核心2想要进行原子增量。当核心的预取逻辑在指令流中看到对该存储器的修改时,它将指示高速缓存获取该存储器。缓存将使用内核总线从核心1的缓存中获取该内存区域的所有权,并将该区域锁定在自己的缓存中。
此时,如果另一个核心尝试读取或修改该内存区域,则在释放锁定之前,它将无法在其缓存中获取该区域。这种通信发生在连接缓存的总线上,并且精确地发生在哪里取决于内存所在的缓存。(如果根本不在缓存中,那么它必须转到主内存。)
缓存锁通常不会被描述为阻塞线程,因为它非常快,并且因为核心通常能够在尝试获取锁定在其他缓存中的内存区域时执行其他操作。从更高级代码的角度来看,原子的实现通常被认为是一个实现细节。
所有原子操作都可以保证不会看到中间结果。这就是使它们成为原子的原因。
答案 2 :(得分:1)
您描述的原子操作是处理器内的指令,硬件将确保在原子写入完成之前不能在内存位置进行读取。这可以保证线程在写入之前读取值或在写入操作之后读取值,但是之间没有任何内容 - 没有机会读取其中一半的字节写入之前的值和写入之后的另一半。
针对处理器运行的代码甚至不知道这个块,但它与使用lock
语句确保更复杂的操作(由许多低级指令组成)是原子的没有什么不同。
单个原子操作始终是线程安全的 - 硬件保证操作的效果是原子的 - 它永远不会在中间被中断。
在绝大多数情况下,一组原子操作不是原子操作(我不是专家所以我不想做出明确的陈述,但我想不出这种情况会有所不同) - 这就是复杂操作需要锁定的原因:整个操作可能由多个原子指令组成,但整个操作可能仍然在这两个指令之间被中断,从而导致另一个线程看到半生不熟的结果的可能性。锁定可确保在共享数据上运行的代码在其他操作完成之前无法访问该数据(可能通过多个线程切换)。
this question / answer中显示了一些示例,但您可以通过搜索找到更多示例。
答案 3 :(得分:1)
“atomic”是一个属性,适用于由实现(硬件或编译器,通常来说)强制执行的操作。对于现实生活中的类比,请查看需要交易的系统,例如银行账户。从一个帐户到另一个帐户的转帐涉及从一个帐户提款和另一个帐户的存款,但通常这些应该原子地执行 - 没有时间提取资金但尚未存入,或者反之亦然。
所以,继续对你的问题进行类比:
“没有其他线程可以观察修改半完成”是什么意思?
这意味着没有线程可以在一个帐户中提取但是没有存入另一个帐户的状态下观察这两个帐户。
在机器术语中,它意味着一个线程中的值的原子读取将看不到具有来自另一个线程的原子写入之前的一些位的值,以及来自相同写入操作之后的一些位的值。比单个读取或写入更复杂的各种操作也可以是原子的:例如,“比较和交换”是一种常见的原子操作,它检查变量的值,将其与第二个值进行比较,并将其替换为另一个值。值,如果比较值相等,则原子 - 例如,如果比较成功,则另一个线程不可能在比较和操作的交换部分之间写入不同的值。任何另一个线程的写入都将完全在原子比较和交换之前或之后完成。
您问题的标题是:
原子操作是否会阻止其他线程?
在“阻止”的通常含义中,答案是否定的;一个线程中的原子操作本身不会导致执行停止在另一个线程中,尽管它可能导致活锁情况或以其他方式阻止进度。
这意味着线程会等到原子操作完成?
从概念上讲,这意味着他们永远不需要等待。操作要么已完成,要么未完成;它永远不会完成。实际上,原子操作可以使用互斥量来实现,但性能成本很高。许多(如果不是大多数)现代处理器在硬件级别支持各种原子基元。
如果上面的语句为真,那么所有原子操作都是线程安全的吗?
如果你组成原子操作,它们就不再是原子的。也就是说,我可以做一个原子比较和交换操作,然后是另一个,两个比较和交换将分别是原子的,但它们是可分的。因此,您仍然可能存在并发错误。
答案 4 :(得分:0)
原子操作意味着系统完全执行操作或根本不执行操作。读取或写入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