C++11 多線程編程 學習總結(下)

單例設計模式 

Class MyCAS //這是一個單例類
{
private:
	MyCAS() {} //私有化了構造函數
	
	Static MyCAS *m_instance; //靜態成員變量
	
public:
	Static MyCAS *GetInstance()
	{
		If(m_instance == NULL)
		{
			m_instance = new MyCAS();
			Static Cgarhuishou cl;
		}
		return m_instance;
	}
	Class CGarhuishou //類中套類,用來釋放對象
	{
	Public:
		~CGarhuishou ()
		{
			If(MyCAS::m_instance)
			{
				Delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	}
	Void func()
	{
		Cout << "測試" << endl;
	}
}

單例設計模式共享數據問題分析、解決

面臨的問題:在自己的線程中創建單例設計模式類,即不是主線程,而且這種線程不止一個,就可能面臨需要getInStance成員函數互斥。

解決:在創建對象的函數中使用,再使用雙重檢查(雙重鎖定)的寫法提高效率。

Class MyCAS //這是一個單例類
{
private:
	MyCAS() {} //私有化了構造函數
	
	static MyCAS *m_instance; //靜態成員變量
    static std::mutex mtx;
	
public:
	static MyCAS *GetInstance()
	{
        //雙重鎖定。因爲只是需要在第一次new的時候加鎖,爲了防止每次都要加鎖,加上兩個判斷。效率提高。
        if(m_instance == NULL){
            std::unique_lock<std::mutex> uniLock(mtx);
            if(m_instance == NULL)
            {
                m_instance = new MyCAS();
                static Cgarhuishou cl;
            }
        }
        return m_instance;
	}
	class CGarhuishou //類中套類,用來釋放對象
	{
	public:
		~CGarhuishou ()
		{
			If(MyCAS::m_instance)
			{
				Delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	}
	void func()
	{
		cout << "測試" << endl;
	}
}
//靜態成員默認初始化
MyCAS *MyCAS::m_instance = nullptr;

call_once

  1. c++11引入的函數,該函數的第二個參數是一個函數名a();
  2. call_once功能是能夠保證函數a()只被調用一次;
  3. call_once具備互斥量這種能力,而且效率上比互斥量消耗的資源更少;
  4. call_once()需要與一個標記結合使用,這個標記std::once_flag;其實once_flag是一個結構;
  5. call_once()就是通過這個標記來決定對應的函數a()是否執行,調用call_once()成功後,call_once()就把這個標記設置爲一種已調用狀態;
  6. 後續再次調用call_once(),只要once_flag被設置爲了“已調用”狀態,那麼對應的函數a()就不會再被執行了;
  7. 比如兩個線程執行到這,搶先的線程執行了 call_once(),另一個線程就必須等待,等到搶先的線程執行完畢,會通過標誌位oneFlag反饋給別的線程,以告知他們不用再執行這個函數,通過這種機制,實現這個函數的 call_once();
  8. 建議:在主線程中先創建單例對象,再創建線程。
Class MyCAS //這是一個單例類
{
private:
	MyCAS() {} //私有化了構造函數
	
	static MyCAS *m_instance; //靜態成員變量
    static std::once_flag oneFlag;

    static void createInstance(){ 
        m_instance = new MyCAS();
        static CGarhuishou cl;
    }
	
public:
	static MyCAS *GetInstance()
	{
        std::call_once(oneFlag, createInstance);//比如兩個線程執行到這,搶先的線程執行了 call_once(),另一個線程就必須等待,等到搶先的線程執行完畢,會通過標誌位oneFlag反饋給別的線程,以告知他們不用再執行這個函數,通過這種機制,實現這個函數的 call_once();
        return m_instance;
	}
	class CGarhuishou //類中套類,用來釋放對象
	{
	public:
		~CGarhuishou ()
		{
			If(MyCAS::m_instance)
			{
				Delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	}
	void func()
	{
		cout << "測試" << endl;
	}
}
//靜態成員默認初始化
MyCAS *MyCAS::m_instance = nullptr;

條件變量

std::condition_variable 條件變量:就是一個類,等待一個條件達成,和互斥量配和使用

1.wait() 和 notify()配合

  • void wait<_Predicate>(std::unique_lock<std::mutex> &__lock, _Predicate __p)
  • void wait(std::unique_lock<std::mutex> &__lock)

wait(mutex, predicate); 第二個參數默認是fasle,即wait()會unlock mutex的對象,但是會堵塞在wait()這一行,直到別的線程notify()執行,wait()纔會返回。

但是可以傳入一個predicate,比如lambda表達式,返回的結果如果是false,效果同上。返回true,wait立即返回,不堵塞。

2.別的線程執行了notify_one()後,首先notify所在的線程肯定要先unlock互斥量。然後wait()會再次獲得互斥量的鎖,對互斥量進行lock。對互斥量lock()後:

(1)如果wait()的第二個predicate有參數,會再次判斷predicate是否爲true,

         + 如果是true,則wait()返回,不再堵塞,但是此時並沒有對互斥量unlock,因爲下面還需對數據進行處理。wait()的行爲相當於等待別的線程產生的數據,這個線程接受、處理,所以wait返回後,依然處於lock狀態,待處理完數據,可以等到編譯器自己unlock,或者自己手動unlock。

         + 如果是false,則繼續堵塞,等待被notify()喚醒。重複上述過程。

(2)如果wait()的第二個predicate沒有參數,默認是true。效果和返回true一樣。

3.注意

(1)notify_once喚醒了wait()後,不一定wait()會成功對互斥量lock(),因爲可能notify()所在的線程,在notify對互斥量unlock()後會再次進行試圖對它lock。因此不一定Wait就會成功對互斥量lock成功。

         因此,wait()因爲多次沒有lock而積累太多數據,處理不過來怎麼辦???具體應用中應該考慮的問題----可以限流

(2)如果某個線程執行力notify_once,但是另一個線程的程序當前沒有堵塞在wait(),而是在別的地方執行,那麼執行了notify_once的執行毫無意義。

4.notify_all():notifies all waiting threads

5.虛假喚醒:wait()中要有第二個參數(lambda)並且這個lambda要正確判斷要處理的公共程序是否存在;

   wait(),notify_one(),notify_all()

線程返回一個結果

1.std::asyncstd::future<T>:希望線程返回一個結果

(1)std::async:函數模板,啓動一個異步任務,它返回一個std::future類模板對象。

(2)std::future<T>:類模板,常用函數,T是返回結果的類型

         + get()       //等待,直到獲取子線程返回值,解除堵塞。注意只能調用一次,因爲get()是用移動語義實現的,使用future.get()後,再次使用future.get()將變成nullptr

         + wait()     //等待,直到子線程結束,不需要返回值,解除堵塞

啓動一個異步任務,就是自動創建一個線程並且開始執行對應的線程入口函數,它返回一個std::future類模板對象。在這個對象裏,含有線程入口函數的返回結果,即線程返回的結果,我們可以通過std::future對象的成員函數get()獲取結果。

std::future,提供了一種訪問異步操作結果的機制,就是說這個結果你可能沒有辦法馬上拿到,在線程執行完畢的時候,你就能夠拿到結果了。

int myThread(){
    std::cout<<"subThread id: "<<std::this_thread::get_id()<<"...subThread start.\n";
    std::chrono::milliseconds time(5000);
    std::this_thread::sleep_for(time);
    std::cout<<"subThread id: "<<std::this_thread::get_id()<<"...subThread end.\n";
    return 5; //線程函數必須要返回值,get()會一直堵塞等待返回值
}
int main(int argc, char const *argv[]) {
    std::cout<<"main Thread id: "<<std::this_thread::get_id()<<"...run...\n";
    std::future<int> retVal = std::async(myThread);//線程開始執行,雖然線程函數會延遲5s,但是不會卡在這兒
    for (size_t i = 0; i < 10; i++) 
        std::cout<<"test..."<<i<<std::endl;
			        
    std::cout<<"subThread return value: "<<retVal.get()<<std::endl;  //如果線程函數沒有執行結束,而是會卡在這裏,因爲這裏需要這個線程函數的返回結果
}

比如,在上面的這個程序裏, std::future<int> retVal = std::async(myThread);啓動一個線程後,雖然線程函數裏有個5s的延時操作,主線程並不會等待這個子線程的完成才繼續執行下去,只有當主線程裏需要用到子線程的數據時,纔會等到子線程的結束,比如retVal.get()會使得主線程必須等到子線程結束。因此這個futute,可以理解爲讓主線程在將來用到子線程返回值時纔等到,否則不等。

這個功能就類似於 thread::join(),讓主線程等到子線程的完成,但是這個異步性質,可以等價準備的控制等待的時間,即,在哪等待。

比如:

std::future<int> retVal = std::async(myThread);
for (size_t i = 0; i < 1000; i++) 
    std::cout << "test_1..." << i << std::endl;
std::cout << "\nsubThread return value: " << retVal.get() << std::endl;          
for (size_t i = 0; i < 1000; i++) 
    std::cout << "test_2..." << i << std::endl;

第一個for循環,會和子線程爭奪資源,但是第二個線程必須等子線程結束才能執行,如果使用join(),第一for循環結束,第二個就會執行。使用異步性質,可以控制程序的執行順序。

注意:如果沒有調用get()或者wait(),那麼子線程會在main()return前將子線程執行結束。相當於編譯器自己在主線程結束前幫你寫了一個wait()。

2.std::async

第一個參數:std::launch類型,是一個枚舉類型,來實現特殊的目的:

/// Launch code for futures
enum class launch
{
    async = 1,
    deferred = 2
};
std::future<int> retVal = std::async(std::launch::deferred,&Async::myThread, &ayc, val);

(1)std::launch::deferred:

  • the task is executed on the calling thread the first time its result is requested (lazy evaluation)
  • 即,這個標誌位下,子線程根本不會被創建,直到get()/wait()相應的入口函數纔會被執行;沒有get()/wait()入口函數就不會被執行。
  • 而且,在有get()/wait()時,線程的入口函數是在調用get()/wait()所在線程執行的

(2)std::launch::async

  • a new thread is launched to execute the task asynchronously
  • 即,顯式的創建一個新的線程執行線程入口函數。

(3)std::launch::deferred | std::launch::asysnc

  • if both the std::launch::async and std::launch::deferred flags are set in policy, it is up to the implementation whether to perform asynchronous execution or lazy evaluation.
  • 系統的默認的標誌位,即當第一個參數不傳入的時候,和顯示的設置std::launch::deferred | std::launch::async效果一樣,即取決於自己的實現,執行哪個。
  • 此時可能創建新的線程執行異步任務(std::launch::async),也有可能在調用異步任務返回值的線程裏直接調用這個異步任務函數而不創建新的線程(std::launch::deferred)。因此可以配合future_status一起使用,來確定這個異步任務執行情況。
int myThread(){
    std::chrono::milliseconds time(5000);  //等待時間爲5s
	std::this_thread::sleep_for(time);
    std::cout<<"subThread id: "<<std::this_thread::get_id()<<" |end."<<"\n";
	std::cout<<"sub|子線程執行結束.\n";
	return 5;
}
			
int main(int argc, char const *argv[]){
	std::future<int> ret = std::async(myThread);
	std::future_status stus = ret.wait_for(std::chrono::seconds(0)); //等待子線程的時間        
	if(stus == std::future_status::deferred){
	    //系統資源緊張了,它採用了std::launch::deferred策略
	    std::cout<<"main|異步任務延遲執行|返回值: "<<ret.get()<<std::endl;
	}
	else {
        //系統創建了新線程
        if(stus == std::future_status::ready){
            std::cout<<"main|異步任務成功返回|返回值: "<<ret.get()<<std::endl;
	    }
        else if(stus == std::future_status::timeout){
            std::cout<<"timeout|異步任務timeout.\n"<<ret.get()<<std::endl;
        }
    }
				        
    std::cout<<"Main thread|id: "<<std::this_thread::get_id()<<std::endl;
    return 0;
}

3.std::packaged_task

std::packaged_task是個類模板,模板參數是各種可調用對象,通過std::packaged_task把各種可調用對象包裝起來,以作爲線程入口函數。

用法如下:

int myThread(int val){
    /***代碼***/
}
int main(int argc, char const *argv[])
{
    std::cout << "current id: " << std::this_thread::get_id() << std::endl;
    std::packaged_task<int(int)> pkg(myThread);
    std::thread trd(std::ref(pkg), 10);
    trd.join();
		
    std::future<int> ret = pkg.get_future();
    std::cout << ret.get() << std::endl;
    std::cout << "Main thread.\n";
    return 0;
}

std::packaged_task 對象將線程入口函數myThread進行封裝,然後傳入std::thread 對象,和普通線程一樣,調用join(),當再次調用ret.get()就不會再等待,前面join()已經等待子線程執行結束了。ret.get()就可以直接獲取值。如果不加join(),直接ret.get(),會報錯。

4.std::promise:類模板

能夠在某個線程中給它賦值,然後在其他線程中,把這個值取出來。std::promise<T> prom,可以作爲一個線程的參數,在這個線程裏進行某些運算,運算結果,保存在std::promise<T>對象之中,在另一個線程中,再將這個值取出來。當然,得確保這個std::promise<T>對象是同一個對象,因此需要使用引用傳遞參數。

與取值有關的函數是std::future<T> obj,通過prom.get_futrue();就可以取出furture對象,然後future.get()取出這個值。

//這個線程計算
void mythread(std::promise<int>& prom, int val){
    val++;
    //假設這個線程花了2s, 得到了運算結果
    prom.set_value(val);
    return;
}
			
//這個線程使用上面那個線程的計算結果
void myTrd(std::future<int>& future){
    int ret = future.get();
    std::cout<<"myTrd val: "<<ret<<std::endl;
    return;
}

int main(int argc, char const *argv[]){			   
    std::promise<int> prom; //int 爲保存的數據類型,通過這個prom實現兩個線程的數據交互
			
    std::thread trd(mythread, std::ref(prom), 10);
    std::future<int> future = prom.get_future();   // 獲取結果值
    std::thread trd2(myTrd, std::ref(future));    // 傳遞給第二個線程
			
    std::cout<<"main thread.\n";
    trd.join();
    trd2.join();
    return 0;
}

5.總結

std::asysnc

std::packaged_task,

std::promise:更像是一個傳遞數據的變量,通過它在線程之間傳遞數據。

(1)他們都可以和std::future<T>配合使用,通過std::future<T>來取出線程中自己需要的值,方式有所不同。

  • std::async是函數模板,函數返回值就是std::future<T>對象
  • td::packaged_taskstd::promise他們的對象有get_future方法,可以得到std::future<T>對象,再獲取相應的值。其中T是獲取的值類型。他們都需要結合std::thread 進行使用,然後別忘記 join()。

(2)std::async

(a)async如果創建子線程,如果子線程還沒結束,主線程任務已經執行結束,那麼主線程會在結束前將子線程任務執行結束再返回。

(b)async更加準確的叫法是,創建一個異步任務,但並不一定創建一個子線程,

         + std::launch:deferred傳入時,誰調用get就當前線程裏創建異步任務,並沒有創建新的子線程;

         + std::launch::async傳入時,強制創建子線程執行異步任務。

(c)std::asysncstd::thread區別

         + thread創建線程,如果系統資源緊張,創建線程失敗,那麼整個程序就會報異常崩潰(有脾氣),而async會強制創建一個新的線程

         + thread如果想獲取線程的返回值,或者一些自己需要的中間值,不容易實現。但是async返回的是std::future<T>對象,或者std::share_futute<T>對象,可以方便的獲取返回值。

(d)由於系統資源限制:

         + 如果用std::thread創建的線程太多,則可能創建失敗,系統報告異常,奔潰。

         + 如果用std::async,一般就不會報異常不會奔潰,因爲,如果系統資源緊張導致無法創建新線程的時候,

            std::async這種不加額外參數的調用就不會創建新線程。而是後續誰調用了result.get()來請求結果,那麼這個異步任務就運行在執行這條get()語句所在的線程上。

            如果強制用std::async一定要創建新線程,那麼就必須使用std::launch::async。承受的代價就是系統資源緊張時,程序奔潰。

         經驗:一個程序裏,線程數量不宜超過100-200。

6.future_status

enum class future_status{
    ready,
    timeout,
    deferred
};

使用:

std::future<int> ret = std::async(std::launch::deferred,myThread, 10);

std::future_status stus = ret.wait_for(std::chrono::seconds(6)); //等待子線程的時間

  • timeout:表示子線程的執行時間,超過主線程等待子線程的時間設定值,就會觸發timeout。但是即便如此,std::async函數模板創建的子線程依然會在主線程返回前執行完。
  • ready:表示線程成功返回。
  • deferred:在使用std::async函數模板創建子線程,並且第一個參數設置爲std::launch::deferred時,這個deferred纔會有效。同時,子線程也是等到std::future<T>obj.get()纔會執行,並且是在主線程中執行。

7.std::shared_future

由於future.get()只能調用一次,所以要想實現不同線程之間通過future實現數據共享,那麼怎麼辦?使std::shared_future<T> ,它也是個類模板

用法:

std::packaged_task<int(int)> pkg(myThread);
std::thread trd(std::ref(pkg), 10);
trd.join();
	
// std::future<int> ret_1 = pkg.get_future();
		
// std::shared_future<int> ret(pkg.get_future());
//std::shared_future<int> ret = pkg.get_future();
std::shared_future<int> ret(std::move(ret_1));

這樣ret就可以反覆的回去線程的返回值,顧名思義,share_future<T>是將返回值通過複製的方獲取,所以很安全,可以多次使用。

原子操作

std::atomic

情況:

有兩個線程,一個線程對一個變量進行讀取操作,另一個線程對一個變量進去寫操作,如果任由兩個線程自由進行操作,最終讀取到的值,可能就不是當前值,也不是寫線程操作之後的值,或許是一個不可預料的中間值。

比如兩個線程同時對一個變量進行寫操作,

int commVar = 0;
std::mutex mtx;
void Write(){
    for (size_t i = 0; i < 1000000; i++) {
        mtx.lock(); 
        commVar++;
        mtx.unlock();
    }
    return;
}
			
int main(int argc, char const *argv[]){
	std::thread t1(Write);
    std::thread t2(Write);
	t1.join();
	t2.join();
			
	std::cout<<commVar<<std::endl;
	std::cout<<"main thread.\n";
}

需要通過互斥量mutex,來保持兩個線程的有序進行,如果不加鎖輸出的值不可預料,不是理想的結果。現在提供一次新的技術,即原子操作技術,不需要加鎖也能保證多個線程對同一塊數據進行有序訪問,要麼訪問到的是其餘線程沒有對這個數據進行操作前的值(即原來的值),或者是被別的線程改動後的值,而不是中間值。

原子操作:在程序執行中,不會被別的線程打斷的程序片段。注意了,原子操作針對的是單個變量,而不是大段的代碼段,大段的代碼還是需要mutex實現。

原子操作狀態:要麼是完成的,要麼是未完成的,不可能出現半中間狀態。

std::atomic操作:其餘不變

std::atomic<int> commVar(10);
			
void Write(){
	for (size_t i = 0; i < 1000000; i++)  
        commVar++;
	return;
}

注意原子操作支持的變量操作:++、--,+=,-=,&=,之類;不支持 var = var+1 之類。

拷貝構造函數,拷貝構造運算符不能用

atomic<int> atm2(atm.load())

auto atm2(atm.load())

Load()以原子方式讀

Store()以原子方式寫

window臨界區

#include<windows.h>

1.臨界區基本用法,類似mutex的lock()、unlock()

EnterCriticalSection(&winsSec);
msg.push_back(i);
LeaveCriticalSection(&winsSec);

2.在“同一個線程”(不同線程就會卡住等待)中,windows中的“相同臨界區變量”代表的臨界區的進入(EnterCriticalSection)可以被多次執行,但是你調用了幾次EnterCriticalSection,你就得調用幾次LeaveCriticalSection;

而在c++11中,不允許同一個線程中lock同一個互斥量多次,否則報異常

EnterCriticalSection(&winsSec); //ok
EnterCriticalSection(&winsSec);
msg.push_back(i);
LeaveCriticalSection(&winsSec);
LeaveCriticalSection(&winsSec);

3.自動析構技術:

windows臨界區實現mutex的自動lock()和unlock()操作(std::lock_guard(std::mutex))。

RAII(resource aquisition is initialization)類,“資源獲取即初始化”,容器,智能指針這種類,都屬於RAII類

在構造函數裏進行初始化,在析構函數裏進行釋放。

class uniLockWins{
private:
	CRITICAL_SECTION *_critical_sec;
public:
    uniLockWins(CRITICAL_SECTION *sec){
	    _critical_sec = sec;
	    EnterCriticalSection(_critical_sec);
	}
	~uniLockWins(){LeaveCriticalSection(_critical_sec);}
};

補充

1.recursive_mutex:允許同一個線程同一個互斥量多次lock()/unlock()

  • 效率更低
  • 應該考慮重構代碼
  • 遞歸次數據說有限制,多次調用可能會異常

2.帶超時的互斥量std::timed_mutex和std::recursive_timed_mutex

(1)std::timed_mutex:是帶超時功能的獨佔互斥量

         Try_lock_for() : 參數是一段時間,是等待一段時間,如果我拿到鎖,或者等待超過時間沒拿到鎖,就走下來;

         Try_lock_until() :   參數是一個未來的時間點,在這個未來的時間沒到的時間內,如果拿到了鎖,那麼就走下來

(2)std::recursive_timed_mutex:是帶超時功能的遞歸獨佔互斥量

3.淺談線程池

(1)場景設想

        服務器程序,--》客戶端,每來 一個客戶端,就創建 一個線程爲該客戶提供服務。

a)網絡遊戲,2萬玩家不可能給每個玩家創建個新線程,此程序寫法在這種場景下不通;

b)程序穩定性問題:編寫的代碼中,偶爾創建一個線程這種代碼,這種寫法,就讓人感到不安;

線程池:把一堆線程弄到一起,統一管理。這種統一管理調度,循環利用線程的方式,就叫線程池;

(2)實現方式

在程序啓動時,我一次性創建好一定數量的線程。10,8,100-200,更讓人放心,覺得程序代碼更穩定;

4.線程創建數量談

(1)線程開的數量極限問題,2000個線程基本就是極限,再創建線程就崩潰;

(2)線程創建數量建議

a)採用某些技術開發程序;api提供商建議 創建線程的數量 = cpu數量, cpu*2, cpu*2+2, 遵照專業建議和指示來,專業意見確保程序高效率執行;

b)創建多線程完成業務;一個線程等於一條執行通路;100要堵塞充值,我們這裏開110個線程,那是很合適的;

c)1800個線程,建議,線程數量儘量不要超過500個,能控制在200個之內。

 

 

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