C/C++ 语言的原子操作

目录

C语言的原子操作

 C++ 中,std::atomic 是真正的「原子」吗?

【C++11】 让多线程开发变得简单--原子变量

C++原子变量atomic详解

C++11/std::atomic - 原子变量(不加锁实现线程互斥)例子

总结1:

在C中,采用单周期指令来设置加锁标志。

中断要么在运行这条指令之前发生,要么在运行这条指令之后发生,因此,在加锁过程中不会被打断。
在一般的应用场景中,由于有入栈和出栈的动作,因此,即使在加锁过程中被中断打断,也是没有问题的;这个要具体问题具体分析。

总结2:??????

1. 在C语言中,简单的、二值量的锁定语句,并不是什么样的指令语句都能实现?

应该采用单周期的指令,以及默认长度的数据才有可能保证是原子操作;这要看具体的 CPU用户手册的说明。

注:char型数据一般可以是原子操作?浮点型数据常常需要调用函数库,因此,一般为非原子型操作?

2. 使用单周期指令加锁,可以避免因被中断打断时,有可能造成的错误(并不一定就一定会产生错误,要具体问题具体分析)。

一般而言,中断的发生,有一个入栈出栈的动作,因此,在单线程下,并不一定会产生问题。

具体是否会产生错误,还需要看程序总体的实现逻辑?

还应注意顺序等等问题:
 

//先锁定,再操作
// 当 string lockVer =“abcdef”时,这种语句不是原子的,随时会被中断打断。
bool lockVer;
lockVer =true;

for(..){}

//先操作,在锁定 : NG

for(..){}

bool lockVer =true;

说明 1:在操作系统场景,原子操作与多线程

Qt 的 copy()函数,也是原子操作?

当拷贝一个大文件时,会造成假死现象。
此时,要么采用其他的文件拷贝函数;要么,使用多线程。

C语言的原子操作

C语言原子操作是在C11(C11:标准是C语言标准的第三版,前一个标准版本是[C99]标准)引入的,定义在头文件

<stdatomic.h>

中。(C++11中内含的 C语言,有原子操作的功能??)

C++11也对原子操作进了封装,定义在头文件

<atomic>

中,这里不过多的介绍。Mac系统里有对原子操作的头文件stdatomic.h,本文的介绍也是基于这个头文件。

作者:薛定喵的鹅
链接:https://www.jianshu.com/p/3e1f4785a521
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一条C语言语句不一定是原子操作,但是一个汇编指令是原子操作吗?

一定

汇编指令也也只是在描述CPU的行为,而没有具体到每一个细节步骤。操作是否是原子的,不是由汇编指令决定的,而是由CPU如何处理这些指令决定的。

1.有些汇编指令实际上是伪指令,可能对应多条真实的二进制指令,或者只是另外一个指令的语法糖。

2.对于实际的物理硬件可以认为是由时序驱动的,在一个时钟周期里硬件的各个部分可以并行工作——结果就是多步骤行为完全是能够在一个时钟周期里被完成的。而且不能排除一个功能需要多个时钟周期才能完成。

3.中断的实现方式也会影响这一结果。CPU究竟在什么时刻,通过什么方式来响应中断,也会影响一项操作是否会被中断打断。


只有少数指令是,这些指令隐含lock操作,如swap,或者不需要操作总线,如sti,cli,dec %rax

如果不是对齐的data,那么可能需要load几次。

在多处理器下,显然不是,因为普通的指令不会锁住其他CPU,否则还要多处理器干嘛。。。。在单处理器下,也不一定,比如缺页中断就会在某些指令期间响应

有指令是 有的指令不是,因为有的指令是批量传输数据,Linux kernel 里面就有 linus写

有些是单周期指令,有些是多周期指令,显然,不一定是。

汇编语言->机器语言->微程序->微指令->逻辑电路

简单的说,一条汇编语言指令对应的是一段由CPU生产商写好并固化在CPU内部的程序。所以,它可以在非常多的地方被打断。具体情况取决于CPU实现方式。

访问对齐内存零次或一次的单条指令是原子的。lock前缀的可以保证多次读写内存原子。


作者:BHEscaper
链接:https://www.zhihu.com/question/28092666/answer/39828018
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

C++ 中,std::atomic 是真正的「原子」吗?

https://www.zhihu.com/question/469476598

是真正的“原子”。只不过,你严重误解了原子这个词。

一个变量是原子的,意思并不是整个程序对这个变量的八万次读写是一个整体。没这回事,

​不要数据库事务的原子性混淆。

变量是原子的,意思是每次操作这个变量,包括读取其值、更改其值,这次操作中执行的唯一一条指令是原子的(对于面向对象语言,也可以是一次接口调用是原子的,这个后面会讲到)。

正因此,很多CPU才不得不专门提供一条Test and Exchange指令,把“检测内容,等于xx则修改其值”这样必须两三条指令才能完成的操作整合进一条指令,且在执行时锁定总线

如果只靠总线锁定前缀的话,这个test and exchange操作先后执行的几条指令,虽然每一条都是原子的,但加起来就不是了。也就是这里必须提供一个方法,用来把几条指令绑起来(的确有CPU允许你在连续执行多条指令期间锁定总线,从而灵活组合一堆指令、让它们加起来是原子的。但这类操作往往需要很高特权级,不是用户态应用可以用的)。

注意这里有个思想:锁操作代价高昂,因此锁的粒度一定要小。最好小到只有一条指令,那么锁操作的代价就完全可控了。

注意,一条指令也是需要取指、译码、执行、写回结果等许多步骤的。

普通指令,这些步骤可以打断,可以相互穿插。比如,字节不对齐时,一条指令可能需要两次以上访存才能把待操作的数字载入寄存器,如果两次访存之间被打断、且另一条指令改写了被操作数字时,这里就会出现脏读

(脏写也类似)。

CPU可以智能的分析指令执行涉及到的东西,从而避免几条指令穿插影响。但这个智能并不太高。比如,操纵和CPU字长相等的、字节对齐的int时,很多CPU可以保证原子性。

没错,不用锁总线,天然支持,只要你保证它的地址对齐满足CPU要求。但你要自己查阅资料来确认这一点,同时还要确认你的编译器

生成的代码能够保证地址对齐。

那么这时候,cpp

的atomic实际上等于什么都没做。

当然了,经常的,仅仅一条甚至七八条指令的原子性也是不够用的。比如,电商开发中经常遇到的,用户购买流程,需要先锁库存,下订单,等待付款,付款完成再把商品标记为出库:这一整套成千上万条指令都必须是原子的。

CPP的atomic不能也不该来保证这一点。

还记得吗?锁的粒度要尽量小。所以CPU努力把它做到了一条指令。

但是,电商这种情况该怎么办?

很简单,用一个atomic变量

来保护数据。

注意了,CPU只能保护这个atomic变量不脏读脏写,它可保护不了你要保护的一大片数据在千百条指令执行过程中的原子性。

那该怎么办?

简单,想象一条虚拟的总线,当函数a执行时,锁住这条虚拟总线

;执行完了,再给这条虚拟总线解锁。

比如,我们可以把数据库连接看作这条总线,然后把一个atomic变量等于1看作锁定状态,等于0则是free状态:那么,只要每个人都确认这个锁的从0到1是自己做的(也就是持有这个锁),那么就可以去修改数据库了。注意改完了要把锁恢复到0哦。

类似的,只要通过接口或者别的什么(甚至是文档中的约定)确保一切访问之前都要举行一个“锁定虚拟总线”的仪式,假装自己真的是通过这条总线访问的,那么当然也能达到原子访问的目的。

这就是为什么很多库会告诉你“随便用!我保证线程安全”的原因:很简单,它把必要的仪式都写死在接口实现代码里了。

注意,你的确得到了原子性/线程安全,执行时但并没有真的锁物理总线

(我在脑中锁虚拟总线,关你物理总线什么事)。

只除了那条至关重要的test and exchange执行时。

仔细阅读、提炼这段描述,你才会理解为什么test and Exchange是必要的、为什么没有它就没办法正确实现锁。

当然,锁数据库链接粒度实在太大,这等于把数据库变成串行的了。实践中,数据库内部替你维护了表(级)锁

、行锁等更小粒度的锁,甚至还区分读写锁

。。。

类似的,电商购物,你也不要真的从用户下订单到支付完成都锁定数据库表或者表中的一行。不合适的使用大粒度锁

是专业水平欠佳的表现。

相反,你要审慎思考:占用库存究竟是”锁住数据库中的一行(从而独占的修改它)”呢,还是“正确的把数据库余额减一(这个操作需要原子性),然后记住这个临时扣除状态,在用户付款后把状态改为永久、或者在用户付款后把余额再加一”:前者带来极长时间的数据库锁定,而后者只需要修改余额这个动作本身是原子性的。

这就是锁的优化。

前些年很是流行过一阵“无锁编程

”,其核心思想就是不用任何大粒度锁,而是把一切同步操作都压缩到一条CPU指令。

换句话说,无锁编程并不是真正的无锁,而是锁粒度的极致优化,优化到只剩一条test and exchange指令。

换句话说:一切复杂操作的原子性、最终都可以归结为“一个guard变量”的原子性。

再换句话说,每次访问之前,都先围绕着guard举行一套仪式,就可以保证另外一堆复杂数据的原子性。这套仪式的关键,最终都可以归结到一条test and exchange指令。

能否认识到这一点,是你能否理解你的问题的必要前提(也是能否看出那群“一提12306,就说锁库存

很可怕”的人的成色的前提)。

换句话说,真的理解了这里提到的东西,你才会明白自己的问题错在哪里。

-------------

感觉现有的回答都不全面。

cpp原子库

有些实现是调用操作系统提供的接口,所以这个实现可能跟操作系统有关。同时,又因为操作系统的实现是依赖于硬件的,所以具体的锁的实现要取决于硬件的支持情况。

题主有一个误区,因为原子操作一定需要关调度,关中断,这个理解是错的。锁的本质是同步数据,理论上说,只要保证特定的数据不被修改即可。在硬件层面上看,就是特定的物理内存

,特定的cache line,不被修改,所以,并不是所有原子操作,都一定要关调度,关中断。

大多数主流的CPU,都会提供硬件指令,原子修改某个内存,对于Intel

的CPU,手册system programming guide的8.1.2.2 Software Controlled Bus Locking里有详细的描述:

The bit test and modify instructions
(BTS, BTR, and BTC).
The exchange instructions (XADD, CMPXCHG, and CMPXCHG8B).
The LOCK prefix is automatically assumed for XCHG instruction.
The following single-operand arithmetic and logical instructions: INC, DEC, NOT, and NEG.
The following two-operand arithmetic and logical instructions: ADD, ADC, SUB, SBB, AND, OR, and XOR.

如果操作系统使用的是硬件指令实现原子操作,那么用这些指令就可以了,这种实现不需要锁,不需要关中断或者调度。

对于ARM/PPC/RISCV也有类似的指令。

但是,也有例外情况。

比如ARM32/PPC32/RISCV32上不支持对8B数据的原子操作,只有Intel在32位环境中提供了CMPXCHG8B的指令(甚至于,Intel还提供了CMPXCHG16B的指令),这种情况就比较麻烦了。

atomic库里是提供了各种长度的数据的,如果硬件本身不支持,那么就需要通过软件实现。atomic库里有std::atomic_is_lock_free来告诉应用程序,这个操作是不是lock free的,如果不是,那么底层就是用锁来实现的。具体可以参考这个:

std::atomic_is_lock_free, ATOMIC_xxx_LOCK_FREE​

en.cppreference.com/w/cpp/atomic/atomic_is_lock_free

如果不是lock free

的,那么其实它的实现跟用户自己用锁来实现是差不多的,甚至性能还不如用户自己实现。通过软件实现的原子操作是需要关中断,关调度,使用mem fence等动作保证数据不被改变,如果是用户自己的代码,确认中断不会更改关键数据的话,那么可以不用关中断。

某些开源库并没有考虑到不同硬件上的原子操作差异,所以在某些平台上,使用了原子操作的开源库实际上是会有风险的(比如openmp

的kmp库在PPC/RISCV上)。

所以,原子操作可能是虚假的(软件模拟),也可能是真实的(硬件指令),在X86平台上,基本上都是真实的,在非X86平台,有可能是虚假的。

-------

,C++当中的std::atomic只是逻辑概念上的“原子”操作,实际如何实现是在编译的时候由编译器根据目标机器的CPU类型决定的。

C++11原子类型与原子操作

1.认识原子操作

原子操作就是在多线程程序中“最小的且不可并行化的”操作,意味着多个线程访问同一个资源时,有且仅有一个线程能对资源进行操作。通常情况下原子操作可以通过互斥的访问方式来保证,例如Linux下的互斥锁(mutex),Windows下的临界区(Critical Section)等。下面看一个Linux环境使用POSIX标准的pthread库实现多线程下的原子操作:

https://cloud.tencent.com/developer/article/1380964

【C++11】 让多线程开发变得简单--原子变量

https://cloud.tencent.com/developer/article/1901820

C++原子变量atomic详解

一、简介

原子类型的对象包含特定type ()的值。

原子对象的主要特征是,从不同的线程访问这个包含的值不会导致数据竞争(即,这样做是明确定义的行为,访问正确排序)。通常,对于所有其他对象,导致同时访问同一对象的数据争用的可能性将操作限定为未定义行为。

【官方介绍】。此外,对象能够通过指定不同的内存顺序来同步对其线程中其他非原子对象的访问。

template <class T> struct atomic;

二、成员函数

2.1、构造函数

std::atomic::atomic。

(1)默认:使对象处于未初始化状态。 atomic() noexcept = default;
(2)初始化 :使用val初始化对象。constexpr atomic (T val) noexcept;
(3)复制 [删除] :无法复制/移动对象。 atomic (const atomic&) = delete;

示例:

std::atomic<bool> ready (false);

2.2、is_lock_free函数

2.3、store函数

2.4、load函数

2.5、exchange函数

2.6、compare_exchange_weak函数

2.7、compare_exchange_strong函数

2.8、专业化支持的操作

总结

原子操作多线程中可以保证线程安全,而且效率会比互斥量好些。

C++11/std::atomic - 原子变量(不加锁实现线程互斥)

std::atomic<long> globalCount = 0;

C++11/std::atomic - 原子变量(不加锁实现线程互斥)_c++原子变量_HW140701的博客-CSDN博客

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
现代 C 语言是指 C11/C17 标准之后的 C 语言版本。其核心特性包括了多线程、原子操作、泛型和 stdatomic 库等。下面具体解析一下现代 C 语言的核心特性。 1. 多线程 多线程是现代 C 语言中最重要的特性之一。多线程使得程序可以同时执行多个任务,从而提高程序性能。现代 C 语言通过 POSIX 线程库和 Windows 线程库实现多线程编程。POSIX 线程库是 POSIX 标准中定义的线程库,可以跨平台使用。Windows 线程库是 Windows 操作系统中的线程库,可以在 Windows 系统上使用。 2. 原子操作 原子操作是现代 C 语言中的另一个重要特性。原子操作可以保证多线程环境下的数据、变量等在并发访问时不会出错。现代 C 语言提供了一组原子操作的 API。这些 API 包括了原子加、原子减、原子赋值、原子与、原子或等。这些原子操作可以在不同的平台上使用。 3. 泛型 现代 C 语言引入了泛型的概念。泛型允许程序员在不同的数据类型上编写通用的代码。这使得现代 C 语言可以实现更加通用的数据结构和算法。现代 C 语言中的泛型使用了类型参数化的技术,即把某些代码中的类型抽象出来作为参数。 4. stdatomic 库 stdatomic 库是现代 C 语言中的一个库,它提供了原子类型和原子操作等特性。stdatomic 库使用了 C11 标准中的 _Atomic 关键字来定义原子类型。stdatomic 库中包含了原子加、原子减、原子赋值等操作函数,这些操作可以在多线程环境下执行,保证数据的正确性。 总之,现代 C 语言的核心特性包括了多线程、原子操作、泛型和 stdatomic 库等。这些特性使得现代 C 语言可以更好地支持并发编程、泛型编程等。开发者可以充分利用这些特性来提高程序性能和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值