多執行序
在 C++11 之前,C++ 並沒有標準化的多線程支持,因此需要依賴於平台特定的庫來實現多線程。 簡單來說,就是很麻煩。無法達到跨平台的效果,
例如:
在 Windows 上可以使用 WinAPI, 必須包含 <windows.h>
標頭檔,不需要額外安裝庫但僅適用於 Windows 系統。
而在 POSIX 系統(如 Linux 和 macOS)上則可以使用 POSIX Threads(pthread), 必須包含 <pthread.h>
標頭檔,POSIX Threads 是跨平台的,但需要安裝相關庫。
C++11 引入了一些新的標準庫來增強對多線程的支持,這些新特性使得編寫多線程程序變得更加簡單和安全。
多執行序 code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>
void print_message(const std::string& message) {
std::cout << message << std::endl;
}
int main() {
std::thread t1(print_message, "Hello from thread 1!");
std::thread t2(print_message, "Hello from thread 2!");
t1.join(); // 等待 t1 執行完畢
t2.join(); // 等待 t2 執行完畢
return 0;
}
執行結果:
由於 thread 的執行順序無法控制,以上述的例子就是無法確定誰會先執行 print_message function 因此兩次的執行結果都不一樣。
code with lock_guard
由於 thread 的執行順序,無法被確定。 而某些 code 我們希望一次只能有一個 thread 去執行,譬如 code 當中有個共同變數, 多個 thread 同時去存取易發生不可預期的資料錯誤。
這時候我們就需要使用 std::mutex
和 std::lock_guard
,防止數據競爭。
以下我們開 5 個 thread, 去執行 print_message, 讓他們去等待直到搶到 lock 的thread 才可以往下執行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
void print_message(int id) {
std::cout << "Thread " << id << " is waiting." << std::endl;
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Thread " << id << " is running." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout << "Thread " << id << " has finished." << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(print_message, i));
}
for (auto& t : threads) {
t.join();
}
return 0;
}
執行結果
解釋一下執行順序:
- t0(thread 0) 等待
- t0 搶到 lock 執行
- t3 等待
- t2 & t4 都在等待
- t1 等待
- t0 完成,釋放 lock
- t3 搶到 lock 執行
- t3 完成,釋放 lock
- t4 搶到 lock 執行
- t4 完成,釋放 lock
- t2 搶到 lock 執行
- t2 完成,釋放 lock
- t1 搶到 lock 執行
- t1 完成,釋放 lock
code with unique_lock
接下來我們希望可以減少 thread 的等待時間,讓整體效率更好。
std::unique_lock
,可以手動解鎖和上鎖。
讓可以被多個 thread 執行的部分繼續執行,執行完繼續上鎖。
比 std::lock_guard
多了更多彈性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
void print_message(int id) {
std::cout << "Thread " << id << " is waiting." << std::endl;
std::unique_lock<std::mutex> lock(mtx);
std::cout << "Thread " << id << " is running." << std::endl;
// 可以手動解鎖
lock.unlock();
// 執行一些不需要鎖保護的工作
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
// 重新上鎖
lock.lock();
std::cout << "Thread " << id << " has finished." << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(print_message, i));
}
for (auto& t : threads) {
t.join();
}
return 0;
}
執行結果
解釋一下執行順序:
- t0 等待
- t1 等待
- t1 搶到 lock 執行
- t3 等待
- t4 等待
- t2 等待
- t0 搶到 lock 執行
- t3 搶到 lock 執行
- t4 搶到 lock 執行
- t2 搶到 lock 執行
- t3 完成,釋放 lock
- t0 完成,釋放 lock
- t2 完成,釋放 lock
- t1 完成,釋放 lock
- t4 完成,釋放 lock
鎖的範圍
鎖的範圍由 std::unique_lock
or std::lock_guard
變量的作用域決定。當 std::unique_lock
變量進入作用域時,它會嘗試鎖定給定的 std::mutex
,並在該變量離開作用域時自動解鎖。
為何離開作用域會自動解鎖 這是因為 std::unique_lock
or std::lock_guard
遵循 RAII(Resource Acquisition Is Initialization)原則。RAII 是 C++ 的一種常見編程習慣,確保資源(如鎖、文件句柄、內存等)在其生命週期內被正確地分配和釋放。
std::unique_lock
or std::lock_guard
在其析構函數中會自動調用 unlock() 方法來釋放鎖。因此,當 std::unique_lock
or std::lock_guard
對象離開其作用域時(例如函數返回或者作用域結束),析構函數被調用,鎖就會被自動釋放。
使用時機
std::thread:當需要並行執行多個任務時,可以使用 std::thread 創建新線程。這在需要提高程序性能,充分利用多核處理器時特別有用。
std::mutex 和 std::lock_guard/std::unique_lock:當多個線程需要訪問共享資源時,使用 std::mutex 來保護這些資源,防止數據競爭。