深入理解C++11原子操作:從內存模型到無鎖編程

文章目錄

    • C++并發編程的新紀元
    • 內存模型基礎:可見性與有序性
      • 數據競爭的根源
      • happens-before關系
      • memory_order枚舉詳解
        • 1. memory_order_relaxed
        • 2. memory_order_acquire/memory_order_release
        • 3. memory_order_seq_cst
    • 原子操作詳解
      • std::atomic模板
      • 核心原子操作
        • 1. 讀取與存儲
        • 2. 交換操作
        • 3. 增減操作
        • 4. 比較并交換(CAS)
      • atomic_flag
      • 初始化注意事項
    • 實戰案例
      • 案例1:原子計數器 vs 互斥量計數器
      • 案例2:基于CAS的無鎖棧
      • 案例3:原子操作實現簡單的讀寫鎖
    • 陷阱與最佳實踐
      • 內存序誤用導致的隱蔽bug
      • lock-free的誤區
      • ABA問題
      • 何時選擇原子操作
    • 總結

最近在寫一個服務程序,多線程下的各種操作相比于單線程而言,需要考慮的細節是冪級提升呀!同時由于對多線程的一些函數使用不夠熟悉,原理不清楚,導致自己很難寫服務端!

C++并發編程的新紀元

在C++11之前,編寫跨平臺的多線程程序意味著要面對POSIX線程與Windows API的差異,甚至同一平臺下不同編譯器的行為不一致。2011年標準的發布徹底改變了這一局面,其中并發支持庫(Concurrency support library)的引入標志著C++正式進入多線程時代。本文將聚焦于該庫的核心組件之一——原子操作(Atomic operations),探討其底層原理、實際應用及避坑指南。

原子操作作為無鎖編程的基礎,為高性能并發提供了可能,但也因其對內存模型的依賴而成為最容易誤用的特性之一。與互斥量(如std::mutex)通過阻塞線程實現同步不同,原子操作通過硬件級別的指令保證操作的不可分割性,從而避免了線程上下文切換的開銷。然而,這種性能優勢是以復雜性為代價的——開發者必須深入理解CPU內存模型和編譯器優化才能正確使用。

內存模型基礎:可見性與有序性

數據競爭的根源

多線程環境下,數據競爭(Data Race)是最常見的bug來源。考慮以下代碼:

int counter = 0;void increment() {for (int i = 0; i < 100000; ++i) {counter++; // 非原子操作,存在數據競爭}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Counter: " << counter << std::endl; // 結果可能小于200000return 0;
}

這段代碼看似簡單,卻可能輸出小于200000的結果。原因在于counter++并非原子操作,它包含三個步驟:讀取當前值、加1、寫回新值。當兩個線程同時執行時,可能出現"讀-讀-寫-寫"的情況,導致其中一個增量操作被覆蓋。

更深層次的原因在于現代CPU的優化機制:

  • 亂序執行:CPU為提高效率可能調整指令執行順序
  • 緩存優化:每個CPU核心有獨立緩存,變量修改可能暫存于緩存而非立即寫入主存
  • 編譯器優化:編譯器可能對代碼進行重排,導致實際執行順序與源碼順序不同

happens-before關系

C++11引入了"happens-before"關系來定義操作間的可見性。如果操作A happens-before操作B,則A的結果對B可見。關鍵規則包括:

  • 同一線程內,按源碼順序執行的操作存在happens-before關系
  • 解鎖操作happens-before后續的加鎖操作
  • 原子操作的內存序約束可建立happens-before關系

memory_order枚舉詳解

C++11定義了六種內存序(memory_order),但實際開發中常用的有三種:

1. memory_order_relaxed

僅保證操作本身的原子性,不提供任何同步或順序約束。適用于純計數器等場景:

std::atomic<int> counter(0);
counter.fetch_add(1, std::memory_order_relaxed); // 僅保證計數正確,不影響其他操作順序
2. memory_order_acquire/memory_order_release
  • release:當前線程的所有寫操作在其他線程對同一原子變量的acquire操作前可見
  • acquire:可見所有在release操作前的寫操作

典型應用是生產者-消費者模型:

std::atomic<bool> data_ready(false);
int shared_data;// 生產者線程
void producer() {shared_data = 42; // 1. 寫入數據data_ready.store(true, std::memory_order_release); // 2. 發布信號
}// 消費者線程
void consumer() {while (!data_ready.load(std::memory_order_acquire)); // 3. 獲取信號std::cout << shared_data; // 4. 安全讀取數據,保證看到42
}
3. memory_order_seq_cst

最強的內存序,保證所有線程看到的操作順序一致,如同在單個全局序列中執行。但性能開銷最大,僅在需要全局同步時使用:

std::atomic<int> seq_cst_var(0);
seq_cst_var.store(1, std::memory_order_seq_cst); // 全局可見的存儲操作

原子操作詳解

std::atomic模板

std::atomic是一個模板類,支持基本類型(bool、char、int、long、指針等)的原子操作。對于用戶自定義類型,需滿足可平凡復制(Trivially Copyable)要求。

std::atomic<int> a(0);          // 整數原子變量
std::atomic<bool> flag(false);  // 布爾原子變量
std::atomic<MyStruct*> ptr(nullptr); // 指針原子變量

可通過atomic_is_lock_free檢查操作是否真正無鎖:

std::cout << std::boolalpha;
std::cout << "int is lock-free: " << std::atomic<int>{}.is_lock_free() << std::endl;
std::cout << "double is lock-free: " << std::atomic<double>{}.is_lock_free() << std::endl;

注意:在某些平臺上,double類型的原子操作可能不是無鎖的,會內部使用互斥量。

核心原子操作

1. 讀取與存儲
std::atomic<int> x(0);
int a = x.load(std::memory_order_relaxed); // 讀取
x.store(5, std::memory_order_relaxed);     // 存儲
x = 10; // 隱式使用memory_order_seq_cst,不推薦
2. 交換操作
std::atomic<int> x(5);
int old_val = x.exchange(10); // 原子交換,返回舊值(5)
3. 增減操作
std::atomic<int> count(0);
count.fetch_add(1); // 原子加1,返回舊值
count.fetch_sub(1); // 原子減1,返回舊值
count += 1; // 隱式使用memory_order_seq_cst
4. 比較并交換(CAS)

CAS是無鎖編程的基石,操作邏輯為:“如果當前值等于預期值,則替換為新值,否則不做修改”。有強弱兩個版本:

std::atomic<int> val(10);
int expected = 10;// 強版本:保證在val != expected時返回false
bool success = val.compare_exchange_strong(expected, 20);// 弱版本:可能偽失敗(即使val == expected也可能返回false),需配合循環使用
do {expected = val.load();// 計算新值
} while (!val.compare_exchange_weak(expected, new_value));

弱版本在某些CPU架構上性能更好,適合循環場景;強版本適合單次嘗試。

atomic_flag

std::atomic_flag是C++11中唯一保證lock-free的原子類型,僅支持test_and_set和clear操作:

std::atomic_flag flag = ATOMIC_FLAG_INIT; // 必須用此宏初始化// 實現簡單自旋鎖
class SpinLock {
private:std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:void lock() {while (flag.test_and_set(std::memory_order_acquire));}void unlock() {flag.clear(std::memory_order_release);}
};

初始化注意事項

C++11中原子變量的初始化需特別注意:

// 正確初始化方式
std::atomic<int> a{0};                  // C++11起支持
std::atomic<int> b(ATOMIC_VAR_INIT(0)); // 兼容C風格宏
std::atomic<int> c;
atomic_init(&c, 0);                     // 動態初始化// 錯誤方式
std::atomic<int> d = 0; // 拷貝初始化被禁用

實戰案例

案例1:原子計數器 vs 互斥量計數器

對比原子操作與互斥量在多線程計數場景下的性能:

#include <atomic>
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>const int THREADS = 10;
const int ITERATIONS = 1000000;// 原子計數器
void atomic_counter_test() {std::atomic<int> counter(0);auto start = std::chrono::high_resolution_clock::now();std::vector<std::thread> threads;for (int i = 0; i < THREADS; ++i) {threads.emplace_back([&]() {for (int j = 0; j < ITERATIONS; ++j) {counter.fetch_add(1, std::memory_order_relaxed);}});}for (auto& t : threads) t.join();auto end = std::chrono::high_resolution_clock::now();std::cout << "Atomic counter: " << counter << " in "<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()<< "ms" << std::endl;
}// 互斥量計數器
void mutex_counter_test() {int counter = 0;std::mutex mtx;auto start = std::chrono::high_resolution_clock::now();std::vector<std::thread> threads;for (int i = 0; i < THREADS; ++i) {threads.emplace_back([&]() {for (int j = 0; j < ITERATIONS; ++j) {std::lock_guard<std::mutex> lock(mtx);counter++;}});}for (auto& t : threads) t.join();auto end = std::chrono::high_resolution_clock::now();std::cout << "Mutex counter: " << counter << " in "<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()<< "ms" << std::endl;
}int main() {atomic_counter_test();mutex_counter_test();return 0;
}

在筆者的4核CPU上,原子計數器通常比互斥量快3-5倍,且線程數越多差距越明顯。

案例2:基于CAS的無鎖棧

#include <atomic>
#include <iostream>template<typename T>
class LockFreeStack {
private:struct Node {T data;Node* next;Node(const T& data) : data(data), next(nullptr) {}};std::atomic<Node*> head;public:LockFreeStack() : head(nullptr) {}// 禁止拷貝構造和賦值LockFreeStack(const LockFreeStack&) = delete;LockFreeStack& operator=(const LockFreeStack&) = delete;~LockFreeStack() {while (Node* old_head = head.load()) {head.store(old_head->next);delete old_head;}}void push(const T& data) {Node* new_node = new Node(data);new_node->next = head.load(std::memory_order_relaxed);// 使用弱CAS循環處理可能的偽失敗while (!head.compare_exchange_weak(new_node->next, new_node,std::memory_order_release,  // 成功時的內存序std::memory_order_relaxed)) // 失敗時的內存序{}}bool pop(T& result) {Node* old_head = head.load(std::memory_order_relaxed);// 循環直到CAS成功或棧為空while (old_head && !head.compare_exchange_weak(old_head, old_head->next,std::memory_order_acquire,  // 成功時的內存序std::memory_order_relaxed)) // 失敗時的內存序{}if (!old_head) return false;result = old_head->data;delete old_head;return true;}bool empty() const {return head.load(std::memory_order_relaxed) == nullptr;}
};int main() {LockFreeStack<int> stack;// 多線程pushstd::thread t1([&]() {for (int i = 0; i < 1000; ++i) {stack.push(i);}});// 多線程popstd::thread t2([&]() {int val;for (int i = 0; i < 500; ++i) {while (!stack.pop(val));std::cout << "Popped: " << val << std::endl;}});t1.join();t2.join();return 0;
}

案例3:原子操作實現簡單的讀寫鎖

#include <atomic>
#include <thread>class ReadWriteLock {
private:std::atomic<int> readers;std::atomic<bool> writer;public:ReadWriteLock() : readers(0), writer(false) {}void read_lock() {// 自旋等待寫鎖釋放while (writer.load(std::memory_order_acquire)) {std::this_thread::yield();}readers.fetch_add(1, std::memory_order_relaxed);}void read_unlock() {readers.fetch_sub(1, std::memory_order_relaxed);}void write_lock() {bool expected = false;// 自旋等待寫鎖并嘗試獲取while (!writer.compare_exchange_weak(expected, true, std::memory_order_acquire, std::memory_order_relaxed)) {expected = false;std::this_thread::yield();}// 等待所有讀者完成while (readers.load(std::memory_order_relaxed) > 0) {std::this_thread::yield();}}void write_unlock() {writer.store(false, std::memory_order_release);}
};

陷阱與最佳實踐

內存序誤用導致的隱蔽bug

最常見的錯誤是過度使用memory_order_relaxed。例如:

// 錯誤示例
std::atomic<bool> ready(false);
int data = 0;void producer() {data = 42;ready.store(true, std::memory_order_relaxed); // 錯誤:應使用release
}void consumer() {while (!ready.load(std::memory_order_relaxed)); // 錯誤:應使用acquireassert(data == 42); // 可能失敗!
}

由于使用了relaxed內存序,編譯器可能重排指令,導致data的寫入在ready之后執行,消費者可能看到ready為true但data仍為0。

lock-free的誤區

并非所有原子操作都是lock-free的。例如:

std::atomic<long double> ld; // 通常不是lock-free的

應始終使用is_lock_free()檢查:

if (!std::atomic<MyType>{}.is_lock_free()) {// 回退到互斥量實現
}

ABA問題

CAS操作可能面臨ABA問題:

std::atomic<Node*> ptr;// 線程1
Node* A = ptr.load();
// 線程2修改ptr從A到B再到A
// 線程1執行CAS,雖然ptr仍為A,但A可能已被修改
ptr.compare_exchange_strong(A, new_node);

解決方案是引入版本號:

struct TaggedPtr {Node* ptr;uint64_t version;
};
std::atomic<TaggedPtr> tagged_ptr;

何時選擇原子操作

  • 適用場景:簡單計數器、標志位、無鎖數據結構
  • 不適用場景:復雜狀態轉換、需要多操作原子性(此時應使用互斥量)

經驗法則:優先使用高級同步原語(如std::mutex、std::condition_variable),僅在性能關鍵路徑且操作簡單時才考慮原子操作。

總結

C++11原子操作為并發編程提供了強大的工具,但也要求開發者深入理解內存模型。正確使用原子操作可以顯著提升性能,但錯誤使用會導致難以調試的并發bug。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/87277.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/87277.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/87277.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

DQL-1-基礎查詢

基礎查詢 DQL-1-基礎查詢 基礎查詢DQL - 介紹DQL - 語法DQL - 基本查詢案例 DQL - 介紹 SQL 英文全稱是 Data Query Language, 數據查詢語言, 用來查詢數據庫中表的記錄 查詢關鍵字: SELECT DQL - 語法 SELECT 字段列表FROM 表名列表WHERE條件列表GROUP BY分組字段列表HAVI…

Prompt 精通之路(七)- 你的終極 AI 寶典:Prompt 精通之路系列匯總

你的終極 AI 寶典&#xff1a;Prompt 精通之路系列匯總 標簽&#xff1a; #Prompt指南 #AI學習資源 #速查手冊 #ChatGPT #系列總結 &#x1f680; Prompt 精通之路&#xff1a;系列文章導航 第一篇&#xff1a;AI 時代的新語言&#xff1a;到底什么是 Prompt&#xff1f;為什么…

P27:RNN實現阿爾茨海默病診斷

&#x1f368; 本文為&#x1f517;365天深度學習訓練營 中的學習記錄博客&#x1f356; 原作者&#xff1a;K同學啊 一、過程解讀 PyTorch 實戰&#xff1a;阿爾茨海默病數據預測模型 今天&#xff0c;我將帶大家一起探索一個基于 PyTorch 的深度學習小項目——利用 RNN 模…

HakcMyVM-Arroutada

信息搜集 主機發現 ┌──(kali?kali)-[~] └─$ nmap -sn 192.168.21.0/24 Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-01 07:13 EDT Nmap scan report for 192.168.21.11 Host is up (0.00062s latency). MAC Address: 08:00:27:4E:CC:FB (PCS Systemtechnik/Or…

TEXT Submitting Solutions

前言 USACO 訓練項目配備了一個自動評分系統&#xff0c;用于批改你的作業題目。你可以直接在題目頁面提交你的程序&#xff1b;系統會對程序進行編譯和評分&#xff0c;幾秒鐘內就能將結果反饋給你。 支持的語言有 C、C&#xff08;含 C11 和 C14&#xff09;、PASCAL、Pyth…

Reactor 瞬態錯誤

在響應式編程中&#xff0c;retryWhen 操作符通過 RetrySignal 接口提供了對重試行為的精細控制&#xff0c;特別是在處理 瞬態錯誤&#xff08;transient errors&#xff09; 時。瞬態錯誤是指那些在一段時間內發生&#xff0c;但隨后會自行恢復的錯誤&#xff0c;例如網絡請求…

基于 SpringBoot+Vue.js+ElementUI 的小型超市商品管理系統設計與實現7000字論文設計

摘要 本論文設計并實現了一個基于 SpringBoot、Vue.js 和 ElementUI 的小型超市商品管理系統。該系統旨在為小型超市提供一個高效、便捷的商品管理解決方案&#xff0c;實現商品信息的錄入、查詢、修改、刪除等功能&#xff0c;同時支持庫存管理、銷售統計等業務需求。論文首先…

Kerberos 認證協議解析

文章目錄 概述核心概念認證流程階段一&#xff1a;Client -> AS&#xff0c;獲取 TGT階段二&#xff1a;Client -> TGS&#xff0c;獲取服務票據階段三&#xff1a;Client -> Server&#xff0c;請求服務 核心安全機制優缺點分析優勢局限性 實踐與排錯關鍵配置 (krb5.…

【設計模式07】適配器

前言 實現目標&#xff0c;組合源&#xff0c;寫個適配方法&#xff0c;適用于沒辦法改變源&#xff0c;但又想實現目標類。我暫時還沒使用到過&#xff0c;但感覺用處還是蠻大的 UML類圖 代碼示例 package com.sw.learn.pattern.C_structre.a_adapter;public class Main {//…

SPI、I2C和UART三種串行通信協議的--------簡單總結

目錄 一、3種協議的對比二、典型應用場景三、選型建議 以下是SPI、I2C和UART三種串行通信協議的對比分析及適用場景總結&#xff1a; 一、3種協議的對比 . 對比其他接口 特性ICSPIUART信號線數量2&#xff08;SCL SDA&#xff09;4&#xff08;SCK MOSI MISO SS/CS&…

VUE admin-element 后臺管理系統三級菜單實現緩存

VUE admin-element 后臺管理系統三級菜單實現緩存 框架無法直接實現三級菜單頁面緩存&#xff0c;原因是由于直接緩存時沒有把上級路由文件名稱緩存進去&#xff0c;所以在框架基礎上參考部分文章進行了一些改造 菜單文件&#xff0c;三級菜單引用文件路徑修改&#xff0c;在…

【筆記】Windows 安裝 Gemini CLI

2025 年 07 月 02 日 Windows 安裝 Gemini CLI google-gemini/gemini-cli&#xff1a;一個開源的 AI 代理&#xff0c;可將 Gemini 的強大功能直接引入您的終端。 一、前置條件 系統要求&#xff1a;Windows 7 及以上版本。 Node.js 環境&#xff1a;Gemini CLI 基于 Node.js …

transformers==4.42.0會有一個BUG

transformers4.42.0版本下&#xff0c;自動安裝模型時出現一個BUG&#xff08;自動從Hugging Faces上下載&#xff09;。 2025-07-02 14:07:08,641 - __main__ - ERROR - 模型加載失敗: Failed to import transformers.models.llama.tokenization_llama_fast because of the f…

Spring-解決IDEA中無法創建JDK17一下的SpringBoot項目

目錄 一.直接創建 二.修改Server URL為https://start.aliyun.com 一.直接創建 目前如果使用https://start.spring.io&#xff08;Spring官方源&#xff09;,已經沒有辦法直接創建JDK17一下的項目了&#xff1a; 如果想要創建JDK8的項目&#xff0c;可以先通…

人工智能-基礎篇-13-基礎應用篇-2~~模型項目開發流程--從0到1創建類似DeepSeek語言模型,應該怎么做?

1、前期準備 1、明確目標與需求分析 應用場景定義&#xff1a;首先需要明確你的模型將用于哪些場景&#xff0c;比如對話系統、文本生成、代碼輔助等。性能指標設定&#xff1a;確定關鍵性能指標(KPI)&#xff0c;如準確率、響應時間、支持的語言種類等。 2、組建團隊 機器…

本周滬鋁想法

核心邏輯&#xff1a;低庫存支撐與淡季需求疲軟博弈&#xff0c;宏觀情緒助推高位震蕩 一、成本下移 VS 價格韌性? 成本端與價格表現呈現出不同態勢。成本端方面&#xff0c;氧化鋁現貨價格在本周持續下跌&#xff0c;山東地區均價降至 3090 元 / 噸&#xff0c;環比下降 1.…

【網絡】SSL/TLS介紹

一、SSL/TLS 概述 SSL&#xff08;Secure Socket Layer&#xff09; &#xff1a; 最初由網景&#xff08;Netscape&#xff09;開發&#xff0c;用于在客戶端和服務器之間建立安全的加密連接&#xff0c;防止數據被竊取或篡改。后來逐步演進&#xff0c;最終被 TLS 取代。 TL…

TLF35584

13、SPI串行外設接口 13.1 介紹 主要功能 SPI 總線是?種以全雙工模式運行的同步串行數據鏈路。TLF35584 在從機模式下進行通信&#xff0c;其中主機(μC)啟動數據幀。TLF35584應該通過專用片選線進行尋址。這允許其他從設備連接到SPI總線。 數據傳輸 開始通信&#xff0c;μ…

word中如何保存高清圖片,并保存為高質量的pdf文件(圖像不失真)

word中如何保存高清圖片 打開word,選擇&#xff0c;選項&#xff0c;高級選項&#xff0c;選擇不壓縮文件中的圖像并保持分辨率高保真 將word保存為高質量的pdf文件 不用另存為或者導出 選擇文件&#xff0c;選擇打印&#xff1a; 選擇中間都打印出pdf即可。 然后再選擇打印…

Day03_C語言IO進程線程

01.思維導圖 02.創建一個進程扇 #include <25051head.h> int main(int argc, const char *argv[]) {pid_t pid;int i;for(i0;i<4;i){pidfork();if(pid0){//printf("子進程:pid[%d]\n",pid);printf("子進程%d:子進程pid[%d],父進程pid[%d]\n",i1,g…