天天看點

C++多線程程式設計 (1)

       對于單處理器系統,處理器在一個單元時間内隻能執行一個程序,作業系統系統以極快的速度在多個程序之間進行切換,營造了一種多個程序同時運作的假象。

1. 一些基本概念:

c++中的靜态庫與動态庫:

1. 靜态庫:*.lib 是指一些已經編譯過的代碼,在程式運作之前,靜态庫在編譯的時候被放入到可執行檔案中。

     靜态庫在連結階段,會将彙編生成的目标檔案.o與引用到的庫一起連結打包到可執行檔案中,對應的連結方式稱為靜态連結。靜态庫與彙編生成的目标檔案(.o檔案)一起連結為可執行檔案,那麼靜态庫必定跟.o檔案格式相似。其實一個靜态庫可以簡單看成是一組目标檔案(.o/.obj檔案)的歸檔集合,即很多目标檔案經過壓縮打包後形成的一個檔案。

靜态庫有兩個重大缺點:

    1)空間浪費

    2)靜态連結對程式的更新、部署和釋出會帶來很多麻煩。一旦程式中有任何子產品更新,整個程式就要重新連結,釋出給使用者。

2. 動态庫:*.dll 與靜态庫不同的是,動态庫在程式開始執行後才開始進行連結,可以将許多程式都會用到的函數放入到動态庫中。在這樣就不必在每個程式中都包含這些函數了,隻需在運作時連結一個動态庫就可以了。

    動态庫在程式編譯時并不會被連接配接到目标代碼中,而是在程式運作是才被載入。不同的應用程式如果調用相同的庫,那麼在記憶體裡隻需要有一份該共享庫的執行個體,規避了空間浪費問題。動态庫在程式運作是才被載入,也解決了靜态庫對程式的更新、部署和釋出頁會帶來麻煩。使用者隻需要更新動态庫即可,增量更新。

C++多線程程式設計 (1)

動态庫特點:

1)代碼共享,所有引用該動态庫的可執行目标檔案共享一份相同的代碼與資料。

2)程式更新友善,應用程式不需要重新連結新版本的動态庫來更新,理論上隻要簡單地将舊的目标檔案覆寫掉。

3)在運作時可以動态地選擇加載各種應用程式子產品

程序和線程的概念:

在多任務系統中,CPU以極快的速度在不同程序之間切換,每個程序隻運作幾毫秒,從嚴格意義上來說,cpu在任何時刻隻運作一個程序,隻不過它的快速切換營造了并行處理的假象。每個程序都有自己的虛拟位址空間和控制線程。線程是作業系統排程器配置設定處理器時間的基礎單元,可以将系統看作運作在準并行環境中的程序集合,在程序間快速反複的切換叫做多任務處理。通常,在一個程序的位址空間中要執行多個線程。

感覺這篇部落格對于線程和程序的解釋很清晰:程序和線程的主要差別(C++多線程程式設計實戰這本書完全連這個概念都沒有講清楚):具體解釋如下

差別:程序是作業系統資源配置設定的基本機關,而線程是任務排程和執行的基本機關

在開銷方面:每個程序都有獨立的代碼和資料空間(程式上下文),程式之間的切換會有較大的開銷;線程可以看做輕量級的程序,同一類線程共享代碼和資料空間,每個線程都有自己獨立的運作棧和程式計數器(PC),線程之間切換的開銷小。

所處環境:在作業系統中能同時運作多個程序(程式);而在同一個程序(程式)中有多個線程同時執行(通過CPU排程,在每個時間片中隻有一個線程執行)

記憶體配置設定方面:系統在運作的時候會為每個程序配置設定不同的記憶體空間;而對線程而言,除了CPU外,系統不會為線程配置設定記憶體(線程所使用的資源來自其所屬程序的資源),線程組之間隻能共享資源。

包含關系:沒有線程的程序可以看做是單線程的,如果一個程序内有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的;線程是程序的一部分,是以線程也被稱為輕權程序或者輕量級程序。

線程:

c++中通過std中的thread方法建立函數:

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>

using std::cout;
using std::endl;
using std::thread;

void func()
{
	// do something 
}

int main()
{
	thread t(func);
	t.join();      // join()函數會阻塞線程,直到線程執行完畢
    return 0;
}
           

關于線程的阻塞:

在某一時刻某一個線程在運作一段代碼的時候,這時候另一個線程也需要運作,但是在運作過程中的那個線程執行完成之前,另一個線程是無法擷取到CPU執行權的(調用sleep方法是進入到睡眠暫停狀态,但是CPU執行權并沒有交出去),這個時候就會造成線程阻塞。

出現線程阻塞的原因:(參考:https://blog.csdn.net/sunshine_2211468152/article/details/87299708)

1. 睡眠狀态

當一個線程執行代碼的時候調用了sleep方法,線程處于睡眠狀态,此時有其他線程需要執行時就會造成線程阻塞,而且sleep方法被調用之後,線程不會釋放鎖對象,也就是說鎖還在該線程手裡,CPU執行權還在自己手裡,等睡眠時間一過,該線程就會進入就緒狀态。

2. 禮讓狀态:

當一個線程正在運作時,調用了yield方法之後,該線程會将執行權禮讓給同等級的線程或者比它高一級的線程優先執行,此時該線程有可能隻執行了一部分而此時把執行權禮讓給了其他線程,這個時候也會進入阻塞狀态,但是該線程會随時可能又被配置設定到執行權,比較講究謙讓;

3.等待狀态:

當一個線程正在運作時,調用了wait方法,此時該線程需要交出CPU執行權,也就是将鎖釋放出去,交給另一個線程,該線程進入等待狀态,但與睡眠狀态不一樣的是,進入等待狀态的線程不需要設定睡眠時間,但是需要執行notify方法或者notifyall方法來對其喚醒,自己是不會主動醒來的,等被喚醒之後,該線程也會進入就緒狀态,但是進入僅需狀态的該線程手裡是沒有執行權的,也就是沒有鎖,而睡眠狀态的線程一旦蘇醒,進入就緒狀态時是自己還拿着鎖的

4. 自閉狀态:

當一個線程正在運作時,調用了一個join方法,此時該線程會進入阻塞狀态,另一個線程會運作,直到運作結束後,原線程才會進入就緒狀态。這個比較像是”走後門“,本來該先把你的事情解決完了再解決後邊的人的事情,但是這時候有走後門的人,那就會停止給你解決,而優先把走後門的人事情解決了;

------------------------------------------------------------------------------------------------------------------------

如果不希望線程被阻塞,可以調用detach方法,将線程和線程對象分離:

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>

using std::cout;
using std::endl;
using std::thread;

void func()
{
	// do something 
}

int main()
{
	thread t(func);
	t.detach();      // join()函數會阻塞線程,直到線程執行完畢
      // do other thing 
      return 0;
}
           

調用detach方法後,線程和線程對象就分離了,讓線程作為背景線程去執行,目前線程也不會阻塞,但是datech之後就無法再和線程發生聯系了。例如,detach之後線程無法再進行join(),線程何時執行完我們也無法控制。

注:

std::thread出了作用域之後将會析構。這時如果線程函數沒有執行完畢将會發生錯誤,是以應該保證線程函數的生命周期線上程變量std::thread的生命周期之内。

void func()
{
	// do something 
}

int main()
{
	thread t(func);
	return 0;
}
           

例如,上面的程式可能會報錯,因為線程對象可能會先于線程函數結束。是以應該用join()阻塞線程或者用detach讓線程在背景運作。

此外,可以将線程對象儲存到一個容器中,以保證線程對象的生命周期:

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <vector>

using std::cout;
using std::endl;
using std::thread;
using std::vector;

vector<thread> g_list;
vector<std::shared_ptr<thread>> g_list2;

void func()
{
	// do something 
}

void CreateThread()
{
	thread t(func);
	g_list.push_back(std::move(t));    // 将線程對象儲存到容器中
	g_list2.push_back(std::make_shared<thread>(func));
}


int main()
{
	CreateThread();
	for (auto& thread : g_list)
	{
		thread.join;
	}

	for (auto& thread : g_list2)
	{
		thread->join();
	}

    return 0;
}

           

線程不支援複制,但是可以将線程移動:

void func()
{
	// do something 
}

int main()
{
	thread t(func);
	thread t1(std::move(t));   // 移動語義
    return 0;
}
           

線程被移動之後,線程對象t将不再代表任何線程了。

線程的基本使用方法:

1. 擷取線程ID,擷取CPU核數

void func()
{
	//do something
}

int main()
{
	thread t(func);
	t.get_id();    // 擷取線程ID
	cout << thread::hardware_concurrency() << endl;   // 擷取CPU核心數
    return 0;
}
           

2,線程休眠:

void func()
{
	std::this_thread::sleep_for(std::chrono::seconds(3));   // 線程休眠
	cout << "finish sleep" << endl;
}

int main()
{
	thread t(func);
	t.join();
    return 0;
}
           

多線程中的互斥量:

互斥量是一種同步語句,是一種線程同步的手段,用來保護多線程同時通路的共享資料:

C++中提供了四種互斥量:

std::mutext:   獨占的互斥量,不能遞歸使用

std::timed_mutex:   帶逾時的互斥量,不能遞歸使用。

std::recursive_mutex:  遞歸互斥量,不帶逾時功能。

std::recursive_timed_mutex:  帶逾時的遞歸互斥量

1. std::mutext:   獨占的互斥量,不能遞歸使用

互斥量的基本接口很相似,一般都是通過lock()方法來阻塞線程,直到獲得互斥量的所有權為止,線上程獲得互斥量并完成任務之後,使用unlock來解除對互斥量的占用。lock(),unlock()應成對出現。try_lock()嘗試鎖定互斥量,成功傳回true,否則為false,是以它是非阻塞的。

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

mutex g_lock;   // 獨占互斥量
void func()
{
	g_lock.lock();   // 鎖
	cout << "Enter thread " << std::this_thread::get_id() << endl;
	std::this_thread::sleep_for(std::chrono::seconds(2));
	cout << "Leaving thread " << std::this_thread::get_id() << endl;
	g_lock.unlock();
}


int main()
{
	thread t1(func);
	thread t2(func);
	thread t3(func);

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

           

結果如下圖所示:

C++多線程程式設計 (1)

一般推薦使用lock_guard()來替代lock/unlock,因為更加安全。lock_guard在構造時會自動鎖定互斥量,在退出作用後進行析構時就會自動解鎖互斥量,避免忘記unlock操作。

lock_guard用到了RAII技術:

使用局部對象管理資源的技術通常稱為“資源擷取就是初始化”,即Resource Acquisition Is Initialization 機制。這一機制是Bjarne Stroustrup首先提出的,要解決的是這樣一個問題:

          在C++中,如果在這個程式段結束時需要完成一些資源釋放的工作,那麼正常情況下自然是沒有什麼問題,但是當一個異常抛出時,釋放資源的語句就不會被執行。于是Bjarne Stroustrup就想到確定 能運作資源釋放代碼的地方就是在這個程式段(棧幀)中放置的對象的析構函數了,因為stack winding會保證它們的析構函數都會被執行。将初始化和資源釋放都放到一個包裝類中的好處:

         a. 保證了資源的正常釋放

         b. 省去了在異常進行中冗長而重複甚至有些還不一定執行到的清理邏輯,進而確定了代碼的異常安全。

         c. 簡化代碼體積。

所在lock_guard在類的構造函數中和會配置設定資源,在析構函數中釋放資源,保證資源在出了作用域之後就釋放,上面的例子使用lock_guard後會更加簡潔:

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

mutex g_lock;   // 獨占互斥量
void func()
{
	std::lock_guard<std::mutex> locker(g_lock);
	cout << "Enter thread " << std::this_thread::get_id() << endl;
	std::this_thread::sleep_for(std::chrono::seconds(2));
	cout << "Leaving thread " << std::this_thread::get_id() << endl;
}


int main()
{
	thread t1(func);
	thread t2(func);
	thread t3(func);

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

           

2. std::recursive_mutex:  遞歸互斥量,不帶逾時功能。

遞歸鎖允許同一線程多次獲得該互斥鎖,可以解決同一線程需要多次擷取互斥量時的死鎖的問題,一個線程多次擷取同一互斥量時會發生死鎖問題:

例如:

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

struct Complex
{
	std::mutex mutex;
	int i;

	Complex() : i(2) {};  // 構造函數

	void mul(int x)
	{
		std::lock_guard<std::mutex> lock(mutex);   // 擷取互斥量
		i *= x;
	}

	void div(int y)
	{
		std::lock_guard<std::mutex> lock(mutex);   // 擷取互斥量
		i /= y;
	}

	void both(int x, int y)
	{
		std::lock_guard<std::mutex> lock(mutex);   // 擷取互斥量
		mul(x);
		div(y);
	}

};


int main()
{
	Complex cmp;
	cmp.both(21, 2);
	cout << cmp.i << endl;
    return 0;
}

           

在主線程中多次擷取互斥量,就會發生死鎖。因為互斥量已被目前線程擷取,無法釋放,就會導緻這樣的問題。

遞歸鎖可以解決這種問題:

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

struct Complex
{
	std::recursive_mutex mutex;
	int i;

	Complex() : i(2) {};  // 構造函數

	void mul(int x)
	{
		std::lock_guard<std::recursive_mutex> lock(mutex);   // 擷取互斥量
		i *= x;
	}

	void div(int y)
	{
		std::lock_guard<std::recursive_mutex> lock(mutex);   // 擷取互斥量
		i /= y;
	}

	void both(int x, int y)
	{
		std::lock_guard<std::recursive_mutex> lock(mutex);   // 擷取互斥量
		mul(x);
		div(y);
	}

};


int main()
{
	Complex cmp;
	cmp.both(21, 2);
	cout << cmp.i << endl;
    return 0;
}

           

但是遞歸鎖會存在如下的缺點:

a. 用到遞歸鎖的多線程互斥處理本身是可以簡化的。遞歸互斥量很容易産生複雜的邏輯,會導緻線程同步引起的晦澀的問題。

b. 與非遞歸鎖相比,遞歸鎖的效率會更低。

c. 遞歸鎖沒有說明一個線程最多可以重複獲得幾次互斥量,一旦超過一定的次數,再調用lock就會抛出std::system的錯誤。

3. std::timed_mutex:   帶逾時的互斥量,不能遞歸使用。

timed_mutex在擷取鎖時增加逾時等待功能,因為有時候不知道擷取鎖需要等待多久,為了不至于一直在等待擷取互斥量,可以設定一個逾時時間,在逾時後還可以做其他的事情。多出的兩個接口為:try_lock_for, try_lock_until.

例如:

// ConsoleApplication1.cpp : 定義控制台應用程式的入口點。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;


void work()
{
	std::timed_mutex mutex;
	// 定義函數
	std::chrono::milliseconds timeout(100);  // 100ms
	while (true)
	{
		if (mutex.try_lock_for(timeout))  // 擷取到互斥量
		{
			cout << "Thread " << this_thread::get_id() << " do work in mutex" << endl;
			std::chrono::milliseconds sleep_time(250);
			this_thread::sleep_for(sleep_time);
			mutex.unlock();    // 釋放互斥量
			this_thread::sleep_for(sleep_time);
		}
		else   // 未擷取到互斥量  處理其他事務
		{
			cout << "Thread " << this_thread::get_id() << " do work not in mutex" << endl;
			std::chrono::milliseconds sleep_time(100);
			this_thread::sleep_for(sleep_time);
		}
	}
}


int main()
{
	thread t1(work);
	thread t2(work);
	t1.join();
	t2.join();
    return 0;
}


           

------------------------------------------------------------分割線------------------------------------------------------------------

繼續閱讀