上一篇文章講了C++多線程的基礎知識, 今天我們來講講原子操作, 原子操作也是C++多線程的主要內容
什麼是原子操作
什麼是原子操作呢? 就是一個操作執行像原子一樣不可再分割, 在多線程環境中就不會被其他線程打斷, 因而就會保證某個操作執行的連續性和完整性
也就是説,一個操作要麼全部執行完畢,要麼完全不執行
如果沒有原子操作
如果沒有原子操作的話, 在多線程環境中會導致數據讀寫混亂, 下面讓我們用代碼舉個經典的反例看看
#include <iostream>
#include <thread>
#include <vector>
int count = 0; // 普通整型變量
// 線程執行的函數:count自增1000次
void increment() {
for (int i = 0; i < 1000; ++i) {
count++; // 看似簡單的自增,實則暗藏危機
}
}
int main() {
std::vector<std::thread> threads;
// 創建10個線程
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
// 等待所有線程執行完畢
for (auto& t : threads) {
t.join();
}
std::cout << "最終count值:" << count << std::endl; // 結果遠小於10000
return 0;
}
上述代碼的基本內容是讓10個線程各自給count加1000次,但實際運行成果總小於1000
是什麼原因呢?
因為count++這個操作,在CPU層面被拆成三步:
- 從內存中讀取count的值存到寄存器
- 寄存器中的值+1
- 把新值寫回內存
因為這三步不是"不可分割的",所以容易造成讀寫混亂,線程A剛讀完count,還沒有在寄存器+1,線程B就插進來讀了同一個值,然後各自+1寫回,相當於白加了一次,導致數據被覆蓋, 這就顯得原子操作是多麼重要了, 下面讓我們來看看原子操作是怎麼使用的~
原子操作的使用
核心工具
std::atomic:它是 C++11 開始提供的原子類型模板,不需要手動加鎖,也可以實現線程安全
常用方法
以int類型變量count為例
- 讀值
count.load() - 寫值
count.store(10) - 自增
count.fetch_add(1) - 交換
count.exchange(20)
下面讓我們用原子操作來修復上述的例子
#include <iostream>
#include <thread>
#include <vector>
#include <atomic> // 包含原子操作的頭文件
std::atomic<int> count = 0; // 原子整型變量
// 線程執行的函數:count自增1000次
void increment() {
for (int i = 0; i < 1000; ++i) {
// 方式1:使用fetch_add原子自增
count.fetch_add(1);
// 方式2:直接用count++(重載後的原子操作,效果一樣)
// count++;
}
}
int main() {
std::vector<std::thread> threads;
// 創建10個線程
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
// 等待所有線程執行完畢
for (auto& t : threads) {
t.join();
}
// 原子變量可以直接輸出,底層會調用load()
std::cout << "最終count值:" << count << std::endl; // 結果一定是10000
return 0;
}
運行結果如下:
這次運行後,count的值10000,使用了原子操作後不會被其他線程打斷
這就是原子操作在多線程場景中的使用意義
總結
- 原子操作的核心是
不可分割,能避免多線程下的數椐競爭 - 原子操作通過
std::atomic實現
那麼我們在上一篇多線程基礎的文章中講到了互斥鎖,也可以解決數據競態條件的問題,那麼原子操作和互斥鎖又有什麼區別和聯繫呢?他們誰的性能更強呢?如果點贊量超過1,我下期將會重點講講原子操作和互斥鎖之間的關係~
希望這篇文章能幫你搞懂 C++ 原子操作!如果覺得有用,別忘了點贊喲
或者關注本人,我將會持續分享高質量的內容~