目錄<thread>this_thread命名空間1. get_id()2. sleep_for()3. sleep_until()4. yield()thread類構造函數:類方法1. get_id()2. join()3. detach()4. joinable()5. operator=6. ...
目錄
<thread>
this_thread命名空間
在C++11中不僅添加了線程類,還添加了一個關於線程的命名空間std::this_thread,在這個命名空間中提供了四個公共的成員函數,通過這些成員函數就可以對當前線程進行相關的操作了。
頭文件
1. get_id()
函數原型
thread::id get_id() noexcept;
get_id可以獲取主調線程的id,即在主線程中獲取到的是主線程id,在子線程中獲取到的是子線程id.
註:在VS中,線程id被封裝了一層,是一個結構體.
2. sleep_for()
命名空間this_thread中提供了一個休眠函數sleep_for(),調用這個函數的線程會馬上從運行態變成阻塞態併在這種狀態下休眠一定的時長,因為阻塞態的線程已經讓出了CPU資源,代碼也不會被執行,所以線程休眠過程中對CPU來說沒有任何負擔。程式休眠完成之後,會從阻塞態重新變成就緒態,就緒態的線程需要再次爭搶CPU時間片,搶到之後才會變成運行態,這時候程式才會繼續向下運行。
這個函數是函數原型如下,參數需要指定一個休眠時長,是一個時間段:
template <class Rep, class Period>
void sleep_for (const chrono::duration<Rep,Period>& rel_time);
示例:
#include<thread>
#include<chrono>
int main() {
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
std::this_thread::sleep_for(std::chrono::seconds());
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
std::chrono::nanoseconds ret = end - start;
std::cout<<"計時器計時時長:"<<ret.count()<<"納秒"<<ret.count()/ 1000000000<<"秒" << "\n";
}
3. sleep_until()
名空間this_thread中提供了另一個休眠函數sleep_until(),功能和sleep_for幾乎類似,區別是sleep_until的參數是時間點.即休眠到某個時間點.
VS中sleep_for調用了sleep_until實現.
函數原型:
template <class Clock, class Duration>
void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
#include<thread>
#include<chrono>
int main() {
std::cout<<std::this_thread::get_id()<<"\n";
std::cout<<"sleep_until 10 seconds after the currend time, start:"<<"\n";
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
std::this_thread::sleep_until(std::chrono::system_clock::now()+std::chrono::seconds(10));
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
std::cout<<"sleep_for:end"<<"\n";
std::chrono::nanoseconds ret = end - start;
std::cout << "計時器計時時長:" << ret.count() << "納秒 ~=" << ret.count() / 1000000000 << "秒" << "\n";
}
4. yield()
描述:線上程中調用這個函數之後,處於運行態的線程會主動讓出自己已經搶到的CPU時間片,最終變為就緒態.線程調用了yield()之後會主動放棄CPU使用權,但是這個變為就緒態的線程會馬上參與到下一輪CPU的搶奪戰中,不排除它能繼續搶到CPU時間片的情況
註意:只是"暫停"繼續執行,而不是結束線程運行從頭開始
函數原型:
void yield() noexcept;
常式:
#include<iostream>
#include<thread>
#include<chrono>
#include<cstdlib>
#pragma warning(disable:4996)
void func1() {
do {
std::cout << std::this_thread::get_id() << "\n";
} while (true);
}
void func2() {
do {
std::this_thread::yield();
std::cout << std::this_thread::get_id() << "\n";
} while (true);
}
int main() {
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
return 0;
}
可以發現,線程主動讓出時間片後,其他線程競爭力概率提高.(與操作系統調度方式有關,概率問題)
使用場景:
-
避免一個線程長時間占用CPU資源,從而導致多線程處理性能下降
在極端情況下,如果當前線程占用CPU資源不釋放就會導致其他線程中的任務無法被處理,或者該線程每次都能搶到CPU時間片,導致其他線程中的任務沒有機會被執行,此時可以使用yield緩解.
-
沒有滿足繼續執行的某種條件時,應主動放棄使用權.
thread類
構造函數:
thread() noexcept; //空線程對象
thread( thread&& other ) noexcept; //移動構造
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args ); //常規構造
thread( const thread& ) = delete; //禁止拷貝
-
線程傳非靜態成員函數時,語法為
//法一: 定義的對象; 線程對象(&類::成員函數,對象/&對象,參數...);
因為成員函數比較特殊,不能像普通函數那樣函數名可以自動轉換成指針,必須要取地址&函數名才可以.
拿到成員函數的地址需要使用到指針和地址相關的操作。在C++中,獲取成員函數的地址並調用它是一種相對複雜的過程,因為涉及到了類的實例(對象)和方法的綁定問題。
常式:
class A { public: static void fun1(){} void fun2(){std::cout<<"func2()"<<"\n"; } }; int main() { A a; std::thread t1(&A::fun2,a); std::thread t2(&A::fun2,&a); //傳地址更優 t1.join(); t2.join(); return 0;}
//法二: 線程對象(std::bind(&類::成員函數,對象/&對象,參數...),參數...);
-
靜態成員函數語法
線程對象(&類::成員函數,參數...); //靜態方法&可加可不加
靜態成員函數簡單,只需要對函數取地址即可
類方法
1. get_id()
函數原型:
std::thread::id get_id() const noexcept;
thread中的類方法get_id()和this_thread::get_id();功能類似,用於返回thread對象的id.this_thread中的功能是返回主調線程的id.
2. join()
功能:
join()字面意思是連接一個線程,主動地等待線程的終止(線程阻塞)。在某個線程中通過子線程對象調用join()函數,調用這個函數的線程被阻塞,但是子線程對象中的任務函數會繼續執行,當任務執行完畢之後join()會清理當前子線程中的相關資源然後返回,同時,調用該函數的線程解除阻塞繼續向下執行。
函數原型:
void join();
為什麼需要join:
- 多線程中,未分離的子線程在結束後會等待將處理結果返回給主線程,如果此時主線程結束,則進程所有資源都會回收,與此同時子線程還在處理數據返回,之後子線程修必然要處理數據,就會發生越界.
- 同理,線程處理任務過程中,如果主線程結束,資源回收,子線程之後也是非法訪問.
join需要註意的幾點.
-
空線程不需要join,因為空線程對象沒有任務,並沒有創建真實線程(C++線程對象是用於管理系統線程調用)
join了會報錯
-
註意,函數體為空的線程是需要調用join方法,因為真實線程被創建了.
-
move後的線程對象也不需要調用join方法,因為資源被轉移到別的線程對象了.
3. detach()
描述:
detach()函數的作用是進行線程分離,分離主線程和創建出的子線程。線上程分離之後,主線程退出也會一併銷毀創建出的所有子線程,在主線程退出之前,它可以脫離主線程繼續獨立的運行,任務執行完畢之後,這個子線程會自動釋放自己占用的系統資源。
線程分離函數detach()不會阻塞線程,子線程和主線程分離之後,在主線程中就不能再對這個子線程做任何控制了
函數原型:
void detach();
註意:
-
detach就算分離,使用的資源依舊屬於進程,主線程結束,資源被回收後,detach的子線程如果未執行完畢,之後的行為將是未定義的.在win10中子線程在主線程結束後同步結束.
主線程結束後,主進程也隨著同步退出,資源回收.detach僅僅能起到提前回收的作用.
測試常式:
void func2() {
do {
std::cout << std::this_thread::get_id() << "\n";
_sleep(100);
} while (true);
}
int main() {
std::thread t2(func2);
t2.detach();
_sleep(1000);
return 0;}
- detach後無法再通過線程對象查看子線程id
很少會分離線程,分離後線程式控制制更加麻煩,需要處理的更多
4. joinable()
函數原型:
bool joinable() const noexcept;
joinable()函數用於判斷主線程和子線程是否處理關聯(連接)狀態,一般情況下,二者之間的關係處於關聯狀態,該函數返回一個布爾類型:
返回值為true:主線程和子線程之間有關聯(連接)關係
返回值為false:主線程和子線程之間沒有關聯(連接)關係
一般情況下,只有線程有任務時,joinable返回真;其他情況,如空線程對象,分離的線程對象以及join之後的線程對象則返回假.
任務執行完,待返回的線程也屬於關聯狀態.
使用場景:
批量創建線程,如果線程後續沒有分配任務,在join時程式後報錯.因此可以在join前判斷是否關聯,關聯才join,這樣程式就不會奔潰.
std::thread t1;
if (t1.joinable() == true)
{
t1.join();
}
總結:
- 在創建的子線程對象的時候,如果沒有指定任務函數,那麼子線程不會啟動,主線程和這個子線程也不會進行連接
- 在創建的子線程對象的時候,如果指定了任務函數,子線程啟動並執行任務,主線程和這個子線程自動連接成功
- 子線程調用了detach()函數之後,父子線程分離,同時二者的連接斷開,調用joinable()返回false
- 在子線程調用了join()函數,子線程中的任務函數繼續執行,直到任務處理完畢,這時join()會清理(回收當前子線程的相關資源,所以這個子線程和主線程的連接也就斷開了,因此,調用join()之後再調用joinable()會返回false。
5. operator=
線程對象不允許拷貝,只能轉移
函數原型
// move (1)
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)
thread& operator= (const other&) = delete;
6. hardware_concurrency(static)
thread線程類還提供了一個靜態方法hardware_concurrency,用於獲取當前電腦的CPU核心數,根據這個結果在程式中創建出數量相等的線程,每個線程獨自占有一個CPU核心,這些線程就不用分時復用CPU時間片,此時程式的併發效率是最高的。
線程數對應CPU的邏輯核心數,現代個人電腦一般只有一個物理CPU.然後CPU內有多個核心(真多核並行).這些核心能過模擬出多個核心(併發),叫做虛擬核心,電腦中的線程數一般都是虛擬核心數
在操作系統看來,核心數一般就是指虛擬核心數.
以一塊8核16線程CPU為例:
一塊CPU內有8個核心,每個核心都能模擬出2個虛擬核心,每個核心對應一個線程,即8核16線程.
CPU的核心數和線程數傻傻分不清,一文科普,清晰明瞭_cpu核心數和線程數-CSDN博客
函數原型:
static unsigned hardware_concurrency() noexcept;
多線程的兩種計算場景
1. IO密集型程式
IO密集型程式主要特點是頻繁的輸入/輸出操作,如讀寫文件、網路通信等。這些操作通常需要花費大量時間等待外部設備完成數據傳輸,導致CPU空閑。因此,IO密集型程式通常在等待IO操作完成時,會釋放CPU資源給其他進程使用。
例如,一個大型文件伺服器或資料庫系統就是典型的IO密集型程式。在處理大量請求時,它們需要頻繁地讀寫磁碟,使得CPU常常處於空閑狀態。
2. CPU密集型程式
相對地,CPU密集型程式主要是進行大量的數學計算、邏輯運算等需要消耗大量CPU資源的任務。這些任務通常不涉及大量的IO操作,而是專註於利用CPU進行高速處理。
例如,科學計算、大數據分析、機器學習等領域的應用就屬於CPU密集型程式。它們需要大量的數學計算和邏輯運算,對CPU資源的需求極高。
實現與優化
對於IO密集型程式,關鍵在於減少不必要的CPU使用,通過多線程、非同步IO等技術來提高效率。由於IO操作通常成為性能瓶頸,因此應優先處理這些操作以降低延遲。
而對於CPU密集型程式,優化的重點在於最大化CPU利用率。這可以通過並行計算、多線程等技術實現。通過將任務分解為多個子任務並分配給多個核心同時處理,可以顯著提高程式的運行速度。
引用參考 :
https://subingwen.cn/tags/C-11/
https://zh.cppreference.com/w/cpp
https://legacy.cplusplus.com/