什么是线程同步以及它与原子性有何不同?

What is thread synchronization and how does it differ form atomicity?

原子性可以通过compare and swap (CS)等机器级指令实现。 它也可以通过对大块代码使用 mutex/lock 来实现,OS 提供帮助。

另一方面我们也有memory model的概念。有些机器可能有像 Arm 这样的 relaxed 模型,它可以在单个线程上重新排序 load/stores,而有些机器有更严格的模型,如 x86.

我想确认一下我对同步这个词的理解。它几乎是 atomicitymemory model 的承诺?即只在一个线程上使用原子操作并不一定要使它与其他线程同步?

某物原子是不可分割的。 同步的事情在时间上同时发生。

原子性

我喜欢将其视为具有表示具有 x、y 坐标的二维点的数据结构。出于我的目的,为了使我的数据被视为“有效”,它必须始终是 x = y 线上的一个点。 x 和 y 必须始终相同。

假设最初我有一个点 { x = 10, y = 10 } 并且我想更新我的数据结构以使其代表点 {x = 20, y = 20}。并假设更新操作的实现基本上是这两个独立的步骤:

  1. x = 20
  2. y = 20

如果我的实现像那样分别写入 x 和 y,那么其他线程可能会在步骤 1 之后但在步骤 2 之前观察到我的点数据结构数据。如果允许在我更改之后读取点的值x 但在我更改 y 之前,其他观察者可能会观察到值 {x = 20, y = 10}.

实际上可以观察到三个值

  • {x = 10, y = 10}(原值)[有效]
  • {x = 20, y = 10}(x已修改但y尚未修改)[INVALID x != y]
  • {x = 20, y = 20}(x和y都被修改)[VALID]

我需要一种方法来同时更新这两个值,这样外部观察者就不可能观察到 {x = 20, y = 10}。

我真的不在乎何时其他观察者会看我观点的价值。它观察到 { x = 10, y = 10 } 很好,如果它观察到 { x = 20, y = 20 } 也很好。两者都有 x == y 的 属性,这使它们在我的场景中有效。

最简单的原子操作

最简单的原子操作是测试和设置单个位。此操作以原子方式读取位的值并用 1 覆盖它,return我们覆盖的位的状态。但是我们得到保证,如果我们的操作已经结束,那么我们将拥有我们覆盖的值,并且任何其他观察者将观察到 1。如果许多代理同时尝试此操作,则只有一个代理将 return 0,并且其他人都会 return 1. 即使是两个 CPU 在完全相同的时钟滴答上写入,电子设备中的某些东西也会保证根据我们的规则逻辑上原子地完成操作。

这就是逻辑原子性。这就是所有原子手段。这意味着您有能力在更新前后使用有效数据执行不间断更新,并且数据在更新期间可能出现的任何中间状态下都无法被其他观察者观察到。它可能是单个位,也可能是整个数据库。

x86 示例

可以在 x86 上以原子方式完成的事情的一个很好的例子是 32 位互锁增量。

此处 32 位(4 字节)值必须递增 1。这可能需要修改所有 4 个字节才能正常工作。如果要将该值从 0x000000FF 修改为 0x00000100,重要的是 0x00 变为 0x00 并且 0xFF 变为 0x00 atomically。否则,我可能会观察值 0x00000000(如果先修改 LSB)或 0x000001FF(如果先修改 MSB)。

硬件保证我们一次可以测试和修改4个字节来实现。 CPU 和内存提供了一种机制,即使有其他 CPU 共享同一内存,也可以执行此操作。 CPU 可以声明一个锁定条件,以防止其他 CPU 干扰此互锁操作。

同步

同步只是谈论事情如何在时间上一起发生。在您提出的上下文中,它是关于我们程序的各个部分的执行顺序以及我们系统的各个组件更改状态的顺序。没有同步,我们就有损坏的风险(输入无效的、语义上无意义的或不正确的程序或其数据执行状态)

假设我们想要一个 64 位数字的互锁增量。让我们假设硬件 而不是 提供一种方法来一次自动更改 64 位。我们将不得不用更复杂的数据结构来完成我们想要的,这意味着 即使只是读取 我们也不能简单地读取最重要的 32 位和最不重要的 32 位我们的 64 位数字分开。我们冒着观察 64 位值的一部分与另一半分开变化的风险。这意味着我们在读取(或写入)这个 64 位值时必须遵守某种协议

为了实现这个,我们需要一个原子测试和设置位操作和一个清除位操作。 (仅供参考,从技术上讲,我们需要的是计算机科学中通常称为 的两个操作,但让我们保持简单。)在读取或写入数据之前,我们对单个(共享)位(通常称为“锁”)。如果我们读到一个 0,那么我们就知道我们是唯一看到 0 的人,其他人一定都看到了 1。如果我们看到 1,那么我们假设其他人正在使用我们的共享数据,因此我们别无选择但再试一次。所以我们循环并继续测试和设置该位,直到我们观察到它为 0。(这称为 自旋锁 ,这是我们在没有操作系统帮助的情况下所能做的最好的事情调度程序。)

当我们最终看到 0 时,我们就可以安全地分别读取 64 位值的两个 32 位部分。或者,如果我们正在写入,我们可以安全地分别写入 64 位值的两个 32 位部分。读取或写入两半后,我们将位清零,允许其他人访问。

任何此类巧妙结合使用原子操作来避免以这种方式损坏的行为都构成了同步,因为我们正在控制程序的某些部分可以[=113]的顺序=].只要我们能够访问某种原子数据,我们就可以实现任何复杂性和任何数量的数据的同步。

一旦我们创建了一个使用锁以无冲突方式共享数据结构的程序,我们也可以将该数据结构称为逻辑原子。 C++提供了一个std::atomic来实现这个,比如

请记住,此级别(使用锁)的同步是通过遵守协议(使用锁保护您的数据)实现的。其他形式的同步,例如当两个 CPU 尝试在同一时钟周期访问同一内存时发生的情况,由 CPU 和主板、内存、控制器等在硬件中解决.但基本上类似的事情正在发生,只是在主板层面。