内存屏障

在计算中,内存屏障,也称为 membar、内存栅栏或栅栏指令,是一种屏障指令,它会导致中央处理器 (CPU) 或编译器对屏障前后发出的内存操作强制执行顺序约束 操作说明。 这通常意味着在屏障之前发出的操作保证在屏障之后发出的操作之前执行。

内存屏故障是必要的,因为大多数现代 CPU 都采用可能导致乱序执行的性能优化。 这种内存操作(加载和存储)的重新排序通常在单个执行线程中不会引起注意,但除非仔细控制,否则可能会导致并发程序和设备驱动程序出现不可预测的行为。 排序约束的确切性质取决于硬件并由体系结构的内存排序模型定义。 一些架构为强制执行不同的排序约束提供了多个障碍。

内存中断通常用于实现在多个设备共享的内存上运行的低级机器代码。 此类代码包括多处理器系统上的同步原语和无锁数据结构,以及与计算机硬件通信的设备驱动程序。

例子

当程序在单 CPU 机器上运行时,硬件会执行必要的簿记,以确保程序执行时就好像所有内存操作都按照程序员指定的顺序(程序顺序)执行,因此内存屏障不是必需的。 但是,当内存与多个设备共享时,例如多处理器系统中的其他 CPU,或内存映射外设,乱序访问可能会影响程序行为。 例如,第二个 CPU 可能会看到xxx个 CPU 以不同于程序顺序的顺序进行内存更改。

程序通过可以是多线程的进程运行(即软件线程,如 pthreads,而不是硬件线程)。 不同的进程不共享内存空间,因此本讨论不适用于两个程序,每个程序都运行在不同的进程(因此不同的内存空间)。 它适用于在单个进程中运行的两个或多个(软件)线程(即单个内存空间,多个软件线程共享单个内存空间)。 单个进程中的多个软件线程可以在多核处理器上同时运行。

以下在多核处理器上运行的多线程程序举例说明了这种乱序执行如何影响程序行为:

最初,内存位置 x 和 f 的值都为 0。在处理器 #1 上运行的软件线程在 f 的值为零时循环,然后打印 x 的值。 在处理器#2 上运行的软件线程将值 42 存储到 x 中,然后将值 1 存储到 f 中。 两个程序片段的伪代码如下所示。

程序的步骤对应于单独的处理器指令。

线程 #1 核心 #1:

而(f == 0); // 这里需要内存栅栏 print x;

线程 #2 核心 #2:

x = 42; // 这里需要内存栅栏 f = 1;

人们可能期望 print 语句总是打印数字 42; 然而,如果线程 #2 的存储操作是乱序执行的,则 f 有可能在 x 之前更新,因此打印语句可能会打印 0。类似地,线程 #1 的加载操作可能 乱序执行,并且有可能在检查 f 之前读取 x,因此 print 语句可能再次打印出意外的值。 对于大多数程序来说,这两种情况都是不可接受的。 必须在线程#2 分配给 f 之前插入内存屏障,以确保 x 的新值在 f 值发生变化时或之前对其他处理器可见。 另一个要点是,还必须在线程#1 访问 x 之前插入一个内存屏障,以确保在看到 f 的值发生变化之前不会读取 x 的值。

内存屏障

如果处理器的存储操作乱序执行,则硬件模块可能会在数据在内存中准备就绪之前被触发。

对于另一个说明性示例(在实际实践中出现的一个不平凡的示例),请参阅双重检查锁定。

多线程编程和内存可见性

多线程程序通常使用高级编程环境或应用程序编程接口 (API)提供的同步原语。 提供诸如互斥锁和信号量之类的同步原语以同步从并行执行线程访问资源。 这些原语通常使用提供预期内存可见性语义所需的内存屏障来实现。

0

点评

点赞

相关文章