文章目錄
- 前言
- C++中的atomic
- 一個簡單的自增運算
- 通過加鎖把自增變為原子操作
- 使用atomic來保證自增的原子性
- 總結
前言
提到atomic這個詞,你首先想到的是什麼呢?作為一個長時間混迹于程式設計世界的菜鳥,我首先想到的一個詞是“原子性”,接着飛入腦海的是 “ACID” 這個縮寫詞組,既然提到了
ACID
我們就來簡單的複習一下。
ACID
是指事務管理的4個特性,常見于資料庫操作管理中,它們分别是:原子性,一緻性,隔離性和持久性。
- 原子性(Atomicity)是指事務是一個不可分割的工作機關,事務中的操作要麼都執行,要麼都不執行。
- 一緻性(Consistency)是指事務前後資料的完整性必須保持一緻,完全符合邏輯原運算。
- 隔離性(Isolation)是指在多個使用者并發通路資料庫時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作資料所幹擾,多個并發事務之間要互相隔離,無需感覺其他事務的存在。
- 持久性(Durability)是指一個事務一旦被送出,它對資料庫中資料的改變就是永久性的,接下來即使資料庫發生故障也不應該對資料造成損壞。
C++中的atomic
原子(atom)是在化學反應中不可分割基本微粒,而程式設計世界中的原子性也是取自這裡的不可分割的含義,不可分割與事務管理中的原子性含義一緻,指的是一個操作或者一系列操作隻能全都執行或者都不執行,不會隻執行其中一部分,那麼C++11中引入atomic有什麼用?不使用atomic能不能保證原子性呢?
其實C++11中引入atomic主要還是降低了程式設計的複雜度,如果不使用atomic同樣可以使用鎖機制來保證原子性,接下來我們來看看為什麼需要原子性。
一個簡單的自增運算
i++
是個再簡單不過的語句了,我們可以使用它來做一個計數器,每次自增加1,假設我們有一個工程項目有兩條商品生産的流水線,每個流水線生産出一件商品則需要計數器加1,這時我們用兩個線程來模拟兩條流水線,每個線程函數來調用自增的計數器,來看看有什麼問題?
#include <iostream>
#include <thread>
int i = 0;
void func(int n)
{
for (int k = 0; k < n; k++) i++;
}
int main(int argc, char* argv[])
{
int n = argc > 1 ? atoi(argv[1]) : 100;
std::thread t1(func, n);
std::thread t2(func, n);
t1.join();
t2.join();
std::cout << "i=" << i << std::endl;
}
測試代碼如上所示,執行
g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 10
指令編譯并運作得到結果
i=20
,貌似很正常,一共兩個線程,每個線程執行10次自增操作,結果就應該是20啊,先别太早下結論,增大自增範圍試試。
執行
g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 100000
得到結果
i=112831
,多次執行發現每次運作結果都不太一樣,但是資料範圍在
100000~200000
,這就有些奇怪了,每個線程執行循環執行一條語句,那麼程式結果應該等于
2n
才對,為什麼結果總是小于
2n
呢,難道有些循環沒有執行?
其實不是這樣的,
i++
從C++語言的層面來看确實是一條語句,但是真正再和機器打交道時一般會解釋成類似于下面這樣3條彙編指令:
// x86 msvc v19.latest
mov eax, DWORD PTR _i$[ebp]
add eax, 1
mov DWORD PTR _i$[ebp], eax
3條指令的含義可以了解為讀取、自增,設定共三步,既然不是真正的一條語句,那麼在多線的環境下就會生語句的交叉執行,比如第一個線程執行讀取變量i的值之後,第二個線程也讀取了變量i的值,這樣兩個線程都進行後續的自增和設定指令後,會發現比預期的值少了一個,這種情況在循環次數較多時尤為明顯。
通過加鎖把自增變為原子操作
既然每個自增操作可能會被分解成3條指令,那麼我們可以加鎖來将3條指令捆綁,當一個線程執行自增操作時加鎖來防止其他程序“搗亂”,具體修改如下,可以在自增操作前直接加鎖:
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
int i = 0;
std::mutex mt;
void inc()
{
std::lock_guard<std::mutex> l(mt);
i++;
}
void func(int n)
{
for (int k = 0; k < n; k++) inc();
}
int main(int argc, char* argv[])
{
int n = argc > 1 ? atoi(argv[1]) : 100;
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
std::thread t1(func, n);
std::thread t2(func, n);
t1.join();
t2.join();
std::cout << "i=" << i << std::endl;
std::chrono::duration<double> duration_cost = std::chrono::duration_cast<
std::chrono::duration<double> >(std::chrono::steady_clock::now() - start);
std::cout << "total cost " << duration_cost.count() << " seconds." << std::endl;
return 0;
}
執行
g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 10000000
指令後運作結果如下:
i=20000000
total cost 2.39123 seconds.
通過加鎖,我們已經保證了結果的正确性,但是我們知道加鎖的額外消耗還是很大的,有沒有其他的方式來實作原子操作呢?
使用atomic來保證自增的原子性
其實在C++11之前可以通過嵌入彙編指令來實作,不過自從C++11引入atomic之後,類似的需求變得簡單了許多,可以直接使用autmic這個模闆類來實作,代碼幾乎不需要修改,隻需将變量
i
改為
atomic<int>
類型,再把鎖去掉就可以了,修改後的代碼如下:
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <atomic>
std::atomic<int> i = 0; // int -> atomic<int>
std::mutex mt;
void inc()
{
//std::lock_guard<std::mutex> l(mt); //remove lock
i++;
}
void func(int n)
{
for (int k = 0; k < n; k++) inc();
}
int main(int argc, char* argv[])
{
int n = argc > 1 ? atoi(argv[1]) : 100;
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
std::thread t1(func, n);
std::thread t2(func, n);
t1.join();
t2.join();
std::cout << "i=" << i << std::endl;
std::chrono::duration<double> duration_cost = std::chrono::duration_cast<
std::chrono::duration<double> >(std::chrono::steady_clock::now() - start);
std::cout << "total cost " << duration_cost.count() << " seconds." << std::endl;
return 0;
}
執行
g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 10000000
指令後運作結果如下:
i=20000000
total cost 1.6554 seconds.
通過對比可以發現,使用
std::atomic
模闆類之後,在保證了結果正确的同時,相比于加鎖實作原子性速度上有了明顯的提升。
總結
-
是指事務管理中的原子性,一緻性,隔離性和持久性4個特性。ACID
- 加鎖(寫鎖)的目的通常是将可能同時發生的操作串行化,以此來避免對資源的競争出現問題
- 操作的并行加快了任務的處理速度,而“加鎖”使部分操作回歸到串行,兩者互相配合是為了在更短的時間内得到正确的結果
-
降低了原子性操作程式設計的難度,同時相比于加鎖實作原子性還有了性能的提升std::atomic
==>> 反爬連結,請勿點選,原地爆炸,概不負責!<<==
時光時光慢些吧,不要再讓你變老了,我願用我一切,換你歲月長留~時間對于每個人來說,都是公平的,真的是這樣嗎?我覺得未必吧!