C++原子操作與記憶體序 1

robot2017發表於2024-03-31

問題

#include<iostream>
#include<thread>
int main()
{
	int sum = 0;
	auto f = [&sum]() {
		for (int i = 0; i < 10000; i++)
			sum += 1;
	};
	std::thread t1(f);
	std::thread t2(f);

	t1.join();
	t2.join();
	std::cout << "the sum of 2 threads is: " << sum << std::endl;
	std::cin.get();
	return 0;
}

這個程式只是簡單的透過兩個執行緒對同一個變數進行累加10000次,正常不管執行緒執行的先後順序,結果都應該是20000才對,可實際輸出結果如圖所示,程式的輸出3次的結果都不一樣,不一定是預期的20000;

執行結果

分析

對於+1操作,具體執行可以分為3個操作,如下圖所示:
累加順序

可以看出問題發生在兩個執行緒寫的時候,如執行緒1剛寫完,執行緒2繼續寫,則丟失一次加法。所以得出的值往往小於20000。

解決

可以透過std::mutex加鎖對變數操作進行保護,有沒有不用鎖也能實現的呢?C++中提供了原子操作可以實現這一目標。

程式碼如下:

	std::atomic<int> sum1 = 0;
	auto f1 = [&sum1]() {
		for (int i = 0; i < 10000; i++)
			sum1+=1;
	};

	std::thread t3(f1);
	std::thread t4(f1);

	t3.join();
	t4.join();
	std::cout << "the sum of 2 threads with atomic is: " << sum1 << std::endl;
輸出如下:

原子變數
可以看出未原子化的sum仍然是每次結果不盡相同,而原子化的sum1每次結果都為20000。
所謂原子操作指的是不可分割的操作,可以理解為只能編譯成一條單獨的CPU執行指令,不可以再分解,C++中,基本透過原子型別來實現原子操作。這種原子型別為std::atomic<T>,其中模板引數T為基本的資料型別,如bool,char,int,指標等。
程式中將sum1原子化,並呼叫+=運算子(已過載為原子操作),之前分解的3步成了不可分割的1步,所以不會出現兩個執行緒同時已經進入寫的狀態,進而能保證累加結果的正確。

注意事項

  1. 若累加操作改為sum1=sum1+1,就不是原子操作了,結果與sum沒有差別

  2. int型++/+=是原子操作fetch_add()的過載,類似的還有fetch_sub()/fetch_and/fetch_or()/fetch_xor()

相關文章