内存模型 原子操作 并发 C 内存屏障 (内存模型 内存结构)

本文目录导航:

C++并发:原子操作、内存模型、内存屏障

原子操作是指在执行过程中不会被中断的操作,要么全部执行成功,要么全部不执行,不会出现部分执行的情况。

原子操作可以看作是不可分割的单元,运行期间不会有任何的上下文切换。

原子操作类常用成员函数有fetch_*、store、load、exchange、compare_exchange_weak和compare_exchange_strong:fetch_*:先获取值再计算,即返回的是修改之前的值。

store:写入数据。

load:加载并返回数据。

exchange:直接设置一个新值。

compare_exchange_weak:先比较第一个参数的值和要修改的内存值(第二个参数)是否相等,如果相等才会修改,该函数有可能在except == value时也会返回false所以一般用在while中,直到为true才退出。

compare_exchange_strong:功能和*_weak一样,不过except == value时该函数保证不会返回false,但该函数性能不如*_weak。

注意:使用操作符(如+=、++、^=等)时要看类成员是否提供对应操作符,否则可能出现意想不到的问题。

C++标准库中的原子类型,如atomic,用于实现原子操作。

它提供了一种线程安全的方式来对特定类型的数据进行读取和写入,以及执行其他常见的原子操作,如增加(增量)和交换等。

内存模型描述的是程序在执行过程中内存操作正确性的问题。

内存操作包括读操作和写操作,每一操作又可以用两个时间点界定:发出(Invoke)和响应(Response)。

内存一致性模型描述的就是这些操作可能的执行顺序中哪些是正确的。

内存序问题:内存序(memory order)问题是由于多线程的并行执行可能导致的对共享变量的读写操作无法按照程序员预期的顺序进行。

多线程运行时,可能出现指令执行级别的优化:乱序优化,流水线,乱序执行,分支预测等合理重排。

因此需要内存序来限制CPU对指令执行顺序的重排程度。

happens-before和synchronizes-with语义如果两个操作之间存在依赖关系,并且一个操作一定比另一个操作先发生,那么者两个操作就存在happens-before关系;synchronizes-with关系指原子类型之间的操作,如果原子操作A在像变量X写入一个之后,接着在同一线程或其它线程原子操作B又读取该值或重新写入一个值那么A和B之间就存在synchronizes-with关系;注意这两中语义只是一种关系,并不是一种同步约束,也就是需要我们编程去保证,而不是它本身就存在memory_order模式C++11中引入了六种内存约束符用以解决多线程下的内存一致性问题(在头文件中),其定义如下:Sequential consistency模型又称为顺序一致性模型,是控制粒度最严格的内存模型。

在顺序一致性模型下,程序的执行顺序与代码顺序严格一致,也就是说,在顺序一致性模型中,不存在指令乱序。

Relax模型对应的是memory_order中的memory_order_relaxed。

从其字面意思就能看出,其对于内存的限制最小,也就是说这种方式只能「保证当前的数据访问是原子操作(不会被其他线程的操作打断)」,但是对内存访问顺序没有任何约束,也就是说对不同的数据的读写可能会被重新排序。

Acquire-Release模型的控制力度介于Relax模型和Sequential consistency模型之间。

其定义如下:假设有一个原子变量A,对其进行写操作X的时候施加了memory_order_release约束符,则在当前线程T1中,该操作X之前的任何读写操作指令都不能放在操作X之后。

当另外一个线程T2对原子变量A进行读操作的时候,施加了memory_order_acquire约束符,则当前线程T1中写操作之前的任何读写操作都对线程T2可见;当另外一个线程T2对原子变量A进行读操作的时候,如果施加了memory_order_consume约束符,则当前线程T1中所有原子变量A所依赖的读写操作都对T2线程可见(没有依赖关系的内存操作就不能保证顺序)。

一个对原子变量的「load操作」时,使用memory_order_acquire约束符:在「当前线程」中,该load之后读和写操作都不能被重排到当前指令前。

如果「其他线程」使用memory_order_release约束符,则对此原子变量进行store操作,在当前线程中是可见的。

假设有一个原子变量A,如果A的读操作X施加了memory_order_acquire标记,则在当前线程T1中,在操作X之后的所有读写指令都不能重排到操作X之前;当其它线程如果对A进行施加了memory_order_release约束符的写操作Y,则这个写操作Y之前所有的读写指令对当前线程T1是可见的。

一个「load操作」使用了memory_order_consume约束符:在「当前线程」中,load操作之后的依赖于此原子变量的读和写操作都不能被重排到当前指令前。

如果有「其他线程」使用memory_order_release内存模型对此原子变量进行store操作,在当前线程中是可见的。

Acquire-Release模型中的其它三个约束符,要么用来约束读,要么用来约束写。

那么如何对一个原子操作中的两个动作执行约束呢?这就要用到 memory_order_acq_rel,它既可以约束读,也可以约束写。

对于使用memory_order_acq_rel约束符的原子操作,对当前线程的影响就是:当前线程T1中此操作之前或者之后的内存读写都不能被重新排序(假设此操作之前的操作为操作A,此操作为操作B,此操作之后的操作为C,那么执行顺序总是ABC,这块可以理解为同一线程内的sequenced-before关系);对其它线程T2的影响是,如果T2线程使用了memory_order_release约束符的写操作,那么T2线程中写操作之前的所有操作均对T1线程可见;如果T2线程使用了memory_order_acquire约束符的读操作,则T1线程的写操作对T2线程可见。

内存屏障的引入,本质上是由于CPU重排序指令引起的。

内存屏障(Memory Barrier)是一种硬件或软件指令,用于控制处理器和内存系统中对内存操作的重新排序和优化。

它们的作用是确保在屏障之前和之后的内存访问按照预期的顺序进行。

内存屏障主要有两种类型:读屏障(Read Barrier)和写屏障(Write Barrier)。

读屏障(也称为加载屏障):确保在读取一个变量的值之前,所有之前的读取操作和加载操作都已经完成。

这可以防止读取过期的或无效的数据。

写屏障(也称为存储屏障):确保在写入一个变量的值之前,所有之前的写入操作和存储操作都已经完成。

这可以防止将新的值预先存储到缓存而不是实际写入到内存中。

内存屏障的使用可以避免在多线程或并发环境下出现的一些问题,例如数据竞争、乱序执行和原子操作的正确性。

通过插入内存屏障,可以使得代码在一个屏障之前或之后的内存访问按照预期的顺序执行,从而确保正确的内存可见性和一致性。

C++中内存屏障包括:std::atomic_thread_fence、std::atomic_signal_fence、std::thread::join。

创建一个内存屏障(memory barrier),用于限制内存访问的重新排序和优化。

它可以保证在屏障之前的所有内存操作都在屏障完成之前完成。

常见的 memory_order 参数包括:std::memory_order_relaxed:最轻量级的内存顺序,允许重排和优化。

std::memory_order_acquire:在屏障之前的内存读操作必须在屏障完成之前完成。

std::memory_order_release:在屏障之前的内存写操作必须在屏障完成之前完成。

std::memory_order_acq_rel:同时具有 acquire 和 release 语义,适用于同时进行读写操作的屏障。

std::memory_order_seq_cst:对于读操作相当于获得,对于写操作相当于释放。

内存模型 原子操作 并发 C 内存屏障 (内存模型 内存结构)

C|并发编程|互斥锁实现

锁Lock在并发编程中扮演着关键角色,确保资源的访问权限。

现实中的锁原理同样适用于编程环境,避免线程间的意外交互。

在严格编程环境下,Functional Programming方法保证了线程安全,但在结构化编程中,锁成为了防止副作用的普遍工具。

自旋Spinning锁是最基础的锁实现,通过不断检测锁状态来获取或释放锁,但这种方法在未获得锁时会导致CPU资源的无效占用。

原子性Atomicity是锁的一种改进版本,通过使用atomic exchange确保锁获取的原子性。

C代码形式中,如TestAndSet和CompareAndSwap,实现了线程安全的锁操作。

饥饿Starvation问题可能在锁机制中出现,导致某些线程长时间无法获得锁。

解决方法之一是使用队列,实现FIFO顺序。

Sleeping锁是另一种改进方式,它在未获取锁时让线程直接进入休眠状态,直到锁被释放。

使用链表作为数据结构,提高了锁的效率。

在锁的设计中,存在Buggy情况,如在自旋和唤醒操作之间,可能导致线程无法被唤醒。

解决方法包括使用特定函数控制线程的等待状态。

Two-phase锁结合了自旋和睡眠机制的优点,通过分阶段实现锁的获取和释放,同时考虑到锁的短暂状态,使用自旋更高效,而在等待时则让线程休眠。

Linux的Mutex实现通过futex_wait和futex_wake维护锁状态,提供了一种高效的锁管理机制。

该机制通过自旋和休眠操作,平衡了锁的获取和释放效率。

在锁的实现中,特别关注了setpark操作,以防止在特定情况下的死锁问题。

同时,考虑了队列空状态下的异常情况,确保锁机制的稳定运行。

将锁机制与轮询和中断类比,可以更好地理解锁的工作原理。

锁就像是CPU不断检查IO状态的轮询操作,而休眠则类似于仅在IO状态改变时触发的中断机制。

信息隔离导致了信息传递效率的差异,锁机制通过确保在特定条件下进行资源访问,避免了不必要的线程交互,提高了系统的整体性能。

int pthread_mutex_init设置互斥锁的强健属性

在C语言的并发编程中,`pthread_mutex_init`函数用于初始化互斥锁,而`pthread_mutexattr_setrobust_np`是一个高级特性,用于设置互斥锁属性的强健性。

此函数在#include 中定义,其原型为:int pthread_mutexattr_setrobust_np(pthread_mutexattr_t *attr, int *robustness);只有在支持`_POSIX_THREAD_PRIO_INHERIT`标志的环境中,这个函数才可用。

`attr`参数是通过`pthread_mutexattr_init()`创建的互斥锁属性对象,而`robustness`则用于定义属主失败时的行为。

它有两个可能的值:PTHREAD_MUTEX_ROBUST_NP和PTHREAD_MUTEX_STALLED_NP,默认值为PTHREAD_MUTEX_STALLED_NP。

- PTHREAD_MUTEX_ROBUST_NP:当属主失败时,所有后续对`pthread_mutex_lock()`的调用会不确定地被阻塞,直至互斥锁状态恢复。

错误代码为EOWNERDEAD。

- PTHREAD_MUTEX_STALLED_NP:属主失败时,会解除锁并让下一个线程尝试获取,失败者会返回EOWNERDEAD。

新属主需要确保状态一致,如果可能,调用`pthread_mutex_consistent_np()`。

否则,不应调用该函数,而是解除锁,其他等待线程将被唤醒,后续调用将无法获取,返回ENOTRECOVERABLE。

此时可能需要重新初始化互斥锁。

如果出现以下错误,`pthread_mutexattr_setrobust_np()`会返回对应的错误代码:- ENOSYS:不支持或未定义`_POSIX_THREAD_PRIO_INHERIT`。

- ENOTSUP:指定的`robustness`值不受支持。

- EINVAL:`attr`或`robustness`参数无效。

因此,设置互斥锁的强健属性需要谨慎处理,确保正确处理属主失败情况,以维护线程安全。

你可能想看:
« 上一篇
下一篇 »

相关推荐

FreeBasic:强大免费开源的64位basic编译器及应用优势

2025年05月10日

82阅读

脊柱侧弯矫正方法大盘点:运动康复、支具矫正等多种方式

2025年05月09日

87阅读

如何在winForm标题栏去掉标题文字且任务栏图标正常显示

2025年05月09日

85阅读

十一黄金周促销广告词大全,国庆购物狂欢等你来

2025年05月09日

77阅读

关闭快速编辑模式及插入模式,设置控制台指定文字颜色举例

2025年05月09日

63阅读

黄金搭档低糖高钙饮品包装及成分功效全解析,适合人群要知晓

2025年05月09日

91阅读