C++11原子操作

目录

1.什么是原子操作

2.为什么需要原子操作?

3.C++中的原子操作

4.原子操作使用及注意

5.应用场景

6.使用原子操作的最佳实践

7.原子操作与锁机制的比较

8.总结


1.什么是原子操作

        原子操作是一种不可分割的操作,即在多线程环境中,这些操作要么全部执行完成,要么根本没有执行,中间不会被其他线程打断。这种特性使得原子操作在保证数据一致性和线程安全方面具有显著优势。

        原子操作是在多线程程序中“最小的且不可并行化的”操作,意味着多个线程访问同一个资源时,有且仅有一个线程能对资源进行操作。通常情况下原子操作可以通过互斥的访问方式来保证,如 Linux下的互斥锁(mutex)和 Windows 下的临界区(Critical Section)等。

        说白了原子操作就是不可中断的操作,要么被执行要不不被执行。

2.为什么需要原子操作?

        多线程编程的一个核心问题是如何在多个线程间安全地共享数据。传统的解决方案是使用锁(Lock)机制,例如互斥锁(Mutex)和读写锁(Read-Write Lock),但锁机制存在以下缺点:

        性能开销大:锁机制会引入额外的上下文切换和系统调用,导致性能下降。

        死锁风险:不当的锁管理可能导致死锁,进而影响程序的稳定性。

        复杂性高:在复杂的多线程环境中,正确管理锁非常困难,容易出错。

        原子操作通过硬件支持,提供了一种轻量级的同步机制,有效避免了上述问题。通过原子操作,我们可以确保在并发环境中对共享数据的访问是安全的,从而避免数据竞争和其他并发问题。

3.C++中的原子操作

        C++11引入了标准库头文件,其中包含了原子操作相关的类和函数。最常用的原子操作类是通过模板std::atomic<T>来定义,它封装了基本的原子操作,并提供了一组易于使用的接口。比如atomic_int64_t是通过typedef atomic<int64_t> atomic_int64_t实现的,使用时需包含头文件<atomic>。除了提供atomic_int64_t,还提供了其它的原子类型。常见的原子类型有:

原子类型名称对应内置类型
atomic_boolbool
atomic_charchar
atomic_ucharunsigned char
atomic_shortshort
atomic_ushortunsigned short
atomic_intint
atomic_uintunsigned int
atomic_longlong
atomic_ulongunsigned long
atomic_llonglong long 
atomic_ullongunsigned long long
atomic_char16_tchat16_t
atomic_char32_tchat32_t
atomic_wchar_twchar_t

        原子操作是平台相关的,原子类型能够实现原子操作是因为 C++11 对原子类型的操作进行了抽象,定义了统一的接口,并要求编译器产生平台相关的原子操作的具体实现。C++11 标准将原子操作定义为 atomic 模板类的成员函数,包括读(load)、写(store)、交换(exchange)等。对于内置类型而言,主要是通过重载一些全局操作符来完成的。比如对上文total+=i的原子加操作,是通过对operator+=重载来实现的。使用g++ 编译的话,在 x86_64 的机器上,operator+=() 函数会产生一条特殊的以 lock 为前缀的 x86_64 指令,用于控制总线及实现 x86_64平台上的原子性加法。下面我们通过几个示例代码来了解std::atomic的基本用法:

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>// 原子整数
std::atomic<int> atomicInt(0);void incrementAtomic() {for (int i = 0; i < 1000; ++i) {++atomicInt; // 原子加法}
}int main() {std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.push_back(std::thread(incrementAtomic));}for (auto& t : threads) {t.join();}std::cout << "Final value: " << atomicInt << std::endl; // 期望输出10000return 0;
}

        在上述示例中,我们使用std::atomic定义了一个原子整数atomicInt,并在多个线程中对其进行原子加法操作。由于原子操作的特性,无论有多少个线程同时执行,最终的结果都是正确的(10000),而不会出现数据竞争问题。

        有一个比较特殊的原子类型是 atomic_flag,因为 atomic_flag 与其他原子类型不同,它是无锁(lock_free)的,即线程对其访问不需要加锁,而其他的原子类型不一定是无锁的。因为atomic<T>并不能保证类型T是无锁的,另外不同平台的处理器处理方式不同,也不能保证必定无锁,所以其他的类型都会有 is_lock_free() 成员函数来判断是否是无锁的。atomic_flag 只支持 test_and_set() 以及 clear() 两个成员函数,test_and_set()函数检查 std::atomic_flag 标志,如果 std::atomic_flag 之前没有被设置过,则设置 std::atomic_flag 的标志;如果之前 std::atomic_flag 已被设置,则返回 true,否则返回 false。clear()函数清除 std::atomic_flag 标志使得下一次调用 std::atomic_flag::test_and_set()返回 false。可以用 atomic_flag 的成员函数test_and_set() 和 clear() 来实现一个自旋锁(spin lock):

#include <unistd.h>
#include <atomic>
#include <thread>
#include <iostream>std::atomic_flag lock = ATOMIC_FLAG_INIT;void func1() {while (lock.test_and_set(std::memory_order_acquire))    // 在主线程中设置为true,需要等待t2线程clear{std::cout << "func1 wait" << std::endl;}std::cout << "func1 do something" << std::endl;
}void func2() {std::cout << "func2 start" << std::endl;lock.clear();
}int main() {lock.test_and_set();             // 设置状态std::thread t1(func1);usleep(1);					 	//睡眠1usstd::thread t2(func2);t1.join();t2.join();return 0;
}

        以上代码中,定义了一个 atomic_flag 对象 lock,使用初始值 ATOMIC_FLAG_INIT 进行初始化,即处于 false 的状态。线程 t1 调用 test_and_set() 一直返回 true(因为在主线程中被设置过),所以一直在等待,而等待一段时间后当线程 t2 运行并调用了 clear(),test_and_set() 返回了 false 退出循环等待并进行相应操作。这样一来,就实现了一个线程等待另一个线程的效果。当然,可以封装成锁操作的方式,比如:

void Lock(atomic_flag& lock){ while ( lock.test_and_set()); }
void UnLock(atomic_flag& lock){ lock.clear(); }

这样一来,就可以通过Lock()和UnLock()的方式来互斥地访问临界区。

        自旋锁使用的时候虽然占用CPU资源(线程在获取锁时会一直循环检查锁是否可用,这会导致线程不断占用CPU时间),但是也有一定的优点:适用于锁被占用时间非常短暂的情况,因为在这种情况下,线程不需要长时间等待锁的释放,使用自旋锁可以避免线程切换带来的开销,提高性能。

4.原子操作使用及注意

        原子操作不能拷贝:只要是原子操作,都不能进行赋值和拷贝(因为调用了两个对象,破坏了原子性--拷贝构造和拷贝赋值都会将第一个对象的值进行读取,然后再写入另外一个。对于两个独立的对象,这里就有两个独立的操作了,合并这两个操作必定是不原子的。因此,操作就不被允许。如:

#include <iostream>
#include <atomic>int main() {std::atomic<int> atomicValue(10);// 不能进行赋值操作std::atomic<int> anotherAtomicValue = atomicValue; // 编译错误// 不能进行拷贝操作// std::atomic<int> copiedAtomicValue(atomicValue); // 编译错误return 0;
}

5.应用场景

     原子操作广泛应用于以下几个场景:

  1. 计数器:多线程环境下的计数操作,如网站访问量统计、资源请求计数等。

  2. 标志位:用于控制程序流转的标志位操作,如任务完成标志、自旋锁等。

  3. 锁自由数据结构:实现锁自由的队列、栈等数据结构,提高并发性能。

6.使用原子操作的最佳实践

        虽然原子操作在性能和安全性方面具有显著优势,但在使用过程中仍需注意以下几点:

        选择合适的数据类型:std::atomic支持的基本数据类型包括bool、整数类型、指针类型等。在实际应用中,应根据具体需求选择合适的数据类型。 

        了解内存序(Memory Order):C++原子操作提供了多种内存序选项,如memory_order_relaxed、memory_order_acquire、memory_order_release等。

        正确选择和使用内存序,有助于提高程序的性能和正确性。 

        避免过度使用原子操作:虽然原子操作性能优越,但不适用于所有场景。在复杂的同步需求中,仍需要结合使用锁机制。 

        深入理解内存序 

        内存序是C++原子操作中的一个重要概念,它控制了原子操作在多线程环境中的执行顺序。内存序主要有以下几种:

        memory_order_relaxed:不保证操作的顺序,仅保证操作的原子性。适用于对顺序没有严格要求的场景,如简单的计数器。

        memory_order_acquire:保证此操作之前的所有读操作都在此操作之前完成。适用于从共享变量读取数据的场景。

        memory_order_release:保证此操作之后的所有写操作都在此操作之后完成。适用于向共享变量写入数据的场景。 

        memory_order_acq_rel:同时具有memory_order_acquire和memory_order_release的特性。适用于读-改-写操作。

        memory_order_seq_cst:最严格的内存序,保证所有操作按顺序执行。适用于对顺序有严格要求的场景。 

        理解和正确使用内存序,可以在保证程序正确性的同时,最大限度地提高并发性能。

7.原子操作与锁机制的比较

        虽然原子操作在许多场景中比锁机制更高效,但两者各有优缺点,适用的场景也有所不同。

原子操作的优点:

        性能高:原子操作由硬件直接支持,通常比锁机制更高效。 

        避免死锁:由于不使用锁,原子操作避免了死锁问题。 

原子操作的缺点:

        适用范围有限:原子操作适用于简单的同步场景,对于复杂的同步需求,可能需要借助锁机制。 

        代码复杂性:在一些情况下,使用原子操作的代码可能比使用锁机制的代码更复杂。 

锁机制的优点:

        适用范围广:锁机制可以处理复杂的同步需求,如保护复杂的数据结构、实现复杂的同步逻辑等。 

        代码简单:在某些情况下,使用锁机制的代码比使用原子操作的代码更简单直观。 

锁机制的缺点:

        性能开销大:锁机制会引入额外的上下文切换和系统调用,导致性能下降。

        死锁风险:不当的锁管理可能导致死锁,影响程序的稳定性。

8.总结

        C++原子操作提供了一种高效、安全的多线程数据访问方式,在性能和安全性方面具有显著优势。通过合理使用std::atomic类和内存序选项,开发者可以编写出高效、可靠的多线程程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/25848.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux介绍-以CentOS和Ubuntu为例---linux入门01

Linux是一种广泛使用的开源操作系统&#xff0c;以其稳定性、安全性和灵活性而闻名。本文将详细介绍Linux操作系统&#xff0c;重点讨论CentOS和Ubuntu这两个常见的发行版&#xff0c;并比较它们的特点、适用场景以及在实际应用中的优劣。 01 Linux操作系统概述 1.1 Linux的起…

Docker面试整理-如果Docker容器无法启动,你会如何诊断和解决问题?

当 Docker 容器无法启动时,可以通过一系列步骤来诊断和解决问题。这些步骤有助于确定问题的根源并采取相应的解决措施。以下是处理 Docker 容器启动问题的一般流程: 1. 检查容器日志 命令:docker logs <container-id或container-name>此命令将显示容器的输出日志,可能…

深度学习与人工智能

深度学习&#xff0c;是一种特殊的人工智能&#xff0c;他与人工智能及机器学习的关系如下&#xff1a; 近些年来&#xff0c;基于人工神经网络的机器学习算法日益盛行起来&#xff0c;逐渐呈现出取代其他机器学习算法的态势&#xff0c;这主要的原因是因为人工神经网络中有一中…

代码随想录算法训练营第17天|二叉树

平衡二叉树 这种开销太大了&#xff0c;最好是能够在获得子树高的递归中同时判断子树是否平衡&#xff0c;但是我纠结的是递归的输出是布尔类型&#xff0c;而不是数字类型&#xff0c;怎么在迭代子树是否平衡时计算子树的高度呢&#xff08;迭代可以计算&#xff0c;但是我想…

php高级之框架源码、宏扩展原理与开发

在使用框架的时候我们经常会看到如下代码 类的方法不会显示地声明在代码里面&#xff0c;而是通过扩展的形式后续加进去&#xff0c;这么做的好处是可以降低代码的耦合度、保证源码的完整性、团队开发的时候可以分别写自己的服务去扩展类&#xff0c;减少代码冲突等等。我自己…

C语言之常用字符串函数总结、使用和模拟实现

文章目录 目录 一、strlen 的使用和模拟实现 二、strcpy 的使用及模拟实现 三、strcat 的使用和模拟实现 四、strcmp 的使用和模拟实现 五、strncpy 的使用和模拟实现 六、strncat 的使用和模拟实现 七、strncmp 的使用和模拟实现 八、strstr 的使用和模拟实现 九、st…

使用Python批量处理Excel的内容

正文共&#xff1a;1500 字 10 图&#xff0c;预估阅读时间&#xff1a;1 分钟 在前面的文章中&#xff08;如何使用Python提取Excel中固定单元格的内容&#xff09;&#xff0c;我们介绍了如何安装Python环境和PyCharm工具&#xff0c;还利用搭好的环境简单测试了一下ChatGPT提…

java程序提供默认实现策略,并支持自定义实现策略的一种方式?并如何避雷?

java程序提供默认实现策略&#xff0c;并支持自定义实现策略的一种方式&#xff1f;并如何避雷&#xff1f; 方案&#xff1f; 说明&#xff1a; 当前是基于自定义策略注册由工具类提供&#xff0c;且默认实现策略全局可访问的前提下进行探讨&#xff0c;其他场景也可进行参…

Java 数据类型 -- Java 语言的 8 种基本数据类型、字符串与数组

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 004 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…

如何秒杀系统架构设计

原文路径:https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e5%a6%82%e4%bd%95%e8%ae%be%e8%ae%a1%e4%b8%80%e4%b8%aa%e7%a7%92%e6%9d%80%e7%b3%bb%e7%bb%9f/00%20%e5%bc%80%e7%af%87%e8%af%8d%20%e7%a7%92%e6%9d%80%e7%b3%bb%e7%bb%9f%e6%9e%b6%e6%9e%84%e8%ae%be%e8%ae%…

纳什均衡:博弈论中的运作方式、示例以及囚徒困境

文章目录 一、说明二、什么是纳什均衡&#xff1f;2.1 基本概念2.2 关键要点 三、理解纳什均衡四、纳什均衡与主导策略五、纳什均衡的例子六、囚徒困境七、如何原理和应用7.1 博弈论中的纳什均衡是什么&#xff1f;7.2 如何找到纳什均衡&#xff1f;7.3 为什么纳什均衡很重要&a…

素数的无穷大的证明

素数的无穷大——欧几里得的证明 文章目录 一、说明二、欧几里得证据三、哥德巴赫对素数无穷性的证明&#xff08;1730&#xff09;四、Frstenberg 对素数无穷性的证明(1955)五、库默尔对欧几里得证明的重述 一、说明 众所周知&#xff0c;素数是无限多的。然而&#xff0c;两…

运维一个宝塔面板的php项目的艰辛历程【解决了http3,ssl,quic】

在这个项目的环境 使用了宝塔面板 有4个php:php5.6,php7.3,php7.4,php8.0 nignx为1.20版本 升级计划&#xff1a; 升级nginx1.26.0版本&#xff0c;添加上http3协议&#xff0c;添加ssl证书 遇到的问题&#xff1a; 升级nginx1.26版本后 无法打开php5.6的后台 原因&#xff…

【知识点】nullptr 和 NULL

在 C 中&#xff0c;nullptr 和 NULL 都可以用来表示空指针&#xff0c;但是它们之间有一些重要的区别。这些区别涉及到类型安全性、代码可读性和在不同版本的 C 中的使用情况。 NULL NULL 是一个宏&#xff0c;通常定义为 0 或 (void*)0&#xff0c;它最初是在 C 语言中引入…

vmware-17虚拟机安装教程,安装linux centos系统

下载VMware 1.进入VMware官网&#xff1a;https://www.vmware.com/sg/products/workstation-pro.html 2.向下翻找到&#xff0c;如下界面并点击“现在安装” 因官网更新页面出现误差&#xff0c;现提供vmware17安装包网盘链接如下&#xff1a; 链接&#xff1a;https://pan.b…

Vue17-条件渲染

一、使用v-show属性做条件渲染 控制元素的显示和隐藏 v-show里面也能是表达式&#xff0c;只要表达式的值是boolean就行。 或者 当时结构还在&#xff1a; 二、使用v-if属性做条件渲染 结构也不在了 三、示例 方式一&#xff1a; 方式二&#xff1a; 当元素有很高的切换频率&am…

从0开始学人工智能测试节选:Spark -- 结构化数据领域中测试人员的万金油技术(四)

上一章节我们了解了 shuffle 相关的概念和原理后其实可以发现一个问题&#xff0c;那就是 shuffle 比较容易造成数据倾斜的情况。 例如上一节我们看到的图&#xff0c;在这批数据中&#xff0c;hello 这个单词的行占据了绝大部分&#xff0c;当我们执行 groupByKey 的时候触发了…

刚刚❗️德勤2025校招暑期实习测评笔试SHL测评题库已发(答案)

&#x1f4e3;德勤 2024暑期实习测评已发&#xff0c;正在申请的小伙伴看过来哦&#x1f440; ㊙️本次暑期实习优先考虑2025年本科及以上学历的毕业生&#xff0c;此次只有“审计及鉴定”“税务与商务咨询”两个部门开放了岗位~ ⚠️测评注意事项&#xff1a; &#x1f44…

pdf分割为bmp

import fitz # PyMuPDF import os from PIL import Imagedef convert_pdf_to_bmp(pdf_path, output_folder):"""将单个PDF文件的每一页转换为BMP格式的图像。:param pdf_path: PDF文件的路径。:param output_folder: 保存BMP图像的输出文件夹路径。""…

简单了解java中的正则表达式

正则表达式 1、正则表达式认识 正则表达式通常用来校验&#xff0c;检查字符串是否符合规则&#xff0c;由一些特定的字符组成的字符串校验规则&#xff0c;就称之为正则表达式。 2、正则表达式能干啥&#xff1f; 正则表达式只能针对字符串格式进行校验&#xff0c;所以它…