線程的互斥和同步(1)- 原子操作與自旋鎖


在進行多線程編成的時候,我們經常會遇到線程的互斥與同步問題。比如多個線程訪問同一個變量,需要互斥的操作,一個線程需要等待另一個線程處理後再進行接下來的操作等等。接下來我們看一下線程的互斥,原子操作。

原子操作 ,是多線程程序中 “最小的且不可並行化的” 的操作。通常一個資源的操作是原子操作的話,意味着多個線程訪問資源時,有且僅有唯一一個線程在對這個資源進行操作。

1. 使用Windows API實現原子操作

Windows 提供了原子操作的API接口,這些API可以對整型變量進行操作,下面列出幾個相關的相關的API及說明:

函數名 函數說明
InterlockedIncrement 將整型變量自增1
InterlockedDecrement 將整型變量自減1
InterlockedExchangeAdd 將整型變量增加n
InterlockedXor 將整型變量異或操作
InterlockedCompareExchange 將整型值與 n1 進行比較,如果相等,則替換成 n2

下面是一個簡單示例,本實例中是繼承自 CThread 寫的多線程程序,關於 CThread 的實現可以參照
使用Windows API實現自定義線程類CThread

頭文件定義:

#include "CThread.h"
class WinAtomicThread : public CThread
{
public:
	void run(void) override;
};

源文件實現:

LONG WinAtomic = 0;
void WinAtomicThread::run(void)
{
	while (1) {
		LONG count = ::InterlockedIncrement(&WinAtomic);
		std::cout << "Run in Thread ID " << ::GetCurrentThreadId() \
			      << " , Number is " << count << std::endl;
		
		Sleep(500);
	}
}

這裏在線程中,對整型變量做簡單的自增操作。
函數調用如下:

// Win原子操作測試
WinAtomicThread *thread1 = new WinAtomicThread;
WinAtomicThread *thread2 = new WinAtomicThread;
thread1->start();
thread2->start();

thread1->wait();
thread2->wait();

運行結果如下:
Run in Thread ID 21112 , Number is 2
Run in Thread ID 6900 , Number is 1
Run in Thread ID 21112 , Number is 4
Run in Thread ID 6900 , Number is 3
Run in Thread ID 21112 , Number is 5
Run in Thread ID 6900 , Number is 6

2. 使用C++11提供的原子對象實現原子操作

上面只是提供了Windows上提供的原子操作相關的API,無法移植到Linux或Mac等其他操作系統上。C++11爲我們提供了對於原子操作標準上的支持。使用模板 std::atomic ,需要包含頭文件 <atomic>
比如使用如下代碼就可以創建一個原子類型的int值對象:

std::atomic<int> a;

除了可以使用模板,也可以使用內置的一些類型

原子類型名稱 對應的內置類型名稱
atomic_bool bool
atomic_char char
atomic_schar signed char
atomic_uchar unsigned char
atomic_int int
atomic_uint unsigned int
atomic_short short
atomic_ushort unsigned short
atomic_long long
atomic_ulong unsigned long
atomic_llong long long
atomic_ullong unsigned long long
atomic_char16_t char16_t
atmoic_char32_t char32_t
atmoic_wchar_t wchart_t

對於線程而言,原子類型屬性資源型數據,這意味着多個線程只能訪問單個原子類型的拷貝。因此在C++11中,原子類型只能從模板類型中進行構造,不允許原子類型進行拷貝構造、移動構造,以及operator=等。std::atomic的實現中有下面幾句代碼:

atomic(const atomic&) = delete;
atomic& operator=(const atomic&) = delete;
atomic& operator=(const atomic&) volatile = delete;

下面是一個簡單的使用示例,同樣也是實現了多線程自增操作:
頭文件:

#include "CThread.h"
#include <atomic>

class STDAtomicThread : public CThread
{
public:
	void run(void) override;
};

源文件:

std::atomic<int> STDAtomicValue(0);
void STDAtomicThread::run(void)
{
	while (1) {
		std::cout << "Run in Thread ID " << ::GetCurrentThreadId() \
			<< " , Number is " << STDAtomicValue++ << std::endl;

		Sleep(500);
	}
}

這裏使用++操作,std::atmoic重載了++操作符,實現了原子量的自增操作。
下面列出了關於 std::atmoic 的主要操作:

操作 atomic_flag atomic_bool atmoic_integral-type atomic<T*> atomic<Class-Type>
test_and_set y
clear y
is_lock_free y y y y
load y y y y
store y y y y
exchange y y y y
compare_exchange_weak +strong y y y y
fetch_add, += y y
fetch_sub, -= y y
fetch_or, |= y y
fetch_and, &= y
fetch_xor, ^= y
++,– y y y

大部分原子類型都有讀、寫、交換、比較交換等操作。
這裏需要指出的是,atomic_flagatomic_bool 是不同的,相比其他的原子類型,atmoic_flag 是無鎖類型,即線程訪問不需要加鎖。典型的使用是使用,成員 test_and_setclear 實現自旋鎖。

3. 使用atmoic_flag實現自旋鎖

自旋鎖(spinlock) :是當一個線程獲取鎖的時候,如果鎖已經被其他線程獲取,那麼該線程會循環等待,直到鎖獲取成功再退出循環。

自旋鎖互斥鎖 都是一種實現資源保護的一種鎖機制。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。但是兩者在調度機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。-- 摘自百度百科。百度百科-自旋鎖

接下來是一個自旋鎖的例子,本例子中線程1等待線程2釋放鎖後執行:
頭文件

#include "CThread.h"
#include <atomic>
#include <iostream>
// 線程1
class SpinLockThread1 : public CThread
{
public:
	void run(void) override;
};
// 線程2
class SpinLockThread2 : public CThread
{
public:
	void run(void) override;
};

源文件:

std::atomic_flag lock{1};

void SpinLockThread1::run(void)
{
	std::cout << "Start Run Thread1" << std::endl;
	// 自旋等待
	while (lock.test_and_set(std::memory_order_acquire))
		std::cout << "Wait For UnLock" << std::endl;
	std::cout << "End Run Thread1" << std::endl;
}

void SpinLockThread2::run(void)
{
	std::cout << "Start Run Thread2" << std::endl;
	Sleep(20);
	std::cout << "Thread2 Free Lock" << std::endl;
	// 解自旋鎖
	lock.clear();
}
  • 線程1中不斷的判斷函數 test_and_set 的返回值,如果返回值爲true,則一直打印 Wait For UnLock ,即進入自旋狀態。函數 test_and_set ,表示設置lock爲true,並返回設置前的值。
  • 線程2中等待20ms後,調用函數 clear() 是指將lock的值設置爲false,因此線程1退出自旋。

具體調用如下:

// 自旋鎖
SpinLockThread1 *thread1 = new SpinLockThread1;
SpinLockThread2 *thread2 = new SpinLockThread2;

thread1->start();
thread2->start();

thread1->wait();
thread2->wait();

運行結果:
Created Thread Success, Id is 17876
Created Thread Success, Id is 12556
Start Run Thread2
Start Run Thread1
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Thread2 Free Lock
Wait For UnLock
End Run Thread1

我們也可以將封裝爲Lock和Unlock函數

// 加鎖
void Lock(std::atomic_flag* lock) {
	while (lock->test_and_set(std::memory_order_acquire));
}
// 解鎖
void Unlock(std::atomic_flag* lock){
	lock->free();
}

關於函數 test_and_set 的參數 std::memory_order_acquire ,表示在本線程中後續的讀操作必須在本條原子操作完成後執行。因爲不同的CPU可能實際的程序執行順序並不是代碼的順序。
還有其他的值可以被設置,如下表所示:

枚舉值 說明
memory_order_relaxed 不對執行順序做任何保證
memory_order_acquire 本線程中, 所有後續的讀操作必須在本條原子操作完成後執行
memory_order_release 本線程中,所有之前的寫操作完成後才能執行本條原子操作
memory_order_acq_rel 同時包含 memory_order_acquire 和 memory_order_release 標記
memory_order_consume 本線程中,所有後續的有關本原子類型的操作,必須本條原子操作完成之後執行
memory_order_seq_cst 全部存取都按順序執行

作者:douzhq
個人博客主頁:不會飛的紙飛機
文章同步頁(可下載完整代碼):線程的互斥和同步(1)- 原子操作與自旋鎖

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