C++11多線程(三):atomic與atomic_flag的使用與原理分析

        C++11提供了線程的原子操作,這些在boost庫中也有,在多個線程時原子操作保證了數據的安全性。C++11的原子操作主要是atomic和atomic_flag,使用時需要包含如下頭文件:

#include<atomic>

一.  atomic類的使用

         template<>  struct atomic<_ITYPE> , 模板類,生成一個T類型的原子對象,並提供了一些原子操作方法。

        例如用兩個線程對數0加兩百萬次,如果不用原子操作,互斥鎖等操作,那麼和一定不是二百萬。代碼如下:

#include <iostream> 
#include <thread>
using  namespace  std;
 
int N = 0;

void  ThreadFun()
{
	for (int i = 0; i < 1000000; ++i)
	{
		++N; //線程併發導致 加操作 重疊,不是原子操作,因此肯定少於2000000
	}
}

int main()
{
	//每個線程100萬次+1
	thread  t1(ThreadFun);
	thread  t2(ThreadFun);

	t1.join();
	t2.join();

	cout << N << endl;//應該是2000000
	system("pause");
	return 0;
}

 結果如下:

 使用原子操作就可以解決加錯得問題,只要添加頭文件,原子對象聲明即可,代碼修改如下,

#include <iostream> 
#include <atomic>//原子操作頭文件
#include <thread>
using  namespace  std;
 
atomic<int>  N = 0;//用atomic保證對N的操作的原子性

  現在的結果:

 用互斥量,臨界區等,也可以解決多線程加法錯誤問題,但是用鎖會犧牲性能,原子操作是比較折中的方式,可以寫代碼測試。

 

二. atomic_flag的使用

        atomic_flag 一種簡單的原子布爾類型,只支持兩種操作,test-and-set 和 clear。

       std::atomic_flag 構造函數如下:

atomic_flag() noexcept = default;
atomic_flag (const atomic_flag&T) = delete;

      std::atomic_flag 只有默認構造函數,拷貝構造函數已被禁用,因此不能從其他的 std::atomic_flag 對象構造一個新的 std::atomic_flag 對象。

       如果在初始化時沒有明確使用 ATOMIC_FLAG_INIT初始化,那麼新創建的 std::atomic_flag 對象的狀態是未指定的(unspecified)(既沒有被 set 也沒有被 clear。)另外,atomic_flag不能被拷貝,也不能 move 賦值。

       ATOMIC_FLAG_INIT: 如果某個 std::atomic_flag 對象使用該宏初始化,那麼可以保證該 std::atomic_flag 對象在創建時處於 clear 狀態。

      下面先看一個簡單的例子,main() 函數中創建了 10 個線程進行計數,率先完成計數任務的線程輸出自己的 ID,後續完成計數任務的線程不會輸出自身 ID:

#include <iostream>              
#include <atomic>                
#include <thread>                
#include <vector>                

std::atomic<bool> ready(false);    
std::atomic_flag winner = ATOMIC_FLAG_INIT;    // always set when checked

void count1m(int id)
{
	while (!ready) 
	{
		std::this_thread::yield();
	} // 等待主線程中設置 ready 爲 true.

	for (int i = 0; i < 1000000; ++i) 
	{
		// 計數.
	} 

	// 如果某個線程率先執行完上面的計數過程,則輸出自己的 ID.
	// 此後其他線程執行 test_and_set 是 if 語句判斷爲 false,
	// 因此不會輸出自身 ID.
	if (!winner.test_and_set()) 
	{
		std::cout << "thread #" << id << " won!\n";
	}
}

int main()
{
	std::vector<std::thread> threads;
	std::cout << "spawning 10 threads that count to 1 million...\n";
	for (int i = 1; i <= 10; ++i)
		threads.push_back(std::thread(count1m, i));

	ready = true;

	for (auto & th : threads)
		th.join();

	system("pause");
	return 0;
}

測試結果如下:

test_and_set 介紹

       std::atomic_flag 的 test_and_set 函數原型如下:

bool test_and_set (memory_order sync = memory_order_seq_cst) volatile noexcept;
bool test_and_set (memory_order sync = memory_order_seq_cst) noexcept;

     test_and_set() 函數檢查 std::atomic_flag 標誌,如果 std::atomic_flag 之前沒有被設置過,則設置 std::atomic_flag 的標誌,並返回先前該 std::atomic_flag 對象是否被設置過,如果之前 std::atomic_flag 對象已被設置,則返回 true,否則返回 false。

test-and-set 操作是原子的(因此 test-and-set 是原子 read-modify-write (RMW)操作)。

test_and_set 可以指定 Memory Order(後續的文章會詳細介紹 C++11 的 Memory Order,此處爲了完整性列出 test_and_set 參數 sync 的取值),取值如下:

 

Memory Order 值 Memory Order 類型
memory_order_relaxed Relaxed
memory_order_consume Consume
memory_order_acquire Acquire
memory_order_release Release
memory_order_acq_rel Acquire/Release
memory_order_seq_cst Sequentially consistent

 一個簡單的例子:

#include <iostream>                
#include <atomic>               
#include <thread>               
#include <vector>               
#include <sstream>                

std::atomic_flag lock_stream = ATOMIC_FLAG_INIT;
std::stringstream stream;

void append_number(int x)
{
	while (lock_stream.test_and_set()) 
	{
	}

	stream << "thread #" << x << '\n';
	lock_stream.clear();
}

int main()
{
	std::vector <std::thread> threads;
	for (int i = 1; i <= 10; ++i)
		threads.push_back(std::thread(append_number, i));

	for (auto & th : threads)
		th.join();

	std::cout << stream.str() << std::endl;

	system("pause");
	return 0;
}

結果如下

atomic_flag::clear() 介紹

           清除 std::atomic_flag 對象的標誌位,即設置 atomic_flag 的值爲 false。clear 函數原型如下:

void clear (memory_order sync = memory_order_seq_cst) volatile noexcept;
void clear (memory_order sync = memory_order_seq_cst) noexcept;

          清除 std::atomic_flag 標誌使得下一次調用 std::atomic_flag::test_and_set 返回 false。

         std::atomic_flag::clear() 可以指定 Memory Order(後續的文章會詳細介紹 C++11 的 Memory Order,此處爲了完整性列出 clear 參數 sync 的取值),取值如下:

Memory Order 值 Memory Order 類型
memory_order_relaxed Relaxed
memory_order_consume Consume
memory_order_acquire Acquire
memory_order_release Release
memory_order_acq_rel Acquire/Release
memory_order_seq_cst Sequentially consistent

        結合 std::atomic_flag::test_and_set() 和 std::atomic_flag::clear(),std::atomic_flag 對象可以當作一個簡單的自旋鎖使用,請看下例:

#include <thread>
#include <vector>
#include <iostream>
#include <atomic>

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_acquire))  // acquire lock
             ; // spin
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // release lock
    }
}

int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f, n);
    }
    for (auto& t : v) {
        t.join();
    }
}

        在上面的程序中,std::atomic_flag 對象 lock 的上鎖操作可以理解爲 lock.test_and_set(std::memory_order_acquire); (此處指定了 Memory Order,更多有關 Memory Order 的概念,我會在後續的文章中介紹),解鎖操作相當與 lock.clear(std::memory_order_release)。

        在上鎖的時候,如果 lock.test_and_set 返回 false,則表示上鎖成功(此時 while 不會進入自旋狀態),因爲此前 lock 的標誌位爲 false(即沒有線程對 lock 進行上鎖操作),但調用 test_and_set 後 lock 的標誌位爲 true,說明某一線程已經成功獲得了 lock 鎖。

        如果在該線程解鎖(即調用 lock.clear(std::memory_order_release)) 之前,另外一個線程也調用 lock.test_and_set(std::memory_order_acquire) 試圖獲得鎖,則 test_and_set(std::memory_order_acquire) 返回 true,則 while 進入自旋狀態。如果獲得鎖的線程解鎖(即調用了 lock.clear(std::memory_order_release))之後,某個線程試圖調用 lock.test_and_set(std::memory_order_acquire) 並且返回 false,則 while 不會進入自旋,此時表明該線程成功地獲得了鎖。

        按照上面的分析,我們知道在某種情況下 std::atomic_flag 對象可以當作一個簡單的自旋鎖使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章