天天看點

線程的互斥和同步(1)- 原子操作與自旋鎖1. 使用Windows API實作原子操作2. 使用C++11提供的原子對象實作原子操作3. 使用atmoic_flag實作自旋鎖

文章目錄

  • 1. 使用Windows API實作原子操作
  • 2. 使用C++11提供的原子對象實作原子操作
  • 3. 使用atmoic_flag實作自旋鎖

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

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

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值對象:

除了可以使用模闆,也可以使用内置的一些類型

原子類型名稱 對應的内置類型名稱
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_flag 和 atomic_bool 是不同的,相比其他的原子類型,atmoic_flag 是無鎖類型,即線程通路不需要加鎖。典型的使用是使用,成員 test_and_set 和 clear 實作自旋鎖。

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)- 原子操作與自旋鎖

繼續閱讀