程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++11讀書筆記—8(多線程使用簡介)

C++11讀書筆記—8(多線程使用簡介)

編輯:關於C++

C/C++程序員最苦惱的是自己跨平台能力不是一半弱。如果想跨平台,有一大波函數庫等著你來深入研究。你再反觀java。。。。

一、原子操作

所謂原子操作,就是多線程中“最小的且不可並行化的操作”。通常原子操作都是互斥訪問保證的。但是互斥一般靠平台相關匯編指令,這也是為什麼C++11之前一直沒有做的原因。

 

#include  //原子操作需要的頭文件
#include  //線程頭文件
#include 

using namespace std;

atomic_llong total{ 0 };//原子數據類型long long
			//這樣的構詞法還有atomic_int等等
//下面的東西不是這一節的內容
void func(int)
{
	for (long long i = 0; i < 100000000LL; ++i)
		total = total + i;
}
int main()
{
	thread t1(func, 0);
	thread t2(func, 0);
	t1.join();
	t2.join();
	cout << total << endl;
	return 0;
}
可以通過中查看內置的原子操作。這就出現一個問題。非內置類型始終怎麼實現原子操作的。這就是atom模板類,std::atomic t; 如:

 

atomic ad{ 12.7f };//這種寫法是C++11推薦的

原子操作通常屬於“資源型”的數據。這意味著多個線程通常只能訪問單個原子類型的拷貝。因此C++11中,原子類型只能從其模板參數中進行構造,標准不允許原子類型進行拷貝構造,移動構造,以及使用operator=等 ,以防止出現意外。這樣無法編譯。

atomic ad1{ad};/./錯誤

 

為了避免線程間關於a的競爭。模板改了很多地方。。

atomic a;

int b =a;//相當於b = a.load();

int a =1;//相當於a.store(1);

二、線程

1.線程對象的創建

#include 
#include 

using namespace std;

void func(int)
{}
int main()
{
	thread t1(func, 0);
	t1.join();
	return 0;
}
線程的構造函數參數,可以視為,(要執行的函數名,該函數參數1,該函數參數2,。。。。)
在這裡,join是阻塞函數。意義為等上面跑完才開始執行下一個操作。我們如果沒有他會怎麼樣呢?主線程繼續往下跑,跑到return 0;但此時t1線程可能沒有跑完,線程對象卻要被強制釋放。如下面代碼;
#include 
#include 
using namespace std;

void func(int a)
{
	for (int i = 0; i < 10; ++i)
		std::cout << a << endl;
}
int main()
{
	int a;//停機變量無意義
	{
		thread t1(func, 1);
		thread t2(func, 2);
	}//運行到這裡,t1,t1沒有了
	std::cin >> a;
	return 0;
}
如果改成這樣,可以正常執行:
int main()
{
	int a;
	{
		thread t1(func, 1);
		thread t2(func, 2);
		t1.join();//主線程被阻塞了,停在這裡,等t1線程對象的線程執行完再運行
		t2.join();
	}
	std::cin >> a;
	return 0;
}
\

 

如果不希望線程被阻塞嗎,將線程與線程對象分離可以用t1.detach();將線程與線程對象分離。這裡要說明下,線程與線程對象是兩碼事(這個很重要)。我們僅是依托線程對象來創建線程。

如下;

 

int main()
{
	int a;
	{
		thread t1(func, 1);
		thread t2(func, 2);
		t1.detach();//主線程沒有被阻塞,將線程與線程對象分離
		t2.detach();
	}//運行到這一步,線程對象依然會析構,但是線程卻可以繼續運行。
	std::cin >> a;
	return 0;
}
這也從側面描述了,用detach將線程與線程對象分開後,就不能合並了。

 

2.線程的特點

線程不能復制,但可以移動(即使用std::move())。線程移動後,線程對象t不再代表任何線程。。

 

另外還可以用std::bind或lambda表達式創建。

 

		thread t1(std::bind(func, 1));
		thread t2([](int, int) {},1,2);
		t1.join();
		t2.join();

三、互斥量(實質就是鎖的變量)

 

互斥量是一種同步原語,線程同步手段,用於保護多線程同時訪問共享數據。“互斥量”的翻譯十分有迷惑性。它就是“鎖類”。以至於如果不這樣理解,將會對後面的條件變量混淆。C++11提供了4種互斥量。

1.獨占互斥量std::mutex

互斥量接口都很相似,一般用法是通過lock()方法來阻塞線程,直到獲得互斥量所有權為止。線程獲得互斥量並完成任務之後,就必須使用unlock()來解除對互斥量的占用,lock()和unlock()必須成對出現。try_lock()嘗試鎖定互斥量,如成功返回true如失敗返回false,他是非阻塞的,看來可以用來檢查當前互斥量的狀態。

改動上面的程序將函數變成加鎖的:

 

#include 
#include 
#include 
using namespace std;

std::mutex uni_lock;

void func(int a)
{
	uni_lock.lock();
	for (int i = 0; i < 10; ++i)
		std::cout << a << endl;
	uni_lock.unlock();
}
int main()
{
	int a;
	{
		thread t1(func, 1);
		thread t2(func, 2);
		t1.join();
		t2.join();
	}
	std::cin >> a;
	return 0;
}

 

這顯示這就友好了

\

官方建議盡量使用更安全的lock_guard。因為他在構造時自動加鎖,析構時自動解鎖,防止忘解鎖的事情發生。lock_guard是個 類模板,形如其名托管互斥量。後面的幾個互斥量基本都用這種方法。

 

std::mutex u_lock;
void func(int a)
{
	std::lock_guard locker(u_lock);
	for (int i = 0; i < 10; ++i)
		std::cout << a << endl;

}

2.遞歸互斥量std::recursive_mutex

遞歸鎖允許同一線程多次獲得該互斥鎖,可以用來解決同一個線程需要多次獲取互斥量時死鎖的問題。

 

需要說明的是,還是盡量不要使用遞歸互斥量的好

(1)需要用到遞歸鎖定的多線程,往往可以簡化為迭代。允許遞歸容易放縱復雜邏輯產生。

(2)遞歸鎖效率一般低一些。

(3)遞歸超過一定數目再lock進行調用會拋出std::system錯誤

3.帶超時的互斥量std::timed_mutex與std::recursive_timed_mutex

可以看做前兩個鎖的改進。超時鎖,主要用在獲取鎖時增加超時等待功能,因為有時不知道獲取鎖需要多久,為了不至於一直在等待獲取互斥量,就設置一個等待超時時間。我們用try_lock_for,try_lock_until兩個接口設置互斥量超時時間。
std::timed_mutex u_lock;

void func(int a)
{
	std::chrono::milliseconds timeout(100);
	while (1)
	{
		if (u_lock.try_lock_for(timeout))
		{
			///...///
		}
	}

}

4.給互斥量上的的兩種區域鎖

上面我們介紹了lock_guard,這其實是一種區域鎖,內部用實現機制是類模板。 std::lock_guard,方便線程對互斥量上鎖。 std::unique_lock,方便線程對互斥量上鎖,但提供了更好的上鎖和解鎖控制。兩者功能很類似嗎,但有一點區別。 用lock_guard去guard一個mutex,必然是在lock_guard的對象離開其作用域時unlock它所guard的mutex,不提供提前unlock的功能。 而unique_lock則提供這個功能,除了像lock_guard一樣在離開作用域時unlock它guard的mutex外,unique還提供unlock函數,使用者可以手動執行unlock。此外,unique_lock還可以設置超時,下一部分條件變量就是用更靈活的unique_lock。

四、條件變量

條件變量是C++11提供的另一種用於等待的同步機制,它能阻塞一個或多個線程。直到收到另一個線程發出的通知或者超時,才會喚醒當前阻塞的線程。條件變量需要和互斥的量配合起來用。C++11提供兩種條件變量。 std::condition_variable:必須與std::unique_lock配合使用(上文提到一種區間鎖)std::condition_variable_any:更加通用的條件變量,可以與任意類型的鎖配合使用,相比前者使用時會有額外的開銷。

他們的成員函數相同。

 

成員函數 說明 notify_one 通知一個等待線程 notify_all 通知全部等待線程 wait 阻塞當前線程直到被喚醒 wait_for 阻塞當前線程直到被喚醒或超過指定的等待時間(長度) wait_until 阻塞當前線程直到被喚醒或到達指定的時間(點) 與其他語言一樣,條件變量必須與鎖一起使用。就像這樣,無論是notify_one或notify_all都是類似於發出脈沖信號,如果對wait的調用發生在notify之後是不會被喚醒的,所以接收者在使用wait等待之前也需要檢查條件(標識)是否滿足,另一個線程(通知者)在nofity前需要修改相應標識供接收者檢查。 下面是一個例子來整合一下我們上面提到的應用:
#include              // std::cout
#include                // std::thread
#include                 // std::mutex, std::unique_lock
#include    // std::condition_variable


std::mutex mtx;					// 全局互斥鎖.
std::condition_variable cv;		// 全局條件變量.
bool ready = false;	// 全局標志位.
//下面是重點
void do_print_id(int id)
{
	std::unique_lock  lck(mtx); //獨占鎖
	while (!ready)				// 如果標志位不為 true, 則等待...
		cv.wait(lck);			// 當前線程被阻塞, 當全局標志位變為 true 之後,
								// 線程被喚醒, 繼續往下執行打印線程編號id.
	std::cout << "thread " << id << '\n';
}

void go()
{
	std::unique_lock  lck(mtx);
	ready = true;				// 設置全局標志位為 true.
	cv.notify_all();			// 通知喚醒所有線程.與上面額wait函數有關。
}
//上面是重點。main函數就是為了生成10個線程。每個線程先死循環,之後突然運行go()打開死循環。
int main()
{
	std::thread threads[10];
	//下面開10個線程:
	for (int i = 0; i < 10; ++i)
		threads[i] = std::thread(do_print_id, i);

	go(); // go!

	for (auto & th : threads) //直接诶是
		th.join();

	return 0;
}
\

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved