真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

C++11———線程庫-創(chuàng)新互聯(lián)

文章目錄
  • 線程庫
    • 線程庫(thread)
      • 線程對象的構(gòu)造方式
      • thread提供的成員函數(shù)
      • 獲取線程的id的方式
      • 線程函數(shù)的參數(shù)問題
      • join與detach
    • 互斥量庫(mutex)
      • mutex的種類
      • lock_guard和unique_lock
    • 原子性操作庫(atomic)
    • 條件變量庫(condition_variable)
  • 實(shí)現(xiàn)兩個線程交替打印1-100

創(chuàng)新互聯(lián)建站專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都做網(wǎng)站、成都網(wǎng)站制作、烏海海南網(wǎng)絡(luò)推廣、重慶小程序開發(fā)公司、烏海海南網(wǎng)絡(luò)營銷、烏海海南企業(yè)策劃、烏海海南品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們大的嘉獎;創(chuàng)新互聯(lián)建站為所有大學(xué)生創(chuàng)業(yè)者提供烏海海南建站搭建服務(wù),24小時服務(wù)熱線:18980820575,官方網(wǎng)址:www.cdcxhl.com線程庫

在C++11之前,涉及到多線程問題,都是和平臺相關(guān)的,比如Windows和Linux下各有自己的接口,這使得代碼的可移植性比較差。C++11中最重要的特性就是對線程進(jìn)行了支持,使得C++在并行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念。

線程庫(thread) 線程對象的構(gòu)造方式

一、調(diào)用無參的構(gòu)造函數(shù)

thread提供了無參的構(gòu)造函數(shù),調(diào)用無參的構(gòu)造函數(shù)創(chuàng)建出來的線程對象沒有關(guān)聯(lián)任何線程函數(shù),即沒有啟動任何線程。比如:

thread t1;

由于thread提供了移動賦值函數(shù),因此當(dāng)后續(xù)需要讓該線程對象與線程函數(shù)關(guān)聯(lián)時,可以以帶參的方式創(chuàng)建一個匿名對象,然后調(diào)用移動賦值將該匿名對象關(guān)聯(lián)線程的狀態(tài)轉(zhuǎn)移給該線程對象。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t1;
	//...
	t1 = thread(func, 10);

	t1.join();
	return 0;
}

場景: 實(shí)現(xiàn)線程池的時候就是需要先創(chuàng)建一批線程,但一開始這些線程什么也不做,當(dāng)有任務(wù)到來時再讓這些線程來處理這些任務(wù)。

二、調(diào)用帶參的構(gòu)造函數(shù)

thread的帶參的構(gòu)造函數(shù)的定義如下:

templateexplicit thread (Fn&& fn, Args&&... args);

參數(shù)說明:

  • fn:可調(diào)用對象,比如函數(shù)指針、仿函數(shù)、lambda表達(dá)式、被包裝器包裝后的可調(diào)用對象等。
  • args...:調(diào)用可調(diào)用對象fn時所需要的若干參數(shù)。

調(diào)用帶參的構(gòu)造函數(shù)創(chuàng)建線程對象,能夠?qū)⒕€程對象與線程函數(shù)fn進(jìn)行關(guān)聯(lián)。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t2(func, 10);

	t2.join();
	return 0;
}

三、調(diào)用移動構(gòu)造函數(shù)

thread提供了移動構(gòu)造函數(shù),能夠用一個右值線程對象來構(gòu)造一個線程對象。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t3 = thread(func, 10);

	t3.join();
	return 0;
}

說明一下:

  • 線程是操作系統(tǒng)中的一個概念,線程對象可以關(guān)聯(lián)一個線程,用來控制線程以及獲取線程的狀態(tài)。
  • 如果創(chuàng)建線程對象時沒有提供線程函數(shù),那么該線程對象實(shí)際沒有對應(yīng)任何線程。
  • 如果創(chuàng)建線程對象時提供了線程函數(shù),那么就會啟動一個線程來執(zhí)行這個線程函數(shù),該線程與主線程一起運(yùn)行。
  • thread類是防拷貝的,不允許拷貝構(gòu)造和拷貝賦值,但是可以移動構(gòu)造和移動賦值,可以將一個線程對象關(guān)聯(lián)線程的狀態(tài)轉(zhuǎn)移給其他線程對象,并且轉(zhuǎn)移期間不影響線程的執(zhí)行。
thread提供的成員函數(shù)

thread中常用的成員函數(shù)如下:

成員函數(shù)功能
join對該線程進(jìn)行等待,在等待的線程返回之前,調(diào)用join函數(shù)的線程將會被阻塞
joinable判斷該線程是否已經(jīng)執(zhí)行完畢,如果是則返回true,否則返回false
detach將該線程與創(chuàng)建線程進(jìn)行分離,被分離后的線程不再需要創(chuàng)建線程調(diào)用join函數(shù)對其進(jìn)行等待
get_id獲取該線程的id
swap將兩個線程對象關(guān)聯(lián)線程的狀態(tài)進(jìn)行交換

此外,joinable函數(shù)還可以用于判定線程是否是有效的,如果是以下任意情況,則線程無效:

  • 采用無參構(gòu)造函數(shù)構(gòu)造的線程對象。(該線程對象沒有關(guān)聯(lián)任何線程)
  • 線程對象的狀態(tài)已經(jīng)轉(zhuǎn)移給其他線程對象。(已經(jīng)將線程交給其他線程對象管理)
  • 線程已經(jīng)調(diào)用join或detach結(jié)束。(線程已經(jīng)結(jié)束)
獲取線程的id的方式

調(diào)用thread的成員函數(shù)get_id可以獲取線程的id,但該方法必須通過線程對象來調(diào)用get_id函數(shù),如果要在線程對象關(guān)聯(lián)的線程函數(shù)中獲取線程id,可以調(diào)用this_thread命名空間下的get_id函數(shù)。比如:

void func()
{cout<< this_thread::get_id()<< endl; //獲取線程id
}
int main()
{thread t(func);

	t.join();
	return 0;
}

this_thread命名空間中還提供了以下三個函數(shù):

函數(shù)名功能
yield當(dāng)前線程“放棄”執(zhí)行,讓操作系統(tǒng)調(diào)度另一線程繼續(xù)執(zhí)行
sleep_until讓當(dāng)前線程休眠到一個具體時間點(diǎn)
sleep_for讓當(dāng)前線程休眠一個時間段
線程函數(shù)的參數(shù)問題

線程函數(shù)的參數(shù)是以值拷貝的方式拷貝到線程??臻g中的,就算線程函數(shù)的參數(shù)為引用類型,在線程函數(shù)中修改后也不會影響到外部實(shí)參,因?yàn)槠鋵?shí)際引用的是線程棧中的拷貝,而不是外部實(shí)參。比如:

void add(int& num)
{num++;
}
int main()
{int num = 0;
	thread t(add, num);
	t.join();

	cout<< num<< endl; //0
	return 0;
}

如果要通過線程函數(shù)的形參改變外部的實(shí)參,可以參考以下三種方式:

方式一:借助std::ref函數(shù)

當(dāng)線程函數(shù)的參數(shù)類型為引用類型時,如果要想線程函數(shù)形參引用的是外部傳入的實(shí)參,而不是線程??臻g中的拷貝,那么在傳入實(shí)參時需要借助ref函數(shù)保持對實(shí)參的引用。比如:

void add(int& num)
{num++;
}
int main()
{int num = 0;
	thread t(add, ref(num));
	t.join();

	cout<< num<< endl; //1
	return 0;
}

方式二:地址的拷貝

將線程函數(shù)的參數(shù)類型改為指針類型,將實(shí)參的地址傳入線程函數(shù),此時在線程函數(shù)中可以通過修改該地址處的變量,進(jìn)而影響到外部實(shí)參。比如:

void add(int* num)
{(*num)++;
}
int main()
{int num = 0;
	thread t(add, &num);
	t.join();

	cout<< num<< endl; //1
	return 0;
}

方式三:借助lambda表達(dá)式

將lambda表達(dá)式作為線程函數(shù),利用lambda函數(shù)的捕捉列表,以引用的方式對外部實(shí)參進(jìn)行捕捉,此時在lambda表達(dá)式中對形參的修改也能影響到外部實(shí)參。比如:

int main()
{int num = 0;
	thread t([&num]{num++; });
	t.join();

	cout<< num<< endl; //1
	return 0;
}
join與detach

啟動一個線程后,當(dāng)這個線程退出時,需要對該線程所使用的資源進(jìn)行回收,否則可能會導(dǎo)致內(nèi)存泄露等問題。thread庫給我們提供了如下兩種回收線程資源的方式:

join方式

主線程創(chuàng)建新線程后,可以調(diào)用join函數(shù)等待新線程終止,當(dāng)新線程終止時join函數(shù)就會自動清理線程相關(guān)的資源。

join函數(shù)清理線程的相關(guān)資源后,thread對象與已銷毀的線程就沒有關(guān)系了,因此一個線程對象一般只會使用一次join,否則程序會崩潰。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t(func, 20);
	t.join();
	t.join(); //程序崩潰
	return 0;
}

但如果一個線程對象join后,又調(diào)用移動賦值函數(shù),將一個右值線程對象的關(guān)聯(lián)線程的狀態(tài)轉(zhuǎn)移過來了,那么這個線程對象又可以調(diào)用一次join。比如:

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t(func, 20);
	t.join();

	t = thread(func, 30);
	t.join();
	return 0;
}

但采用join的方式結(jié)束線程,在某些場景下也可能會出現(xiàn)問題。比如在該線程被join之前,如果中途因?yàn)槟承┰驅(qū)е鲁绦虿辉賵?zhí)行后續(xù)代碼,這時這個線程將不會被join

void func(int n)
{for (int i = 0; i<= n; i++)
	{cout<< i<< endl;
	}
}
bool DoSomething()
{return false;
}
int main()
{thread t(func, 20);

	//...
	if (!DoSomething())
		return -1;
	//...

	t.join(); //不會被執(zhí)行
	return 0;
}

因此采用join方式結(jié)束線程時,join的調(diào)用位置非常關(guān)鍵,為了避免上述問題,可以采用RAII的方式對線程對象進(jìn)行封裝,也就是利用對象的生命周期來控制線程資源的釋放。比如:

class myThread
{public:
	myThread(thread& t)
		:_t(t)
	{}
	~myThread()
	{if (_t.joinable())
			_t.join();
	}
	//防拷貝
	myThread(myThread const&) = delete;
	myThread& operator=(const myThread&) = delete;
private:
	thread& _t;
};

使用方式如下:

  • 每當(dāng)創(chuàng)建一個線程對象后,就用myThread類對其進(jìn)行封裝產(chǎn)生一個myThread對象。
  • 當(dāng)myThread對象生命周期結(jié)束時就會調(diào)用析構(gòu)函數(shù),在析構(gòu)中會通過joinable判斷這個線程是否需要被join,如果需要那么就會調(diào)用join對其該線程進(jìn)行等待。

例如剛才的代碼中,使用myThread類對線程對象進(jìn)行封裝后,就能保證線程一定會被join。

int main()
{thread t(func, 20);
	myThread mt(t); //使用myThread對線程對象進(jìn)行封裝

	//...
	if (!DoSomething())
		return -1;
	//...

	t.join();
	return 0;
}

detach方式

主線程創(chuàng)建新線程后,也可以調(diào)用detach函數(shù)將新線程與主線程進(jìn)行分離,分離后新線程會在后臺運(yùn)行,其所有權(quán)和控制權(quán)將會交給C++運(yùn)行庫,此時C++運(yùn)行庫會保證當(dāng)線程退出時,其相關(guān)資源能夠被正確回收。

  • 使用detach的方式回收線程的資源,一般在線程對象創(chuàng)建好之后就立即調(diào)用detach函數(shù)。
  • 否則線程對象可能會因?yàn)槟承┰?,在后續(xù)調(diào)用detach函數(shù)分離線程之前被銷毀掉,這時就會導(dǎo)致程序崩潰。
  • 因?yàn)楫?dāng)線程對象被銷毀時會調(diào)用thread的析構(gòu)函數(shù),而在thread的析構(gòu)函數(shù)中會通過joinable判斷這個線程是否需要被join,如果需要那么就會調(diào)用terminate終止當(dāng)前程序(程序崩潰)。
互斥量庫(mutex) mutex的種類

四種互斥量

在C++11中,mutex中總共包了四種互斥量:

1、std::mute
mutex鎖是C++11提供的最基本的互斥量,mutex對象之間不能進(jìn)行拷貝,也不能進(jìn)行移動。

mutex中常用的成員函數(shù)如下:

成員函數(shù)功能
lock對互斥量進(jìn)行加鎖
try_lock嘗試對互斥量進(jìn)行加鎖
unlock對互斥量進(jìn)行解鎖,釋放互斥量的所有權(quán)

線程函數(shù)調(diào)用lock時,可能會發(fā)生以下三種情況:

  • 如果該互斥量當(dāng)前沒有被其他線程鎖住,則調(diào)用線程將該互斥量鎖住,直到調(diào)用unlock之前,該線程一致?lián)碛性撴i。
  • 如果該互斥量已經(jīng)被其他線程鎖住,則當(dāng)前的調(diào)用線程會被阻塞。
  • 如果該互斥量被當(dāng)前調(diào)用線程鎖住,則會產(chǎn)生死鎖(deadlock)。

線程調(diào)用try_lock時,類似也可能會發(fā)生以下三種情況:

  • 如果該互斥量當(dāng)前沒有被其他線程鎖住,則調(diào)用線程將該互斥量鎖住,直到調(diào)用unlock之前,該線程一致?lián)碛性撴i。
  • 如果該互斥量已經(jīng)被其他線程鎖住,則try_lock調(diào)用返回false,當(dāng)前的調(diào)用線程不會被阻塞。
  • 如果該互斥量被當(dāng)前調(diào)用線程鎖住,則會產(chǎn)生死鎖(deadlock)。

2、std::recursive_mutex
recursive_mutex叫做遞歸互斥鎖,該鎖專門用于遞歸函數(shù)中的加鎖操作。

  • 如果在遞歸函數(shù)中使用mutex互斥鎖進(jìn)行加鎖,那么在線程進(jìn)行遞歸調(diào)用時,可能會重復(fù)申請已經(jīng)申請到但自己還未釋放的鎖,進(jìn)而導(dǎo)致死鎖問題。
  • 而recursive_mutex允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得互斥量對象的多層所有權(quán),但是釋放互斥量時需要調(diào)用與該鎖層次深度相同次數(shù)的unlock

除此之外,recursive_mutex也提供了lock、try_lockunlock成員函數(shù),其的特性與mutex大致相同。

3、std::timed_mutex
timed_mutex中提供了以下兩個成員函數(shù):

  • try_lock_for:接受一個時間范圍,表示在這一段時間范圍之內(nèi)線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間之內(nèi)還是沒有獲得鎖),則返回false。
  • try_lock_untill:接受一個時間點(diǎn)作為參數(shù),在指定時間點(diǎn)未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間點(diǎn)到來時還是沒有獲得鎖),則返回false。

除此之外,timed_mutex也提供了locktry_lockunlock成員函數(shù),其的特性與mutex相同。

4、std::recursive_timed_mutex
recursive_timed_mutex就是recursive_mutex和timed_mutex的結(jié)合,recursive_timed_mutex既支持在遞歸函數(shù)中進(jìn)行加鎖操作,也支持定時嘗試申請鎖。

加鎖示例

在沒有使用互斥鎖保證線程安全的情況下,讓兩個線程各自打印1-100的數(shù)字,就會導(dǎo)致控制臺輸出錯亂。比如:

void func(int n)
{for (int i = 1; i<= n; i++)
	{cout<< i<< endl;
	}
}
int main()
{thread t1(func, 100);
	thread t2(func, 100);

	t1.join();
	t2.join();
	return 0;
}

如果要讓兩個線程的輸出不會相互影響,即不會讓某一次輸出中途被另一個線程打斷,那么就需要用互斥鎖對打印過程進(jìn)行保護(hù)。

這里加鎖的方式有兩種,一種是在for循環(huán)體內(nèi)進(jìn)行加鎖,一種是在for循環(huán)體外進(jìn)行加鎖。比如:

void func(int n, mutex& mtx)
{mtx.lock(); //for循環(huán)體外加鎖
	for (int i = 1; i<= n; i++)
	{//mtx.lock(); //for循環(huán)體內(nèi)加鎖
		cout<< i<< endl;
		//mtx.unlock();
	}
	mtx.unlock();
}
int main()
{mutex mtx;
	thread t1(func, 100, ref(mtx));
	thread t2(func, 100, ref(mtx));

	t1.join();
	t2.join();
	return 0;
}

說明一下:

  • 此處在for循環(huán)體外加鎖比在for循環(huán)體內(nèi)加鎖更高效,因?yàn)樵趂or循環(huán)體內(nèi)加鎖會導(dǎo)致線程打印數(shù)字時頻繁進(jìn)行加鎖解鎖操作,而如果在for循環(huán)體外加鎖,那么這兩個線程只需要在開始打印1之前進(jìn)行一次加鎖,在打印完100后進(jìn)行一次解鎖就行了。
  • 在for循環(huán)體外加鎖也就意味著兩個線程的打印過程變成了串行的,即一個線程打印完1-100后另一個線程再打印,但這時打印效率提高了,因?yàn)楸苊饬诉@兩個線程間的頻繁切換。
  • 為了保證兩個線程使用的是同一個互斥鎖,線程函數(shù)必須以引用的方式接收傳入的互斥鎖,并且在傳參時需要使用ref函數(shù)保持對互斥鎖的引用。
  • 此外,也可以將互斥鎖定義為全局變量,或是用lambda表達(dá)式定義線程函數(shù),然后以引用的方式將局部的互斥鎖進(jìn)行捕捉,這兩種方法也能保證兩個線程使用的是同一個互斥鎖。

經(jīng)驗(yàn)分享:

  • 在項(xiàng)目中實(shí)際不太建議定義全局變量,因?yàn)槿肿兞咳绻x在頭文件中,當(dāng)這個頭文件被多個源文件包含時,在這多個源文件中都會對這個全局變量進(jìn)行定義,這時就會導(dǎo)致變量重定義,但如果將全局變量定義為靜態(tài),那這個全局變量就只在當(dāng)前文件可見。
  • 如果確實(shí)有一些變量需要在多個文件中使用,那么一般建議將這些變量封裝到一個類當(dāng)中,然后將這個類設(shè)計(jì)成單例模式,當(dāng)需要使用這些變量時就通過這個單例對象去訪問即可。
lock_guard和unique_lock

使用互斥鎖時可能出現(xiàn)的問題

使用互斥鎖時,如果加鎖的范圍太大,那么極有可能在中途返回時忘記了解鎖,此后申請這個互斥鎖的線程就會被阻塞住,也就是造成了死鎖問題。比如:

mutex mtx;
void func()
{mtx.lock();
	//...
	FILE* fout = fopen("data.txt", "r");
	if (fout == nullptr)
	{//...
		return; //中途返回(未解鎖)
	}
	//...
	mtx.unlock();
}
int main()
{func();
	return 0;
}

因此使用互斥鎖時如果控制不好就會造成死鎖,最常見的就是此處在鎖中間代碼返回,此外還有一個比較常見的情況就是在鎖的范圍內(nèi)拋異常,也很容易導(dǎo)致死鎖問題。

因此C++11采用RAII的方式對鎖進(jìn)行了封裝,于是就出現(xiàn)了lock_guard和unique_lock。

lock_guard

lock_guard是C++11中的一個模板類,其定義如下:

templateclass lock_guard;

lock_guard類模板主要是通過RAII的方式,對其管理的互斥鎖進(jìn)行了封裝。

  • 在需要加鎖的地方,用互斥鎖實(shí)例化一個lock_guard對象,在lock_guard的構(gòu)造函數(shù)中會調(diào)用lock進(jìn)行加鎖。
  • 當(dāng)lock_guard對象出作用域前會調(diào)用析構(gòu)函數(shù),在lock_guard的析構(gòu)函數(shù)中會調(diào)用unlock自動解鎖。

通過這種構(gòu)造對象時加鎖,析構(gòu)對象時自動解鎖的方式就有效的避免了死鎖問題。比如:

mutex mtx;
void func()
{lock_guardlg(mtx); //調(diào)用構(gòu)造函數(shù)加鎖
	//...
	FILE* fout = fopen("data.txt", "r");
	if (fout == nullptr)
	{//...
		return; //調(diào)用析構(gòu)函數(shù)解鎖
	}
	//...
} //調(diào)用析構(gòu)函數(shù)解鎖
int main()
{func();
	return 0;
}

從lock_guard對象定義到該對象析構(gòu),這段區(qū)域的代碼都屬于互斥鎖的保護(hù)范圍。

如果只想用lock_guard保護(hù)某一段代碼,可以通過定義匿名的局部域來控制lock_guard對象的生命周期。比如:

mutex mtx;
void func()
{//...
	//匿名局部域
	{lock_guardlg(mtx); //調(diào)用構(gòu)造函數(shù)加鎖
		FILE* fout = fopen("data.txt", "r");
		if (fout == nullptr)
		{	//...
			return; //調(diào)用析構(gòu)函數(shù)解鎖
		}
	} //調(diào)用析構(gòu)函數(shù)解鎖
	//...
}
int main()
{func();
	return 0;
}

模擬實(shí)現(xiàn)lock_guard

模擬實(shí)現(xiàn)lock_guard類的步驟如下:

  • lock_guard類中包含一個鎖成員變量(引用類型),這個鎖就是每個lock_guard對象管理的互斥鎖。
  • 調(diào)用lock_guard的構(gòu)造函數(shù)時需要傳入一個被管理互斥鎖,用該互斥鎖來初始化鎖成員變量后,調(diào)用互斥鎖的lock函數(shù)進(jìn)行加鎖。
  • lock_guard的析構(gòu)函數(shù)中調(diào)用互斥鎖的unlock進(jìn)行解鎖。
  • 需要刪除lock_guard類的拷貝構(gòu)造和拷貝賦值,因?yàn)閘ock_guard類中的鎖成員變量本身也是不支持拷貝的。

代碼如下:

namespace cl
{templateclass lock_guard
	{public:
		lock_guard(Mutex& mtx)
			:_mtx(mtx)
		{	mtx.lock(); //加鎖
		}
		~lock_guard()
		{	mtx.unlock(); //解鎖
		}
		lock_guard(const lock_guard&) = delete;
		lock_guard& operator=(const lock_guard&) = delete;
	private:
		Mutex& _mtx;
	};
}

unique_lock

但由于lock_guard太單一,用戶沒有辦法對鎖進(jìn)行控制,因此C++11又提供了unique_lock。

unique_lock與lock_guard類似,unique_lock類模板也是采用RAII的方式對鎖進(jìn)行了封裝。在創(chuàng)建unique_lock對象調(diào)用構(gòu)造函數(shù)時也會調(diào)用lock進(jìn)行加鎖,在unique_lock對象銷毀調(diào)用析構(gòu)函數(shù)時也會調(diào)用unlock進(jìn)行解鎖。

但lock_guard不同的是,unique_lock更加的靈活,提供了更多的成員函數(shù):

  • 加鎖/解鎖操作:lock、try_lock、try_lock_for、try_lock_until和unlock。
  • 修改操作:移動賦值、swap、release(返回它所管理的互斥量對象的指針,并釋放所有權(quán))。
  • 獲取屬性:owns_lock(返回當(dāng)前對象是否上了鎖)、operator bool(與owns_lock的功能相同)、mutex(返回當(dāng)前unique_lock所管理的互斥量的指針)。

比如如下場景就適合使用unique_lock:

  • 要用互斥鎖保護(hù)函數(shù)1的大部分代碼,但是中間有一小塊代碼調(diào)用了函數(shù)2,而調(diào)用函數(shù)2時不需要用函數(shù)1中的互斥鎖進(jìn)行保護(hù),函數(shù)2內(nèi)部的代碼由其他互斥鎖進(jìn)行保護(hù)。
  • 因此在調(diào)用函數(shù)2之前需要對當(dāng)前互斥鎖進(jìn)行解鎖,當(dāng)函數(shù)2調(diào)用返回后再進(jìn)行加鎖,這樣當(dāng)調(diào)用函數(shù)2時其他線程調(diào)用函數(shù)1就能夠獲取到這個鎖。

如下圖:
在這里插入圖片描述

原子性操作庫(atomic)

線程安全問題

多線程最主要的問題是共享數(shù)據(jù)帶來的問題(即線程安全)。如果共享數(shù)據(jù)都是只讀的,那么沒問題,因?yàn)橹蛔x操作不會影響到數(shù)據(jù),更不會涉及對數(shù)據(jù)的修改,所以所有線程都會獲得同樣的數(shù)據(jù)。但是,當(dāng)一個或多個線程要修改共享數(shù)據(jù)時,就會產(chǎn)生很多潛在的麻煩。比如:

void func(int& n, int times)
{for (int i = 0; i< times; i++)
	{n++;
	}
}
int main()
{int n = 0;
	int times = 100000; //每個線程對n++的次數(shù)
	thread t1(func, ref(n), times);
	thread t2(func, ref(n), times);

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

上述代碼中分別讓兩個線程對同一個變量n進(jìn)行了100000次++操作,理論上最終n的值應(yīng)該是200000,但最終打印出n的值卻是小于200000的。

根本原因就是++操作并不是一個原子操作,該操作分為三步:

  • load:將共享變量n從內(nèi)存加載到寄存器中。
  • update:更新寄存器里面的值,執(zhí)行+1操作。
  • store:將新值從寄存器寫回共享變量n的內(nèi)存地址。

++操作對應(yīng)的匯編代碼如下:
在這里插入圖片描述
因此可能當(dāng)線程1剛將n的值加載到寄存器中就被切走了,也就是只完成了++操作的第一步,而線程2可能順利完成了一次完整的++操作才被切走,而這時線程1繼續(xù)用之前加載到寄存器中的值完成剩余的兩步操作,最終就會導(dǎo)致兩個線程分別對共享變量n進(jìn)行了一次++操作,但最終n的值卻只被++了一次。

加鎖解決線程安全問題

C++98中對于這里出現(xiàn)的線程安全的問題,會選擇對共享修改的數(shù)據(jù)進(jìn)行加鎖保護(hù)。比如:

void func(int& n, int times, mutex& mtx)
{mtx.lock();
	for (int i = 0; i< times; i++)
	{//mtx.lock();
		n++;
		//mtx.unlock();
	}
	mtx.unlock();
}
int main()
{int n = 0;
	int times = 100000; //每個線程對n++的次數(shù)
	mutex mtx;
	thread t1(func, ref(n), times, ref(mtx));
	thread t2(func, ref(n), times, ref(mtx));

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

這里可以選擇在for循環(huán)體里面進(jìn)行加鎖解鎖,也可以選擇在for循環(huán)體外進(jìn)行加鎖解鎖。但效果終究是不盡人意的,在for循環(huán)體里面進(jìn)行加鎖解鎖會導(dǎo)致線程的頻繁進(jìn)行加鎖解鎖操作,在for循環(huán)體外面進(jìn)行加鎖解鎖會導(dǎo)致兩個線程的執(zhí)行邏輯變?yōu)榇?,而且如果鎖控制得不好,還容易造成死鎖。

原子類解決線程安全問題

C++11中引入了原子操作類型,使得線程間數(shù)據(jù)的同步變得非常高效。如下:

原子類型名稱對應(yīng)的內(nèi)置類型名稱
atomic_boolbool
atomic_charchar
atomic_scharsigned char
atomic_ucharunsigned char
atomic_intint
atomic_uintunsigned int
atomic_shortshort
atomic_ushortunsigned short
atomic_longlong
atomic_ulongunsigned long
atomic_llonglong long
atomic_ullongunsigned long long
atomic_char16_tchar16_t
atomic_char32_tchar32_t
atomic_wchar_twchar_t

注意: 需要用大括號對原子類型的變量進(jìn)行初始化。

程序員不需要對原子類型進(jìn)行加鎖解鎖操作,線程能夠?qū)υ宇愋妥兞炕コ庠L問。比如剛才的代碼可以改為:

void func(atomic_int& n, int times)
{for (int i = 0; i< times; i++)
	{n++;
	}
}
int main()
{atomic_int n = {0 };
	int times = 100000; //每個線程對n++的次數(shù)
	thread t1(func, ref(n), times);
	thread t2(func, ref(n), times);

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

除此之外,也可以使用atomic類模板定義出任意原子類型。比如上述代碼還可以改為:

void func(atomic& n, int times)
{for (int i = 0; i< times; i++)
	{n++;
	}
}
int main()
{atomicn = 0;
	int times = 100000; //每個線程對n++的次數(shù)
	thread t1(func, ref(n), times);
	thread t2(func, ref(n), times);

	t1.join();
	t2.join();
	cout<< n<< endl; //打印n的值
	return 0;
}

說明一下:

  • 原子類型通常屬于“資源類型”數(shù)據(jù),多個線程只能訪問單個原子類型的拷貝,因此在C++11中,原子類型只能從其模板參數(shù)中進(jìn)行構(gòu)造,不允許原子類型進(jìn)行拷貝構(gòu)造、移動構(gòu)造以及operator=等。
  • 為了防止意外,標(biāo)準(zhǔn)庫已經(jīng)將atomic模板類中的拷貝構(gòu)造、移動構(gòu)造、operator=默認(rèn)刪除掉了。
  • 原子類型不僅僅支持原子的++操作,還支持原子的--、加一個值、減一個值、與、或、異或操作。
條件變量庫(condition_variable)

condition_variable中提供的成員函數(shù),可分為wait系列和notify系列兩類。

wait系列成員函數(shù)

wait系列成員函數(shù)的作用就是讓調(diào)用線程進(jìn)行阻塞等待,包括wait、wait_forwait_until。

下面先以wait為例進(jìn)行介紹,wait函數(shù)提供了兩個不同版本的接口:

//版本一
void wait(unique_lock& lck);
//版本二
templatevoid wait(unique_lock& lck, Predicate pred);

函數(shù)說明:

  • 調(diào)用第一個版本的wait函數(shù)時只需要傳入一個互斥鎖,線程調(diào)用wait后會立即被阻塞,直到被喚醒。
  • 調(diào)用第二個版本的wait函數(shù)時除了需要傳入一個互斥鎖,還需要傳入一個返回值類型為bool的可調(diào)用對象,與第一個版本的wait不同的是,當(dāng)線程被喚醒后還需要調(diào)用傳入的可調(diào)用對象,如果可調(diào)用對象的返回值為false,那么該線程還需要繼續(xù)被阻塞。

為什么調(diào)用wait系列函數(shù)時需要傳入一個互斥鎖?

  • 因?yàn)閣ait系列函數(shù)一般是在臨界區(qū)中調(diào)用的,為了讓當(dāng)前線程調(diào)用wait阻塞時其他線程能夠獲取到鎖,因此調(diào)用wait系列函數(shù)時需要傳入一個互斥鎖,當(dāng)線程被阻塞時這個互斥鎖會被自動解鎖,而當(dāng)這個線程被喚醒時,又會自動獲得這個互斥鎖。
  • 因此wait系列函數(shù)實(shí)際上有兩個功能,一個是讓線程在條件不滿足時進(jìn)行阻塞等待,另一個是讓線程將對應(yīng)的互斥鎖進(jìn)行解鎖。

wait_for和wait_until函數(shù)的使用方式與wait函數(shù)類似:

  • wait_for函數(shù)也提供了兩個版本的接口,只不過這兩個版本的接口都比wait函數(shù)對應(yīng)的接口多了一個參數(shù),這個參數(shù)是一個時間段,表示讓線程在該時間段內(nèi)進(jìn)行阻塞等待,如果超過這個時間段則線程被自動喚醒。
  • wait_until函數(shù)也提供了兩個版本的接口,只不過這兩個版本的接口都比wait函數(shù)對應(yīng)的接口多了一個參數(shù),這個參數(shù)是一個具體的時間點(diǎn),表示讓線程在該時間點(diǎn)之前進(jìn)行阻塞等待,如果超過這個時間點(diǎn)則線程被自動喚醒。
  • 線程調(diào)用wait_for或wait_until函數(shù)在阻塞等待期間,其他線程調(diào)用notify系列函數(shù)也可以將其喚醒。此外,如果調(diào)用的是wait_for或wait_until函數(shù)的第二個版本的接口,那么當(dāng)線程被喚醒后還需要調(diào)用傳入的可調(diào)用對象,如果可調(diào)用對象的返回值為false,那么當(dāng)前線程還需要繼續(xù)被阻塞。

注意: 調(diào)用wait系列函數(shù)時,傳入互斥鎖的類型必須是unique_lock。

notify系列成員函數(shù)

notify系列成員函數(shù)的作用就是喚醒等待的線程,包括notify_onenotify_all

  • notify_one:喚醒等待隊(duì)列中的首個線程,如果等待隊(duì)列為空則什么也不做。
  • notify_all:喚醒等待隊(duì)列中的所有線程,如果等待隊(duì)列為空則什么也不做。

注意: 條件變量下可能會有多個線程在進(jìn)行阻塞等待,這些線程會被放到一個等待隊(duì)列中進(jìn)行排隊(duì)。

實(shí)現(xiàn)兩個線程交替打印1-100

嘗試用兩個線程交替打印1-100的數(shù)字,要求一個線程打印奇數(shù),另一個線程打印偶數(shù),并且打印數(shù)字從小到大依次遞增。

該題目主要考察的就是線程的同步和互斥。

  • 互斥:兩個線程都在向控制臺打印數(shù)據(jù),為了保證兩個線程的打印數(shù)據(jù)不會相互影響,因此需要對線程的打印過程進(jìn)行加鎖保護(hù)。
  • 同步:兩個線程必須交替進(jìn)行打印,因此需要用到條件變量讓兩個線程進(jìn)行同步,當(dāng)一個線程打印完再喚醒另一個線程進(jìn)行打印。

但如果只有同步和互斥是無法滿足題目要求的。

  • 首先,我們無法保證哪一個線程會先進(jìn)行打印,不能說先創(chuàng)建的線程就一定先打印,后創(chuàng)建的線程先打印也是有可能的。
  • 此外,有可能會出現(xiàn)某個線程連續(xù)多次打印的情況,比如線程1先創(chuàng)建并打印了一個數(shù)字,當(dāng)線程1準(zhǔn)備打印第二個數(shù)字的時候線程2可能還沒有創(chuàng)建出來,或是線程2還沒有在互斥鎖上進(jìn)行等待,這時線程1就會再次獲取到鎖進(jìn)行打印。

鑒于此,這里還需要定義一個flag變量,該變量的初始值設(shè)置為true。

  • 假設(shè)讓線程1打印奇數(shù),線程2打印偶數(shù)。那么就讓線程1調(diào)用wait函數(shù)阻塞等待時,傳入的可調(diào)用對象返回flag的值,而讓線程2調(diào)用wait函數(shù)阻塞等待時,傳入的可調(diào)用對象返回!flag的值。
  • 由于flag的初始值是true,就算線程2先獲取到互斥鎖也不能進(jìn)行打印,因?yàn)樽铋_始線程2調(diào)用wait函數(shù)時,會因?yàn)榭烧{(diào)用對象的返回值為false而被阻塞,這就保證了線程1一定先進(jìn)行打印。
  • 為了讓兩個線程交替進(jìn)行打印,因此兩個線程每次打印后都需要更改flag的值,線程1打印完后將flag的值改為false并喚醒線程2,這時線程2被喚醒時其可調(diào)用對象的返回值就變成了true,這時線程2就可以進(jìn)行打印了。
  • 當(dāng)線程2打印完后再將flag的值改為true并喚醒線程1,這時線程1就又可以打印了,就算線程2想要連續(xù)打印也不行,因?yàn)槿绻€程1不打印,那么線程2的可調(diào)用對象的返回值就一直為false,對于線程1也是一樣的道理。

代碼如下:

int main()
{int n = 100;
	mutex mtx;
	condition_variable cv;
	bool flag = true;
	//奇數(shù)
	thread t1([&]{int i = 1;
		while (i<= 100)
		{	unique_lockul(mtx);
			cv.wait(ul, [&flag]()->bool{return flag; }); //等待條件變量滿足
			cout<< this_thread::get_id()<< ":"<< i<< endl;
			i += 2;
			flag = false;
			cv.notify_one(); //喚醒條件變量下等待的一個線程
		}
	});
	//偶數(shù)
	thread t2([&]{int j = 2;
		while (j<= 100)
		{	unique_lockul(mtx);
			cv.wait(ul, [&flag]()->bool{return !flag; }); //等待條件變量滿足
			cout<< this_thread::get_id()<< ":"<< j<< endl;
			j += 2;
			flag = true;
			cv.notify_one(); //喚醒條件變量下等待的一個線程
		}
	});

	t1.join();
	t2.join();
	return 0;
}

你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧


網(wǎng)站標(biāo)題:C++11———線程庫-創(chuàng)新互聯(lián)
本文來源:http://weahome.cn/article/dejses.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部