c++11 多線程入門教程(一)


 

原文作者:aircraft

原文鏈接:https://www.cnblogs.com/DOMLX/p/10945309.html

    

 

本網絡編程入門系列博客是連載學習的,有興趣的可以看我博客其他篇。。。。c++ 網絡編程課設入門超詳細教程 ---目錄

 

 

      最近在找c++服務端開發的實習(大佬們有推薦嗎QAQ。。),恰好寫了一些c++11多線程有關的東西,就寫一下筆記留着以后自己忘記回來看吧,也不是專門寫給讀者看的,我就想到哪就寫到哪吧

 

  c++11呢,就是c++升級之后的一個版本,現在馬上就出c++20了,里面增加了很多對多線程支持的類,讓多線程編程更加簡單了,好了廢話不多說,先來建立一個簡單的多線程編程案例,看看c++11下多線程編程創建到底有多么的簡單。

 

1.創建一個簡單的多線程案例:

首先導入#include<thread>---用於創建線程

其次導入#include<chrono>--用於時間延時 獲取時間之類的

定義一個線程對象t1,這就自動創建了一個線程,參數就是你要線程去執行的函數,t1是變量名字 隨便取

std::thread t1(func);

下面這里返回一個毫秒級別的時間間隔參數值,間隔10毫秒 

std::chrono::milliseconds(10)

this_thread::sleep_for()就是讓此線程休眠,可以傳入休眠的時間

this_thread::sleep_for(std::chrono::milliseconds(10));讓本線程休眠10毫秒

 

好了知道這些參數意思就行了,看一下代碼:

#include<windows.h> #include <iostream> #include <chrono> #include <thread> using namespace std; int number = 1; int ThreadProc1() { while (number < 100) { cout << "thread 1 :" << number << endl; ++number; this_thread::sleep_for(std::chrono::milliseconds(10)); } return 0; } int ThreadProc2() { while (number < 100) { cout << "thread 2 :" << number << endl; ++number; this_thread::sleep_for(std::chrono::milliseconds(10)); } return 0; } int main() { thread t1(ThreadProc1); thread t2(ThreadProc2); t1.join(); t2.join(); system("pause"); return 0; }

  join()就是阻塞線程,直到線程函數執行完畢,如果函數有返回值,在這里會直接忽略。阻塞的目的就是讓Main主線程等待一下創建的線程,免得我函數還在跑,程序就直接結束了。

  如果不想阻塞在這里就將join()換成使用線程的detach()方法,將線程與線程對象分離,線程就可以繼續運行下去,並且不會造成影響。

  從示例可以看到c++11下創建多線程多么方便了吧 ,比在Linux下用posix創建還簡便,而這個也是可以在windows使用的(想想windows下多線程的代碼,看着都頭疼好吧,亂七八糟一大堆)。

 

2.互斥量的使用

  跟往常的多線程一樣,多線程在運行過程中都會對臨界區進行訪問,也就是一起訪問共享資源。這樣就會造成一個問題,當兩個線程都要對一個變量int value值假如為11,加一時,線程一取出11 進行加一還沒有存入value,這時候線程二又取得value的11進行加一,然后線程一存入12,線程二又存入12,這就導入兩個線程訪問沖突,也就是臨界區問題。所以引進互斥量來解決。

導入#include <mutex>

代碼案例:

一個線程對變量number進行加一100次,另外一個減一100次,最后結果應該還是原來的值0。

 

#include<windows.h>
#include <iostream> #include <chrono> #include <thread> #include <mutex> using namespace std; int number = 0; mutex g_lock; int ThreadProc1() { for (int i = 0; i < 100; i++) { g_lock.lock(); ++number; cout << "thread 1 :" << number << endl; g_lock.unlock(); this_thread::sleep_for(std::chrono::milliseconds(10)); } return 0; } int ThreadProc2() { for (int i = 0; i < 100; i++) { g_lock.lock(); --number; cout << "thread 2 :" << number << endl; g_lock.unlock(); this_thread::sleep_for(std::chrono::milliseconds(10)); } return 0; } int main() { thread t1(ThreadProc1); thread t2(ThreadProc2); t1.detach(); t2.detach(); system("pause"); return 0; }

 

 

上面的每次都要對mutex變量進行鎖以及解鎖,有時候忘記解鎖就涼涼了。所以c++11還提供了一個lock_guard類,它利用了RAII機制可以保證安全釋放mutex。

在std::lock_guard對象構造時,傳入的mutex對象(即它所管理的mutex對象)會被當前線程鎖住。在lock_guard對象被析構時,它所管理的mutex對象會自動解鎖,不需要程序員手動調用lock和unlock對mutex進行上鎖和解鎖操作。lock_guard對象並不負責管理mutex對象的生命周期,lock_guard對象只是簡化了mutex對象的上鎖和解鎖操作,方便線程對互斥量上鎖,即在某個lock_guard對象的生命周期內,它所管理的鎖對象會一直保持上鎖狀態;而lock_guard的生命周期結束之后,它所管理的鎖對象會被解鎖。程序員可以非常方便地使用lock_guard,而不用擔心異常安全問題。

代碼:

 

#include<windows.h> #include <iostream> #include <chrono> #include <thread> #include <mutex> using namespace std; int number = 0; mutex g_lock; int ThreadProc1() { lock_guard<mutex> loker(g_lock); for (int i = 0; i < 100; i++) { ++number; cout << "thread 1 :" << number << endl; } //this_thread::sleep_for(std::chrono::milliseconds(100)); return 0; } int ThreadProc2() { lock_guard<mutex> loker(g_lock); for (int i = 0; i < 100; i++) { --number; cout << "thread 2 :" << number << endl; //this_thread::sleep_for(std::chrono::milliseconds(10));  } return 0; } int main() { thread t1(ThreadProc1); thread t2(ThreadProc2); t1.detach(); t2.detach(); system("pause"); return 0; }

 

除了lock_guard,之外c++11還提供了std::unique_lock

類 unique_lock 是通用互斥包裝器,允許延遲鎖定、鎖定的有時限嘗試、遞歸鎖定、所有權轉移和與條件變量一同使用
unique_lock比lock_guard使用更加靈活,功能更加強大。
使用unique_lock需要付出更多的時間、性能成本。

#include <iostream>       // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock #include <vector> std::mutex mtx; // mutex for critical section std::once_flag flag; //定義一個once_flag類型的變量作為call_once參數, //用std::call_once來保證多線程環境中只被調用一次 void print_block (int n, char c) { //unique_lock有多組構造函數, 這里std::defer_lock不設置鎖狀態 std::unique_lock<std::mutex> my_lock (mtx, std::defer_lock); //嘗試加鎖, 如果加鎖成功則執行 //(適合定時執行一個job的場景, 一個線程執行就可以, 可以用更新時間戳輔助) if(my_lock.try_lock()){ for (int i=0; i<n; ++i) std::cout << c; std::cout << '\n'; } } void run_one(int &n){ std::call_once(flag, [&n]{n=n+1;}); //只執行一次, 適合延遲加載; 多線程static變量情況 } int main () { std::vector<std::thread> ver; int num = 0; for (auto i = 0; i < 10; ++i){ ver.emplace_back(print_block,50,'*'); ver.emplace_back(run_one, std::ref(num)); //emplace_back比push_back更好 是c++11增加的  } for (auto &t : ver){ t.join(); } std::cout << num << std::endl; return 0; } 

  

 這里還要補充一下跟互斥量很像的條件變量的知識。

條件變量std::condition_variable的使用

  std::condition_variable 是為了解決死鎖而生的。當互斥操作不夠用而引入的。比如,線程可能需要等待某個條件為真才能繼續執行,而一個忙等待循環中可能會導致所有其他線程都無法進入臨界區使得條件為真時,就會發生死鎖。所以,condition_variable實例被創建出現主要就是用於喚醒等待線程從而避免死鎖。std::condition_variable的 notify_one()用於喚醒一個線程;notify_all() 則是通知所有線程。
C++11中的std::condition_variable就像Linux下使用pthread_cond_wait和pthread_cond_signal一樣,可以讓線程休眠,直到別喚醒,現在在從新執行。線程等待在多線程編程中使用非常頻繁,經常需要等待一些異步執行的條件的返回結果。
示例代碼:

#include<iostream> #include<thread> #include<condition_variable> #include<mutex> #include<chrono> std::mutex g_mu; std::condition_variable g_vc; bool g_ready = false; void dispaly_id(int id) { std::unique_lock<std::mutex> lck(g_mu); g_vc.wait(lck, []() {return g_ready; }); //線程阻塞,直到第二個參數返回值為真  std::cout << "id:" << id << std::endl; } void ready() { std::unique_lock<std::mutex> lck(g_mu); g_ready = true; g_vc.notify_all(); //喚醒所有的等待線程 } int main() { std::thread t[8]; for (int i = 0; i < 8; i++) { t[i] = std::thread(dispaly_id, i); } std::this_thread::sleep_for(std::chrono::seconds(3)); std::cout << "all thread lock......" << std::endl; ready(); for (auto & th : t) th.join(); system("pause"); return 0; }

 

某年滴滴打車公司修改題目:有3個線程A,B, C, 請用多線程編程實現在屏幕上循環打印10次ABCABC..., 其中A線程打印“A”, B線程打印“B”, C線程打印“C”。  滴滴面試題修改為用十個線層,打印字母也變為十個。有興趣可自行修改

#include <iostream> #include <thread> #include <condition_variable> #include <vector> #include <algorithm> std::mutex mtx; std::condition_variable cvar; char g_ch = 0; void print_fun(char ch) { int cyle_cnt = 10; char ch_ = ch - 'A'; for (int i = 0; i < cyle_cnt; i++) { std::unique_lock<std::mutex>ulk(mtx); cvar.wait(ulk, [ch_] {return ch_ == g_ch; }); std::cout << (char)(ch_ + 'A'); g_ch = (ch_ + 1) % 3; ulk.unlock(); cvar.notify_all(); } } int main() { std::vector<std::thread> threads; threads.push_back(std::thread(print_fun, 'A')); threads.push_back(std::thread(print_fun, 'B')); threads.push_back(std::thread(print_fun, 'C')); std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); std::cout << std::endl; system("pause"); return 0; }

 

 

 

 

3.原子變量的使用

  在新標准C++11,引入了原子操作的概念,原子操作更接近內核,並通過這個新的頭文件提供了多種原子操作數據類型,例如,atomic_bool,atomic_int等等,如果我們在多個線程中對這些類型的共享資源進行操作,編譯器將保證這些操作都是原子性的,也就是說,確保任意時刻只有一個線程對這個資源進行訪問,編譯器將保證,多個線程訪問這個共享資源的正確性。從而避免了鎖的使用,提高了效率。

  上面我們用互斥鎖來實現加一百次,減少一百次。使用原子變量會更加簡潔。

 

#include<windows.h> #include <iostream> #include <chrono> #include <thread> #include <mutex> #include <atomic> using namespace std; atomic<int> number(0);//定義原子變量 一次只允許一個線程對其進行訪問 //int number = 0; //mutex g_lock; int ThreadProc1() { //lock_guard<mutex> loker(mutex); for (int i = 0; i < 100; i++) { ++number; cout << "thread 1 :" << number << endl; } //this_thread::sleep_for(std::chrono::milliseconds(100)); return 0; } int ThreadProc2() { //lock_guard<mutex> loker(mutex); for (int i = 0; i < 100; i++) { --number; cout << "thread 2 :" << number << endl; //this_thread::sleep_for(std::chrono::milliseconds(10));  } return 0; } int main() { thread t1(ThreadProc1); thread t2(ThreadProc2); t1.detach(); t2.detach(); system("pause"); return 0; }

 

可以看到使用了原子變量之后,代碼簡化了很多,以及以后對某些共享資源我們都可以酌情的定義為原子變量類型,很方便有木有。。。。。

 

 4.future與promise的使用

  在c++11中增加的線程庫很方便的讓我們去使用線程,但是因為做出了一些改變,我們並不能像往常一樣直接使用thread.join()獲取線程函數的返回值了,而我們有時候又確實要利用線程函數的返回值。

  而thread庫提供了future用來訪問異步操作的結果,因為一個異步操作的結果往往不能立即獲取,只能在未來的某個時候從某個地方獲取,這個異步操作的結果是一個未來的期待值,所以被稱為future

  future和promise的作用是在不同線程之間傳遞數據。

假設線程1需要線程2的數據,那么組合使用方式如下:

  1.     線程1初始化一個promise對象和一個future對象,promise傳遞給線程2,相當於線程2對線程1的一個承諾;future相當於一個接受一個承諾,用來獲取未來線程2傳遞的值
  2.     線程2獲取到promise后,需要對這個promise傳遞有關的數據,之后線程1的future就可以獲取數據了。
  3.     如果線程1想要獲取數據,而線程2未給出數據,則線程1阻塞,直到線程2的數據到達

示例代碼:

#include <iostream>
#include <chrono> #include <thread> #include <mutex> #include <atomic> #include <future> #include <vector> void disPlay(std::future<int>& value) { std::cout << "wait some times......" << std::endl; auto result = value.get(); //沒有獲取到值會阻塞等待獲取 std::cout << "Value:" << result << std::endl; } int main() { std::promise<int> promise; std::future<int> value = promise.get_future(); //將promise與future綁定  std::thread t1(disPlay, std::ref(value)); //創建線程並且函數傳參,ref()是傳一個引用 std::this_thread::sleep_for(std::chrono::seconds(1)); //線程延時1秒 //給線程傳值進去 promise.set_value(15); t1.join(); system("pause"); return 0; }

  獲取future的結果有三種方式上面是get()獲取異步結果值返回,還有wait()等待異步操作完成,以及wait_for()超時等待返回結果。

 

 5.future與package_task的使用

  std::packaged_task包裝一個可調用的對象,並且允許異步獲取該可調用對象產生的結果。
std::packaged_task將其包裝的可調用對象的執行結果傳遞給一個std::future對象,與std::promise某種程度上是很像的,promise保存一個共享狀態的值,而package_task保存的是一個函數。

 

示例代碼:

#include <iostream>
#include <chrono> #include <thread> #include <mutex> #include <atomic> #include <future> #include <vector> inline int func(int x) { return x + 6; } int main() { std::packaged_task<int(int)> tsk(func); std::future<int> fut = tsk.get_future(); //獲取future綁定起來  std::thread(std::move(tsk), 2).detach();//直接將task轉移作為線程函數使用  auto value = fut.get(); std::cout << "result:" << value << std::endl; system("pause"); return 0; }

 

 6.線程異步操作函數async的用法

  ,std::async比std::packaged_task,std::promise中,std::thread更高一層,它可以直接用來創建異步的task,異步的結果也保存在future中。完成后,外面再通過future.get/wait來獲取這個未來的結果,強烈推薦使用async,我們不需要關注異步任務的結果,只要等待任務完成獲取值就行了。

  現在來看看std::async的原型async(std::launch::async | std::launch::deferred, f, args...),第一個參數是線程的創建策略,有兩種策略,默認的策略是立即創建線程:

 

  • std::launch::async:在調用async就開始創建線程。
  • std::launch::deferred:延遲加載方式創建線程。調用async時不創建線程,直到調用了future的get或者wait時才創建線程。

 

第二個參數是線程函數,第三個參數是線程函數的參數。

代碼示例:

#include <iostream>
#include <chrono> #include <thread> #include <mutex> #include <atomic> #include <future> #include <vector> int main() { std::future<int> fut = std::async(std::launch::async, []() { return 9; }); std::cout << "result:" << fut.get() << std::endl; system("pause"); return 0; }

  []()這是c++11里面lambda表達式用法

 

7.std::future::wait_for()函數作用

  函數原型:

template< class Rep, class Period >
std::future_status wait_for( const std::chrono::duration<Rep,Period>& timeout_duration ) const;
 

  等待結果變得可用。阻塞直至經過指定的 timeout_duration ,或結果變為可用,兩者的先到來者。返回值鑒別結果的狀態。

此函數可能由於調度或資源爭議延遲而阻塞長於 timeout_duration

推薦標准庫用穩定時鍾度量時長。若實現用系統時鍾代替,則等待時間可能也對時鍾調整敏感。

若調用此函數前 valid()== false 則行為未定義。

參數

timeout_duration - 要阻塞的最大時長

返回值

 
常量 解釋
future_status::deferred 要計算結果的函數仍未啟動
future_status::ready 結果就緒
future_status::timeout 已經過時限

異常

時鍾、時間點或時長在執行中可能拋的任何異常(標准庫提供的時鍾、時間點和時長決不拋出)。

注意

鼓勵實現在調用前檢測 valid == false 的情況並拋出以 future_errc::no_state 為 error_condition 的 future_error

 

代碼示例:

#include <iostream>
#include <future> #include <thread> #include <chrono> int main() { std::future<int> future = std::async(std::launch::async, [](){ std::this_thread::sleep_for(std::chrono::seconds(3)); return 8; }); std::cout << "waiting...\n"; std::future_status status; do { status = future.wait_for(std::chrono::seconds(1)); if (status == std::future_status::deferred) { std::cout << "deferred\n"; } else if (status == std::future_status::timeout) { std::cout << "timeout\n"; } else if (status == std::future_status::ready) { std::cout << "ready!\n"; } } while (status != std::future_status::ready); std::cout << "result is " << future.get() << '\n'; }

可能結果:

waiting...
timeout
timeout
ready! result is 8

 

后面還會出很多一系列的入門教程,可以關注我噢。(我博客難道寫的不清楚嗎,你們還不關注我???小聲bb)。。。。hhhhhhhh

 

也可以補一下基礎多線程編程教程如下:

c++ 網絡編程課設入門超詳細教程 ---目錄

 

若有興趣交流分享技術,可關注本人公眾號,里面會不定期的分享各種編程教程,和共享源碼,諸如研究分享關於c/c++,python,前端,后端,opencv,halcon,opengl,機器學習深度學習之類有關於基礎編程,圖像處理和機器視覺開發的知識


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM