Linux的多線程
Linux的子線程實際上也是個進程,但是比傳統的進程輕量化。
pthread
pthread是用于Linux系統下的線程庫,頭文件是<pthread.h>。C++11 之前的多線程開發高度依賴平臺原生 API,Windows 以?CreateThread
?和內核對象為核心,Linux 則遵循 POSIX 標準的?pthread
?庫。二者在接口設計、同步機制及資源管理上的差異顯著增加了跨平臺開發成本,這也是?std::thread
?標準庫被引入的重要動力。
創建線程pthread_create()
線程ID:
ID類型為 pthread_t,是給usigned long int,
查看當前線程ID,調用如下函數:
pthread_t pthread_self(void)? ? ? ? //返回當前線程的線程ID
創建線程函數pthread_create()
pthread_create
?是 POSIX 線程庫(pthread)中用于創建線程的函數,其語法如下:
#include <pthread.h>int pthread_create(pthread_t *thread, // 指向線程標識符的指針(輸出參數)const pthread_attr_t *attr, // 線程屬性(NULL表示默認屬性)void *(*start_routine)(void*), // 線程入口函數(返回void*,參數為void*)void *arg // 傳遞給入口函數的參數
);
// Compile and link with -pthread, 線程庫的名字叫pthread, 全名: libpthread.so libptread.a
//注意:pthread的源代碼是個動態庫,在編譯的時候要鏈接動態庫
?代碼:
#include <iostream>
#include <pthread.h>
#include <string>
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr );std::cout<<"主線程:"<<pthread_self()<<std::endl;for (int i = 0; i < 5; i++){std::cout<<i<<std::endl;}system("pause");return 0;
}
編譯可執行文件:
g++ mythread.cpp -lpthread -o mythread
線程庫文件(動態庫),需要在編譯的時候通過參數指定出來,動態庫名為 libpthread.so需要使用的參數為 -l,根據規則掐頭去尾最終形態應該寫成:-lpthread(參數和參數值中間可以有空格)
線程退出pthread_exit()
在編寫多線程程序的時候,如果想要讓線程退出,但是不會導致虛擬地址空間的釋放(針對于主線程),我們就可以調用線程庫中的線程退出函數,只要調用該函數當前線程就馬上退出了,并且不會影響到其他線程的正常運行,不管是在子線程或者主線程中都可以使用。
#include <pthread.h>
void pthread_exit(void *retval);
參數: 線程退出的時候攜帶的數據,當前子線程的主線程會得到該數據。如果不需要使用,指定為NULL
#include <iostream>
#include <pthread.h>
#include <string>
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr );std::cout<<"主線程:"<<pthread_self()<<std::endl;pthread_exit(NULL);return 0;
}
線程回收pthread_join()阻塞等待
線程函數
線程和進程一樣,子線程退出的時候其內核資源主要由主線程回收,線程庫中提供的線程回收函叫做pthread_join(),這個函數是一個阻塞函數,如果還有子線程在運行,調用該函數就會阻塞,子線程退出函數解除阻塞進行資源的回收,函數被調用一次,只能回收一個子線程,如果有多個子線程則需要循環進行回收。
另外通過線程回收函數還可以獲取到子線程退出時傳遞出來的數據,函數原型如下:
#include <pthread.h>
// 這是一個阻塞函數, 子線程在運行這個函數就阻塞
// 子線程退出, 函數解除阻塞, 回收對應的子線程資源, 類似于回收進程使用的函數 wait()
int pthread_join(pthread_t thread, void **retval);
參數:
thread: 要被回收的子線程的線程ID
retval: 二級指針, 指向一級指針的地址, 是一個傳出參數, 這個地址中存儲了pthread_exit() 傳遞出的數據,如果不需要這個參數,可以指定為NULL
返回值:線程回收成功返回0,回收失敗返回錯誤號。
代碼:
#include <iostream>
#include <pthread.h>
#include <string>
struct Test
{int num;int age;
};
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}struct Test *t = static_cast<struct Test *>(arg); //將void*轉換為Test*t->num=100;t->age=6;pthread_exit(t); //注意不要返回局部變量,不然有內存問題,這里的t指向的地址是主線程傳入的地址//如果t是局部變量,主線程無法訪問到return nullptr;
}
int main()
{pthread_t tid;struct Test t;pthread_create(&tid,nullptr,callback,&t );std::cout<<"主線程:"<<pthread_self()<<std::endl;void *ptr;pthread_join(tid,&ptr); //第二個參數,傳入的是指針ptr的地址,是個二級指針,ptr最終指向callback函數的tstruct Test * pt=static_cast<struct Test *>(ptr);std::cout << "Thread returned: num = " << pt->num << ", age="<<pt->age<<std::endl; return 0;
}
注意pthread_exit(t)不要返回局部變量,不然有內存問題。這里的t指向的地址是主線程傳入的地址;如果t是局部變量,子線程退出后,子線程棧區數據會被釋放,主線程將無法訪問到。
線程分離pthread_detach()
在某些情況下,程序中的主線程有屬于自己的業務處理流程,如果讓主線程負責子線程的資源回收,調用pthread_join()只要子線程不退出主線程就會一直被阻塞,主要線程的任務也就不能被執行了。
在線程庫函數中為我們提供了線程分離函數pthread_detach(),調用這個函數之后指定的子線程就可以和主線程分離,當子線程退出的時候,其占用的內核資源就被系統的其他進程接管并回收了。線程分離之后在主線程中使用pthread_join()就回收不到子線程資源了。
#include <pthread.h>
// 參數就子線程的線程ID, 主線程就可以和這個子線程分離了
int pthread_detach(pthread_t thread);
下面的代碼中,在主線程中創建子線程,并調用線程分離函數,實現了主線程和子線程的分離:
#include <iostream>
#include <pthread.h>
#include <string>
struct Test
{int num;int age;
};
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}struct Test t; //在子線程中創建結構體t,因為主線程棧區數據在主線程結束后將訪問不到t.num=100; t.age=6;pthread_exit(&t); return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr); //創建線程,傳入回調函數和參數std::cout<<"主線程:"<<pthread_self()<<std::endl;pthread_detach(tid); //分離線程,主線程不需要等待子線程結束return 0;
}
注意,線程分離后,如果callback參數是主線程棧區數據,將出現內存異常,子線程訪問不到。
主線程正常退出(return/exit)?
若主線程通過return
或exit
結束,整個進程會立即終止,所有子線程(包括已分離的線程)將被強制終止,無論子線程是否完成
主線程調用pthread_exit退出(std::thread中沒有與這個對應的函數)?
若主線程調用pthread_exit
退出,進程會繼續運行直到所有非分離線程結束。此時已分離的子線程可繼續獨立執行,但其生命周期受限于進程存活狀態
資源回收機制?
分離后的子線程終止時,系統會自動回收其資源(棧空間、寄存器狀態等),無需其他線程調用pthread_join
。但若主線程導致進程終止,分離線程的資源也會隨進程一起被操作系統
風險提示
分離線程若訪問主線程已釋放的資源(如棧變量),會導致未定義行為。
在守護進程(daemon)中,主線程退出后分離線程可能繼續運行,但需注意僵尸線程風險
?
線程取消pthread_cancel()
線程取消的意思就是在某些特定情況下在一個線程中殺死另一個線程。使用這個函數殺死一個線程需要分兩步:
1、在線程A中調用線程取消函數pthread_cancel,指定殺死線程B,這時候線程B是死不了的
2、在線程B中進程一次系統調用(從用戶區切換到內核區),否則線程B可以一直運行。
#include <pthread.h>
// 參數是子線程的線程ID
int pthread_cancel(pthread_t thread);
?參數:要殺死的線程的線程ID
返回值:函數調用成功返回0,調用失敗返回非0錯誤號。
在下面的示例代碼中,主線程調用線程取消函數,只要在子線程中進行了系統調用,當子線程執行到這個位置就掛掉了。
#include <iostream>
#include <pthread.h>
#include <string>
struct Test
{int num;int age;
};
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}struct Test t;t.num=100;t.age=6;return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr); //創建線程,傳入回調函數和參數std::cout<<"主線程:"<<pthread_self()<<std::endl;for(int i=0; i < 100; ++i){std::cout << "主線程: " << i << std::endl;}pthread_cancel(tid);return 0;
}
關于系統調用有兩種方式:
1、直接調用Linux系統函數
2、調用標準C庫函數,為了實現某些功能,在Linux平臺下標準C庫函數會調用相關的系統函數
線程ID比較pthread_equal()
在Linux中線程ID本質就是一個無符號長整形,因此可以直接使用比較操作符比較兩個線程的ID,但是線程庫是可以跨平臺使用的,在某些平臺上 pthread_t可能不是一個單純的整形,這中情況下比較兩個線程的ID必須要使用比較函數,函數原型如下:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
參數:t1 和 t2 是要比較的線程的線程ID
返回值:如果兩個線程ID相等返回非0值,如果不相等返回0
C++線程類?
++11之前,C++語言沒有對并發編程提供語言級別的支持,這使得我們在編寫可移植的并發程序時,存在諸多的不便。現在C++11中增加了線程以及線程相關的類,很方便地支持了并發編程,使得編寫的多線程程序的可移植性得到了很大的提高。
C++11中提供的線程類叫做std::thread,基于這個類創建一個新的線程非常的簡單,只需要提供線程函數或者函數對象即可,并且可以同時指定線程函數的參數。
C++語言級別的多線程編程=》代碼可以跨平臺windows/linux/mac
本節主要內容:
thread/
mutex/
condition_variable/
lock_guard/
unique_lock/
atomic 原子類型 基于CAS操作的原子類型 線程安全的
sleep_for
本質上相當于在語言層面加了層封裝,在底層仍然調用操作系統各自的API
C++語言層面:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???thread
底層:? ? ? ? ? ? ? ? ? ? ? windows? ? ? ? ? ? ? ? ? ????????????????????????linux(調用stace ./a.out可以看過程)
? ? ? ? ? ? ? ? ? ? ? ? ? ? createThread????????????????????????????????pthread_create
構造函數
// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;
構造函數①:默認構造函,構造一個線程對象,在這個線程中不執行任何處理動作
構造函數②:移動構造函數,將 other 的線程所有權轉移給新的thread 對象。之后 other 不再表示執行線程。
構造函數③:創建線程對象,并在該線程中執行函數f中的業務邏輯,args是要傳遞給函數f的參數
任務函數f的可選類型有很多,具體如下:
????????普通函數,類成員函數,匿名函數,仿函數(這些都是可調用對象類型)
????????可以是可調用對象包裝器類型,也可以是使用綁定器綁定之后得到的類型(仿函數)
構造函數④:使用=delete顯示刪除拷貝構造, 不允許線程對象之間的拷貝
公共成員函數
get_id()
應用程序啟動之后默認只有一個線程,這個線程一般稱之為主線程或父線程,通過線程類創建出的線程一般稱之為子線程,每個被創建出的線程實例都對應一個線程ID,這個ID是唯一的,可以通過這個ID來區分和識別各個已經存在的線程實例,這個獲取線程ID的函數叫做get_id(),函數原型如下:
std::thread::id get_id() const noexcept;
獲取子線程t1的線程id:t1.get_id();
獲取當前線程的線程id:this_thread::get_id()
線程回收
在C++標準中,?必須?對std::thread
對象顯式調用join()
或detach()
,否則會導致程序終止(觸發std::terminate
)。這是由C++11標準嚴格規定的線程生命周期管理機制。
因此,必須二選一:
加入式(join())? ? ? ? ? ? ? ? ? ? ? ? ——同步等待
分離式(detach())? ? ? ? ? ? ? ? ? ?——異步分離
join()
在某個線程中通過子線程對象調用join()函數,調用這個函數的線程被阻塞,但是子線程對象中的任務函數會繼續執行,當任務執行完畢之后join()會清理當前子線程中的相關資源然后返回,同時,調用該函數的線程解除阻塞繼續向下執行。
調用方法:t1.join();
detach()
detach()函數的作用是進行線程分離,分離主線程和創建出的子線程。在線程分離之后,主線程退出也會一并銷毀創建出的所有子線程,在主線程退出之前,它可以脫離主線程繼續獨立的運行,任務執行完畢之后,這個子線程會自動釋放自己占用的系統資源。
調用方法:t1.detach()
detach()應用場景適配性?
- 適用于?后臺任務?(如日志記錄、心跳檢測),主線程無需等待其完成
- 允許主線程快速響應新請求,而分離線程持續處理耗時操作
- 避免
join()
導致的線程嵌套阻塞問題(如線程A等待線程B,線程B又等待線程A)
joinable()
joinable()函數用于判斷主線程和子線程是否處理關聯(連接)狀態,一般情況下,二者之間的關系處于關聯狀態,該函數返回一個布爾類型:
返回值為true:主線程和子線程之間有關聯(連接)關系
返回值為false:主線程和子線程之間沒有關聯(連接)關系
bool joinable() const noexcept;
#include<iostream>
#include<thread>
#include <chrono>
void threadFunction() {std::this_thread::sleep_for(std::chrono::seconds(2));
}
int main() {std::thread t;std::cout << "before starting, joinable: " << t.joinable() << std::endl;t = std::thread(threadFunction);std::cout << "after starting, joinable: " << t.joinable() << std::endl;t.join();std::cout << "after joining, joinable: " << t.joinable() << std::endl;std::thread t1(threadFunction);std::cout << "after starting, joinable: " << t1.joinable() << std::endl;t1.detach();std::cout << "after detaching, joinable: " << t1.joinable() << std::endl;return 0;
}
?結果如下
結論:
1、在創建的子線程對象的時候,如果沒有指定任務函數,那么子線程不會啟動,主線程和這個子線程也不會進行連接
2、在創建的子線程對象的時候,如果指定了任務函數,子線程啟動并執行任務,主線程和這個子線程自動連接成功
3、子線程調用了detach()函數之后,父子線程分離,同時二者的連接斷開,調用joinable()返回false
4、在子線程調用了join()函數,子線程中的任務函數繼續執行,直到任務處理完畢,這時join()會清理(回收)當前子線程的相關資源,所以這個子線程和主線程的連接也就斷開了,因此,調用join()之后再調用joinable()會返回false。
靜態函數 hardware_concurrency()
thread線程類還提供了一個靜態方法,用于獲取當前計算機的CPU核心數,根據這個結果在程序中創建出數量相等的線程,每個線程獨自占有一個CPU核心,這些線程就不用分時復用CPU時間片,此時程序的并發效率是最高的。
static unsigned hardware_concurrency() noexcept;
?代碼:
int main() {int num=std::thread::hardware_concurrency();std::cout<<"CPU number:"<<num<<std::endl;return 0;
}
命名空間 - this_thread
get_id()
調用命名空間std::this_thread中的get_id()方法可以得到當前線程的線程ID,函數原型如下:
thread::id get_id() noexcept;
sleep_for()?
線程被創建后有這五種狀態:創建態,就緒態,運行態,阻塞態(掛起態),退出態(終止態)
線程和進程的執行有很多相似之處,在計算機中啟動的多個線程都需要占用CPU資源,但是CPU的個數是有限的并且每個CPU在同一時間點不能同時處理多個任務。為了能夠實現并發處理,多個線程都是分時復用CPU時間片,快速的交替處理各個線程中的任務。因此多個線程之間需要爭搶CPU時間片,搶到了就執行,搶不到則無法執行(因為默認所有的線程優先級都相同,內核也會從中調度,不會出現某個線程永遠搶不到CPU時間片的情況)。
命名空間this_thread中提供了一個休眠函數sleep_for(),調用這個函數的線程會馬上從運行態變成阻塞態并在這種狀態下休眠一定的時長,因為阻塞態的線程已經讓出了CPU資源,代碼也不會被執行,所以線程休眠過程中對CPU來說沒有任何負擔。這個函數是函數原型如下,參數需要指定一個休眠時長,是一個時間段:
template <class Rep, class Period>void sleep_for (const chrono::duration<Rep,Period>& rel_time);
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;void func()
{for (int i = 0; i < 10; ++i){this_thread::sleep_for(chrono::seconds(1));cout << "子線程: " << this_thread::get_id() << ", i = " << i << endl;}
}int main()
{thread t(func);t.join();
}
在func()函數的for循環中使用了this_thread::sleep_for(chrono::seconds(1));之后,每循環一次程序都會阻塞1秒鐘,也就是說每隔1秒才會進行一次輸出。需要注意的是:程序休眠完成之后,會從阻塞態重新變成就緒態,就緒態的線程需要再次爭搶CPU時間片,搶到之后才會變成運行態,這時候程序才會繼續向下運行。
sleep_until()
命名空間this_thread中提供了另一個休眠函數sleep_until(),和sleep_for()不同的是它的參數類型不一樣
sleep_until():指定線程阻塞到某一個指定的時間點time_point類型,之后解除阻塞
sleep_for():指定線程阻塞一定的時間長度duration 類型,之后解除阻塞
該函數的函數原型如下:?
template <class Clock, class Duration>void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;void func()
{for (int i = 0; i < 10; ++i){// 獲取當前系統時間點auto now = chrono::system_clock::now();// 時間間隔為2schrono::seconds sec(2);// 當前時間點之后休眠兩秒this_thread::sleep_until(now + sec);cout << "子線程: " << this_thread::get_id() << ", i = " << i << endl;}
}int main()
{thread t(func);t.join();
}
yield()
命名空間this_thread中提供了一個非常紳士的函數yield(),在線程中調用這個函數之后,處于運行態的線程會主動讓出自己已經搶到的CPU時間片,最終變為就緒態,這樣其它的線程就有更大的概率能夠搶到CPU時間片了。使用這個函數的時候需要注意一點,線程調用了yield()之后會主動放棄CPU資源,但是這個變為就緒態的線程會馬上參與到下一輪CPU的搶奪戰中,不排除它能繼續搶到CPU時間片的情況,這是概率問題。
void yield() noexcept;
#include <iostream>
#include <thread>
using namespace std;void func()
{for (int i = 0; i < 100000000000; ++i){cout << "子線程: " << this_thread::get_id() << ", i = " << i << endl;this_thread::yield();}
}int main()
{thread t(func);thread t1(func);t.join();t1.join();
}
結論:
1、std::this_thread::yield() 的目的是避免一個線程長時間占用CPU資源,從而導致多線程處理性能下降。
2、std::this_thread::yield() 是讓當前線程主動放棄了當前自己搶到的CPU資源,但是在下一輪還會繼續搶。
互斥鎖
解決多線程數據混亂的方案就是進行線程同步,最常用的就是互斥鎖,在C++11中一共提供了四種互斥鎖:
std::mutex:獨占的互斥鎖,不能遞歸使用
std::timed_mutex:帶超時的獨占互斥鎖,不能遞歸使用
std::recursive_mutex:遞歸互斥鎖,不帶超時功能
std::recursive_timed_mutex:帶超時的遞歸互斥鎖
互斥鎖在有些資料中也被稱之為互斥量,二者是一個東西。
?std::mutex
成員函數
lock()函數用于給臨界區加鎖,并且只能有一個線程獲得鎖的所有權,它有阻塞線程的作用,函數原型如下
void lock();
獨占互斥鎖對象有兩種狀態:鎖定和未鎖定。如果互斥鎖是打開的,調用lock()函數的線程會得到互斥鎖的所有權,并將其上鎖,其它線程再調用該函數的時候由于得不到互斥鎖的所有權,就會被lock()函數阻塞。當擁有互斥鎖所有權的線程將互斥鎖解鎖,此時被lock()阻塞的線程解除阻塞,搶到互斥鎖所有權的線程加鎖并繼續運行,沒搶到互斥鎖所有權的線程繼續阻塞。
除了使用lock()還可以使用try_lock()獲取互斥鎖的所有權并對互斥鎖加鎖,函數原型如下:
bool try_lock();
二者的區別在于try_lock()不會阻塞線程,lock()會阻塞線程:
如果互斥鎖是未鎖定狀態,得到了互斥鎖所有權并加鎖成功,函數返回true
如果互斥鎖是鎖定狀態,無法得到互斥鎖所有權加鎖失敗,函數返回false
當互斥鎖被鎖定之后可以通過unlock()進行解鎖,但是需要注意的是只有擁有互斥鎖所有權的線程也就是對互斥鎖上鎖的線程才能將其解鎖,其它線程是沒有權限做這件事情的。該函數的函數原型如下:
void unlock();
通過介紹以上三個函數,使用互斥鎖進行線程同步的大致思路差不多就能搞清楚了,主要分為以下幾步:
1、找到多個線程操作的共享資源(全局變量、堆內存、類成員變量等),也可以稱之為臨界資源
2、找到和共享資源有關的上下文代碼,也就是臨界區(下圖中的黃色代碼部分)
3、在臨界區的上邊調用互斥鎖類的lock()方法
4、在臨界區的下邊調用互斥鎖的unlock()方法
線程同步的目的是讓多線程按照順序依次執行臨界區代碼,這樣做線程對共享資源的訪問就從并行訪問變為了線性訪問,訪問效率降低了,但是保證了數據的正確性。
當線程對互斥鎖對象加鎖,并且執行完臨界區代碼之后,一定要使用這個線程對互斥鎖解鎖,否則最終會造成線程的死鎖。死鎖之后當前應用程序中的所有線程都會被阻塞,并且阻塞無法解除,應用程序也無法繼續運行。
注意:
1、在所有線程的任務函數執行完畢之前,互斥鎖對象是不能被析構的,一定要在程序中保證這個對象的可用性。
2、互斥鎖的個數和共享資源的個數相等,也就是說每一個共享資源都應該對應一個互斥鎖對象。互斥鎖對象的個數和線程的個數沒有關系。
代碼示例:
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<chrono>
std::mutex mtx; // Global mutex for thread safety
int ticketCount = 50;
void sellTicket(int i)
{while(ticketCount > 0) //ticketCount=1 鎖+雙重判斷,以防ticketCount出現-1的情況{mtx.lock();if(ticketCount > 0) {std::cout<<"窗口"<<i<<"售出一張票,剩余票數:"<<ticketCount<<std::endl;ticketCount--;}mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate}
}int main() {std::list<std::thread> tList;for(int i=0;i<3;i++){tList.push_back(std::thread(sellTicket,i));}for(auto &t : tList){if(t.joinable()){t.join();}}return 0;
}
std::lock_guard
lock_guard是C++11新增的一個模板類,使用這個類,可以簡化互斥鎖lock()和unlock()的寫法,同時也更安全(防止ulock()調用不到)。這個模板類的定義和常用的構造函數原型如下:
// 類的定義,定義于頭文件 <mutex>
template< class Mutex >
class lock_guard;// 常用構造函數
explicit lock_guard( mutex_type& m );
lock_guard在使用上面提供的這個構造函數構造對象時,會自動鎖定互斥量,而在退出作用域后進行析構時就會自動解鎖,從而保證了互斥量的正確操作,避免忘記unlock()操作而導致線程死鎖。lock_guard使用了RAII技術,就是在類構造函數中分配資源,在析構函數中釋放資源,保證資源出了作用域就釋放。
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<chrono>
std::mutex mtx; // Global mutex for thread safety
int ticketCount = 50;
void sellTicket(int i)
{while(ticketCount > 0) //ticketCount=1 鎖+雙重判斷,以防ticketCount出現-1的情況{//mtx.lock();{// 保證所有線程都能釋放鎖,防止死鎖問題發生std::lock_guard<std::mutex> lock(mtx);if(ticketCount > 0) {std::cout<<"窗口"<<i<<"售出一張票,剩余票數:"<<ticketCount<<std::endl;ticketCount--;}}//mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate}
}int main() {std::list<std::thread> tList;for(int i=0;i<3;i++){tList.push_back(std::thread(sellTicket,i));}for(auto &t : tList){if(t.joinable()){t.join();}}return 0;
}
缺陷:
鎖粒度控制不靈活?
- 固定作用域鎖定?:在構造時立即加鎖,析構時自動解鎖,無法在作用域內手動釋放鎖。
- ?性能影響?:若臨界區范圍過大(如循環內部存在耗時操作),會導致鎖持有時間過長,降低并發效率
功能局限性?
- ?不支持延遲加鎖?:創建時必須立即鎖定互斥量,無法實現“先構造后加鎖”。
- ?不可手動解鎖?:無法在作用域結束前主動釋放鎖(如條件判斷后提前解鎖)。
- ?所有權不可轉移?:不支持移動語義,無法在函數間傳遞鎖所有權。
lock_guard無法在函數間傳遞所有權,錯誤代碼示例:
#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx;// 嘗試傳遞lock_guard作為參數(錯誤示例)
void process_data(std::lock_guard<std::mutex> lock) { // 編譯錯誤:lock_guard不可拷貝std::cout << "Processing data with lock held\n";
}int main() {std::lock_guard<std::mutex> lock(mtx); // 正確用法:局部作用域鎖定// process_data(lock); // 錯誤:lock_guard不能拷貝傳遞return 0;
}
可以改用unique_lock配合std::move實現所有權轉移
#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx;// 使用unique_lock的正確方案
void process_data(std::unique_lock<std::mutex> lock) { // 支持移動語義std::cout << "Processing data with lock held\n";
}int main() {std::unique_lock<std::mutex> lock(mtx); // 先獲取鎖process_data(std::move(lock)); // 通過移動語義傳遞所有權// 此時lock不再擁有互斥量if (!lock.owns_lock()) {std::cout << "Lock ownership transferred\n";}return 0;
}
可以理解為lock_guard相當于scoped_ptr,unique_lock相當于unique_ptr。
std::unique_lock
unique_lock與lock_guard區別:?
1、lock_guard只能用在簡單的臨界區代碼段的互斥操作中,不能用在函數參數傳遞或者返回過程中
2、unique_lock不僅能用在簡單的臨界區代碼段的互斥操作中,還能用在函數調用過程中
3、unique_lock有lock、unlock、try_lock等構造方法,支持延遲加鎖
// lock_guard用法(無法手動解鎖)
std::mutex mtx;
{std::lock_guard<std::mutex> lk(mtx); // 自動加鎖// 臨界區代碼
} // 自動解鎖// unique_lock用法(支持手動控制)
std::unique_lock<std::mutex> ulk(mtx, std::defer_lock);
ulk.lock(); // 顯式加鎖
// 臨界區代碼
ulk.unlock(); // 顯式解鎖
?
多線程編程的兩個核心問題:互斥與同步通信
1. 線程間互斥:解決競態條件問題
問題本質
當多個線程同時訪問?共享資源?(內存、文件、設備等)時,如果至少有一個線程執行?寫操作?,就會產生?競態條件?。最終結果取決于線程執行的隨機順序,導致程序行為不可預測。
關鍵概念
- ?臨界區?:訪問共享資源的代碼段(需要保護的區域)
- ?原子操作?:不可分割的操作(要么完全執行,要么完全不執行)
- ?數據競爭?:多個線程同時訪問同一內存位置,且至少有一個是寫操作
類比解釋
想象一個公共衛生間:
- ?共享資源?:衛生間(只能一個人使用)
- ?臨界區?:衛生間內部
- ?線程?:需要如廁的人
- ?競態條件?:多人同時嘗試進入衛生間導致混亂
解決方案與技術
// 共享資源
int shared_counter = 0;// 解決方案1:互斥鎖(Mutex)
std::mutex mtx;
void safe_increment() {std::lock_guard<std::mutex> lock(mtx); // 進入臨界區前加鎖shared_counter++; // 臨界區操作// 離開作用域自動解鎖
}// 解決方案2:原子操作(Atomic)
std::atomic<int> atomic_counter(0);
void atomic_increment() {atomic_counter++; // 無鎖原子操作
}
關鍵要點
- 互斥確保?同一時間只有一個線程?訪問臨界區
- 臨界區應盡可能?短小?(減少鎖持有時間)
- 優先考慮?無鎖設計?(原子操作、線程本地存儲)
- 警惕?死鎖?(多個鎖使用固定順序)
2. 線程間同步通信:協調執行順序
問題本質
當線程之間存在?依賴關系?時(例如生產者-消費者),需要協調它們的執行順序。線程同步通信解決的是"?何時執行?"的問題,而非"是否沖突"。
關鍵概念
- ?執行順序依賴?:一個線程需要等待另一個線程完成特定操作
- ?事件通知?:線程間發送信號通知狀態變化
- ?阻塞/喚醒機制?:讓線程在條件不滿足時休眠,條件滿足時喚醒
類比解釋
餐廳廚房工作流程:
- ?生產者?:廚師(制作菜肴)
- ?消費者?:服務員(上菜)
- ?共享資源?:出菜臺
- ?同步需求?:服務員必須等待廚師完成菜肴才能上菜
生產者-消費者模型實現
同步機制詳解
機制 | 作用 | 特點 |
---|---|---|
std::condition_variable | 線程等待/通知 | 必須與std::mutex 配合使用 |
wait(lock, predicate) | 條件等待 | 自動釋放鎖,被喚醒后重新加鎖 |
notify_one() | 通知一個等待線程 | 精確喚醒,避免驚群效應 |
notify_all() | 通知所有等待線程 | 適用于多消費者場景 |
互斥與同步的關系與區別
特性 | 線程互斥 | 線程同步通信 |
---|---|---|
?核心問題? | 防止并發訪問沖突 | 協調執行順序 |
?關注點? | "能否訪問"資源 | "何時訪問"資源 |
?類比? | 衛生間門鎖 | 廚師-服務員協作 |
?典型場景? | 計數器更新 | 生產者-消費者 |
?主要機制? | 互斥鎖、原子操作 | 條件變量、信號量 |
?性能重點? | 減少鎖競爭 | 精確喚醒、避免忙等待 |
最佳實踐總結
- ?分層設計?:先解決互斥(數據安全),再處理同步(執行順序)
- ?RAII原則?:使用
lock_guard
/unique_lock
管理鎖資源 - ?精確通知?:使用
notify_one()
替代notify_all()
減少不必要喚醒 - ?避免虛假喚醒?:條件變量等待始終使用謂詞檢查
- ?無鎖設計?:對于高性能場景,考慮原子操作或無鎖隊列
- ?線程退出管理?:確保所有線程能安全結束(避免死等)
通過理解這兩個問題的本質區別與內在聯系,可以設計出正確高效的多線程程序,既保證數據安全,又實現線程間高效協作。