線程
線程的創建
在 C++ 中,線程的創建核心是通過std::thread
類實現的,其構造函數需要傳入一個可調用對象(Callable Object)作為線程入口。可調用對象包括普通函數、lambda 表達式、函數對象(functor)、類的成員函數等。下面詳細介紹幾種常見的線程創建方式:
一、使用普通函數創建線程
最基礎的方式是將普通函數作為線程入口,可同時傳遞參數給函數。
#include <iostream>
#include <thread>// 普通函數:線程入口
void print_info(int thread_id, const std::string& message) {std::cout << "線程 " << thread_id << ": " << message << std::endl;
}int main() {// 創建線程:傳入函數名和參數(參數按順序傳遞)std::thread t1(print_info, 1, "Hello from thread 1");std::thread t2(print_info, 2, "Hello from thread 2");// 等待線程完成t1.join();t2.join();return 0;
}
說明:
std::thread
構造時,第一個參數是函數名,后續參數會被傳遞給該函數。- 若函數需要多個參數,直接在構造函數中按順序添加即可。
二、使用 lambda 表達式創建線程
lambda 表達式適合編寫簡短的線程邏輯,尤其當需要捕獲外部變量時非常方便。
#include <iostream>
#include <thread>int main() {int base = 100; // 外部變量// 用lambda表達式創建線程(捕獲外部變量base)std::thread t([&base](int offset) {// 線程邏輯:使用捕獲的base和傳入的offsetstd::cout << "線程內計算:" << base + offset << std::endl;}, 50); // 傳遞給lambda的參數(offset=50)t.join(); // 等待線程完成return 0;
}
說明:
- lambda 的捕獲列表(
[&base]
)用于訪問外部變量,&
表示按引用捕獲(可修改外部變量),=
表示按值捕獲(只讀)。 - lambda 后的參數(如
50
)會作為 lambda 的輸入參數。
三、使用函數對象(Functor)創建線程
函數對象是重載了operator()
的類 / 結構體,適合需要攜帶狀態(成員變量)的線程邏輯。
#include <iostream>
#include <thread>// 函數對象:重載operator()
struct Counter {int count; // 攜帶的狀態// 構造函數初始化狀態Counter(int init) : count(init) {}// 線程入口:operator()void operator()(int step) {for (int i = 0; i < 5; ++i) {count += step;std::cout << "當前計數:" << count << std::endl;}}
};int main() {// 創建函數對象(初始狀態count=0)Counter counter(0);// 用函數對象創建線程,傳遞參數step=2std::thread t(std::ref(counter), 2); // 注意用std::ref傳遞引用t.join();// 線程執行后,counter的狀態已被修改std::cout << "最終計數:" << counter.count << std::endl;return 0;
}
說明:
- 函數對象的成員變量(如
count
)可用于存儲線程的狀態,避免使用全局變量。 - 若需在線程中修改原對象(而非副本),需用
std::ref
傳遞引用(否則std::thread
會復制對象)。
四、使用類的成員函數創建線程
當線程邏輯需要訪問類的成員變量時,可將類的成員函數作為線程入口,需同時指定對象指針。
#include <iostream>
#include <thread>
#include <string>class Worker {
private:std::string name; // 成員變量public:Worker(const std::string& n) : name(n) {}// 成員函數:線程入口void work(int task_id) {std::cout << "工人 " << name << " 正在執行任務 " << task_id << std::endl;}
};int main() {Worker worker("Alice"); // 創建對象// 用成員函數創建線程:參數為(對象指針,成員函數地址,函數參數)std::thread t(&Worker::work, &worker, 1001); // &worker是對象指針t.join();return 0;
}
說明:
std::thread
構造時,第一個參數是成員函數地址(&Worker::work
),第二個參數是對象指針(&worker
),后續參數是成員函數的參數。- 若對象是動態分配的(
new Worker(...)
),則傳遞堆對象的指針即可。
關鍵注意事項
線程必須被 join 或 detach:
std::thread
對象銷毀前,必須調用join()
(等待線程結束)或detach()
(分離線程,使其獨立運行),否則會觸發std::terminate()
終止程序。參數傳遞的拷貝問題:
線程構造時傳遞的參數會被拷貝到線程內部,若需傳遞引用,需用std::ref
或std::cref
(常量引用),但需確保引用的對象生命周期長于線程。線程入口的生命周期:
若線程入口是臨時對象(如 lambda 或函數對象),需確保其生命周期覆蓋線程執行期,避免懸空引用。
總結
C++ 線程創建的核心是通過std::thread
綁定可調用對象,不同方式的適用場景:
- 普通函數:適合簡單、無狀態的線程邏輯。
- lambda 表達式:適合簡短邏輯或需要捕獲外部變量的場景。
- 函數對象:適合需要攜帶狀態的復雜邏輯。
- 成員函數:適合面向對象編程中,線程邏輯需訪問類成員的場景。
線程的銷毀
我們使用std::thread創建的線程對象是進程中的子線程,一般進程中還有主線程,在程序中就是main線程,那么當我們創建線程后至少是有兩個線程的,那么兩個線程誰先執行完畢誰后執行完畢,這是隨機的,但是當進程執行結束之后,主線程與子線程都會執行完畢,進程會回收線程擁有的資源。并且,主線程main執行完畢,其實整個進程也就執行完畢了。一般我們有兩種方式讓子線程結束,一種是主線程等待子線程執行完畢,我們使用join函數,讓主線程回收子線程的資源;另外一種是子線程與主線程分離,我們使用detach函數,此時子線程駐留在后臺運行,這個子線程就相當于被C++運行時庫接管,子線程執行完畢后,由運行時庫負責清理該線程相關的資源。使用detach之后,表明就失去了對子線程的控制。
void func()
{cout << "void func()" << endl;cout << "I'm child thread" << endl;
}void test()
{cout << "I'm main thread" << endl;thread th1(func);th1.join();//主線程等待子線程
}
線程的狀態
線程類中有一成員函數joinable,可以用來檢查線程的狀態。如果該函數為true,表示可以使用join()或者detach()函數來管理線程生命周期。
void test()
{thread t([]{cout << "Hello, world!" << endl;});if (t.joinable()) {t.detach();}
}void test2()
{thread th1([]{cout << "Hello, world!" << endl;});if (t.joinable()) {t.join();}
}
線程id
為了唯一標識每個線程,可以給每個線程一個id,類型為std::thread::id,可以使用成員函數get_id()進行獲取。
void test()
{thread th1([](){cout << "子線程ID:" << std::this_thread::get_id() << endl;});th1.join();
}
互斥鎖mutex
互斥鎖是一種同步原語,用于協調多個線程對共享資源的訪問。互斥鎖的作用是保證同一時刻只有一個線程可以訪問共享資源,其他線程需要等待互斥鎖釋放后才能訪問。在多線程編程中,多個線程可能同時訪問同一個共享資源,如果沒有互斥鎖的保護,就可能出現數據競爭等問題。
然而,互斥鎖的概念并不陌生,在Linux下,POSIX標準中也有互斥鎖的概念,這里我們說的互斥鎖是C++11語法層面提出來的概念,是C++語言自身的互斥鎖std::mutex,互斥鎖只有兩種狀態:上鎖與解鎖。
2、頭文件
#include <mutex>
class mutex;
3、常用函數接口
3.1、構造函數
constexpr mutex() noexcept;
mutex( const mutex& ) = delete;
3.2、上鎖
void lock();
3.3、嘗試上鎖
bool try_lock();
3.4、解鎖
void unlock();
3.5、使用示例
int gCount = 0;
mutex mtx;//初始化互斥鎖
?
void threadFunc()
{for(int idx = 0; idx < 1000000; ++idx){mtx.lock();//上鎖++gCount;mtx.unlock();//解鎖}
}
?
int main(int argc, char *argv[])
{thread th1(threadFunc);thread th2(threadFunc);
?th1.join();th2.join();cout << "gCount = " << gCount << endl;return 0;
}
三、lock_guard與unique_lock
在 C++ 多線程編程中,std::lock_guard
?和?std::unique_lock
?都是用于管理互斥鎖(std::mutex
)的RAII 風格工具類,核心作用是自動加鎖和解鎖,避免手動操作鎖導致的死鎖(如忘記解鎖、異常時未釋放鎖等問題)。但它們的靈活性和適用場景有顯著區別。
一、核心共同點
- 都遵循RAII 原則:構造時獲取鎖,析構時自動釋放鎖(無論正常退出還是異常退出)。
- 都用于保護臨界區,防止多線程并發訪問共享資源導致的數據競爭。
二、關鍵區別與適用場景
特性 | std::lock_guard | std::unique_lock |
---|---|---|
靈活性 | 簡單,功能有限 | 靈活,支持更多操作 |
手動解鎖 | 不支持(只能通過析構函數自動解鎖) | 支持(通過?unlock() ?手動解鎖) |
延遲鎖定 | 不支持(構造時必須鎖定) | 支持(通過?std::defer_lock ?延遲鎖定) |
嘗試鎖定 | 不支持 | 支持(通過?std::try_to_lock ?嘗試鎖定) |
所有權轉移 | 不支持(不可復制、不可移動) | 支持(可移動,不可復制) |
性能開銷 | 更低(輕量級) | 略高(因靈活性帶來的額外狀態管理) |
適用場景 | 簡單臨界區(全程需要鎖定) | 復雜場景(如條件變量、中途解鎖、延遲鎖定等) |
三、詳細說明與示例
1.?std::lock_guard
:簡單場景的首選
lock_guard
?是輕量級鎖管理工具,設計用于最簡單的場景:進入臨界區時加鎖,離開時解鎖,全程不需要手動干預。
特點:
- 構造函數必須鎖定互斥量(要么直接鎖定,要么接受一個已鎖定的互斥量,通過?
std::adopt_lock
?標記)。 - 沒有?
unlock()
?方法,只能在析構時自動解鎖(通常是離開作用域時)。 - 不可復制、不可移動,所有權無法轉移。
示例:
#include <mutex>
#include <iostream>std::mutex mtx;
int shared_data = 0;void increment() {// 構造時自動鎖定mtx,離開作用域(函數結束)時析構,自動解鎖std::lock_guard<std::mutex> lock(mtx);// 臨界區:安全訪問共享資源shared_data++;std::cout << "當前值: " << shared_data << std::endl;// 無需手動解鎖,lock析構時自動處理
}
適用場景:
- 臨界區邏輯簡單,從進入到退出全程需要鎖定。
- 不需要中途解鎖、延遲鎖定等復雜操作。
- 追求最小性能開銷。
2.?std::unique_lock
:復雜場景的靈活選擇
unique_lock
?是功能更全面的鎖管理工具,支持手動解鎖、延遲鎖定、嘗試鎖定等操作,適合需要靈活控制鎖狀態的場景。
特點:
- 支持延遲鎖定:通過?
std::defer_lock
?標記,構造時不鎖定互斥量,后續可通過?lock()
?手動鎖定。 - 支持手動解鎖:通過?
unlock()
?方法中途釋放鎖,之后可再次通過?lock()
?重新鎖定。 - 支持嘗試鎖定:通過?
std::try_to_lock
?標記,嘗試鎖定互斥量(成功返回 true,失敗不阻塞)。 - 支持所有權轉移:可通過移動語義(
std::move
)轉移鎖的所有權(不可復制)。 - 是條件變量(
std::condition_variable
)的必需參數:條件變量的?wait()
?方法需要?unique_lock
?作為參數,因為?wait()
?會在等待時釋放鎖,被喚醒時重新獲取鎖(這要求鎖可以手動解鎖和鎖定)。
示例 1:延遲鎖定與手動解鎖
#include <mutex>
#include <iostream>std::mutex mtx;void complex_operation() {// 延遲鎖定:構造時不鎖定,僅關聯互斥量std::unique_lock<std::mutex> lock(mtx, std::defer_lock);// 做一些不需要鎖定的操作std::cout << "準備鎖定..." << std::endl;// 手動鎖定lock.lock();std::cout << "已鎖定,執行臨界區操作..." << std::endl;// 中途手動解鎖(釋放鎖,允許其他線程訪問)lock.unlock();std::cout << "臨時解鎖,執行其他操作..." << std::endl;// 再次鎖定lock.lock();std::cout << "再次鎖定,完成剩余操作..." << std::endl;// 析構時自動解鎖(若當前處于鎖定狀態)
}
示例 2:與條件變量配合
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;void consumer() {std::unique_lock<std::mutex> lock(mtx);// 等待條件滿足:會釋放鎖并阻塞,被喚醒時重新獲取鎖cv.wait(lock, []{ return data_ready; });// 條件滿足,執行消費操作std::cout << "數據已準備好,開始處理..." << std::endl;
}void producer() {{std::lock_guard<std::mutex> lock(mtx);data_ready = true; // 生產數據} // 離開作用域,自動解鎖cv.notify_one(); // 通知消費者
}int main() {std::thread t1(consumer);std::thread t2(producer);t1.join();t2.join();return 0;
}
適用場景:
- 需要中途解鎖(如臨界區中間有耗時操作但無需鎖定)。
- 需要延遲鎖定(如先做準備工作,再根據條件決定是否鎖定)。
- 需要與條件變量配合(
wait()
?必須使用?unique_lock
)。 - 需要轉移鎖的所有權(如將鎖傳遞給其他函數)。
四、總結
- 優先使用?
std::lock_guard
:當場景簡單,臨界區全程需要鎖定時,它更輕量、更高效。 - 使用?
std::unique_lock
:當需要靈活性(手動解鎖、延遲鎖定、配合條件變量等)時,犧牲少量性能換取功能。
兩者的核心目標都是安全管理鎖的生命周期,避免手動操作鎖導致的錯誤,選擇時主要依據場景的復雜度和靈活性需求。
條件變量condition_variable
在 C++ 多線程編程中,std::condition_variable
(條件變量)是用于線程間同步的核心機制,它允許線程在滿足特定條件前阻塞等待,當條件滿足時被其他線程喚醒,從而實現高效的協作(避免無效輪詢)。
一、核心作用
條件變量解決的核心問題:讓線程在 “條件不滿足” 時進入休眠狀態,在 “條件滿足” 時被喚醒繼續執行,避免線程通過 “輪詢”(反復檢查條件)浪費 CPU 資源。
例如:
- 消費者線程等待生產者線程生成數據(“數據就緒” 是條件)。
- 主線程等待子線程完成初始化(“初始化完成” 是條件)。
二、核心 API 與工作機制
std::condition_variable
?定義在?<condition_variable>
?頭文件中,核心方法如下:
方法 | 作用 |
---|---|
wait(lock, pred) | 阻塞當前線程,釋放鎖并等待被喚醒;被喚醒后重新獲取鎖,檢查pred 是否為true ,若為true 則繼續執行,否則重新阻塞。 |
notify_one() | 喚醒一個正在等待該條件變量的線程(若有)。 |
notify_all() | 喚醒所有正在等待該條件變量的線程。 |
關鍵細節:
必須配合互斥鎖:條件變量的操作必須與互斥鎖(
std::mutex
)結合,且必須使用?std::unique_lock
(而非?std::lock_guard
),因為?wait()
?過程需要先釋放鎖、被喚醒后重新獲取鎖(unique_lock
?支持手動解鎖 / 加鎖,lock_guard
?不支持)。處理 “虛假喚醒”:操作系統可能在無明確通知時喚醒線程(虛假喚醒),因此?
wait()
?必須配合條件謂詞(pred)?使用,確保只有當條件真正滿足時才繼續執行。
三、工作流程(以生產者 - 消費者為例)
消費者線程:
- 鎖定互斥鎖,檢查條件(如 “數據是否就緒”)。
- 若條件不滿足,調用?
wait()
:釋放鎖并阻塞等待。 - 被喚醒后,重新獲取鎖,再次檢查條件(避免虛假喚醒)。
- 條件滿足時,執行操作(如消費數據)。
生產者線程:
- 鎖定互斥鎖,修改共享資源(如生成數據)。
- 調用?
notify_one()
?或?notify_all()
?喚醒等待的消費者。 - 釋放鎖(由?
unique_lock
?或?lock_guard
?自動完成)。
四、完整示例:生產者 - 消費者模型
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>// 共享隊列(緩沖區)
std::queue<int> buffer;
const int MAX_SIZE = 5; // 緩沖區最大容量// 同步工具
std::mutex mtx;
std::condition_variable cv_producer; // 生產者等待的條件變量(緩沖區不滿)
std::condition_variable cv_consumer; // 消費者等待的條件變量(緩沖區非空)// 生產者:向緩沖區添加數據
void producer(int id) {for (int i = 0; i < 10; ++i) {std::unique_lock<std::mutex> lock(mtx);// 等待緩沖區不滿(若滿則阻塞)cv_producer.wait(lock, []{ return buffer.size() < MAX_SIZE; });// 生產數據int data = id * 100 + i;buffer.push(data);std::cout << "生產者 " << id << " 生產: " << data << ",緩沖區大小: " << buffer.size() << std::endl;// 通知消費者:緩沖區非空cv_consumer.notify_one();// 模擬生產耗時std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}// 消費者:從緩沖區取出數據
void consumer(int id) {for (int i = 0; i < 10; ++i) {std::unique_lock<std::mutex> lock(mtx);// 等待緩沖區非空(若空則阻塞)cv_consumer.wait(lock, []{ return !buffer.empty(); });// 消費數據int data = buffer.front();buffer.pop();std::cout << "消費者 " << id << " 消費: " << data << ",緩沖區大小: " << buffer.size() << std::endl;// 通知生產者:緩沖區不滿cv_producer.notify_one();// 模擬消費耗時std::this_thread::sleep_for(std::chrono::milliseconds(150));}
}int main() {// 創建2個生產者和2個消費者std::thread p1(producer, 1);std::thread p2(producer, 2);std::thread c1(consumer, 1);std::thread c2(consumer, 2);// 等待所有線程完成p1.join();p2.join();c1.join();c2.join();return 0;
}
五、關鍵注意事項
必須使用?
unique_lock
:wait()
?方法的第一個參數必須是?std::unique_lock<std::mutex>
,因為?wait()
?內部會執行 “解鎖→阻塞→被喚醒后重新加鎖” 的操作,unique_lock
?支持這種靈活的鎖狀態管理(lock_guard
?不支持手動解鎖,無法配合?wait()
)。條件謂詞不可省略:即使你認為 “不會有虛假喚醒”,也必須在?
wait()
?中傳入條件謂詞(第二個參數)。例如:// 錯誤:未處理虛假喚醒 cv.wait(lock); // 正確:確保條件滿足才繼續 cv.wait(lock, []{ return condition; });
notify_one()
?與?notify_all()
?的選擇:notify_one()
:喚醒一個等待線程,適用于 “只有一個線程能處理” 的場景(如緩沖區只有一個數據)。notify_all()
:喚醒所有等待線程,適用于 “多個線程都能處理” 的場景(如廣播一個全局事件)。過度使用?notify_all()
?可能導致線程喚醒后競爭鎖,浪費資源。
避免持有鎖時長時間操作:喚醒線程后,應盡快釋放鎖(完成臨界區操作),避免其他線程被喚醒后因無法獲取鎖而阻塞。
生命周期管理:確保條件變量在所有等待線程退出前保持有效,避免訪問已銷毀的條件變量。
六、總結
std::condition_variable
?是多線程協作的高效工具,通過 “等待 - 通知” 機制替代輪詢,減少 CPU 浪費。其核心是:線程在條件不滿足時阻塞,條件滿足時被喚醒,配合互斥鎖和條件謂詞確保同步安全。典型應用包括生產者 - 消費者模型、線程池任務調度、事件驅動同步等。