文章目錄
- C++11 線程庫
- 線程對象的構造方式
- 無參的構造函數
- 調用帶參的構造函數
- 調用移動構造函數
- thread常用成員函數
- this_thread命名空間
- join && detach
- mutex
C++11 線程庫
線程對象的構造方式
無參的構造函數
1、調用無參的構造函數,調用無參的構造函數創建出來的線程對象沒有關聯任何線程函數,即沒有啟動任何線程
thread t1;
2、thread提供了移動賦值函數,當后續需要讓該線程對象與線程函數關聯時,可以以帶參的方式創建一個匿名對象,然后調用移動賦值將該匿名對象關聯線程的狀態轉移給該線程對象
void Print(size_t n , const std::string& s)
{for (size_t i = 0; i < n; i++){std::cout << std::this_thread::get_id() << ":" << i << std::endl;}
}
int main()
{int n = 10;// 創建n個線程執行Printstd::vector<std::thread> vthd(n);size_t j = 0;for (auto& thd : vthd){// 移動賦值thd = std::thread(Print, 10, "線程" + std::to_string(j++));}for (auto& thd : vthd){thd.join();}return 0;}
場景:
實現線程池的時候, 需要先創建一批線程,但,一開始這些線程什么也不做,當有任務到來時再讓這些線程來處理這些任務。
#include <iostream>
#include <vector>
#include <thread>
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>class ThreadPool {
public:ThreadPool(size_t threads = 4); // 構造函數,可以指定線程池的大小~ThreadPool(); // 析構解函數void enqueue(std::function<void()> task); // 添加任務到線程池private:std::vector<std::thread> workers; // 線程向量std::queue<std::function<void()>> tasks; // 任務隊列隊std::mutex queue<std::function<void()>> completedTasks;std::condition_variable cond; // 條件變量bool stop; // 停止標志void work(); // 線程工作函數
};// 構造函數初始化線程池
ThreadPool::ThreadPool(size_t threads) : stop(false) {for(size_t i = 0; i < threads; ++i)workers.emplace_back(std::thread(&ThreadPool::work, this));
}// 析構解函數等待所有線程完成
ThreadPool::~ThreadPool() {{std::unique_lock<std::mutex> lock(this->mtx);this->stop = true;}cond.notify_all();for(std::thread &worker : workers) {if(worker.joinable()) {worker.join(); // 等待線程結束}}
}// 添加任務到線程池
void ThreadPool::enqueue(std::function<void()> task) {{std::unique_lock<std::mutex> lock(mtx);if(stop)throw std::runtime_error("enqueue on stopped ThreadPool");tasks.emplace(task);}cond.notify_one();
}void ThreadPool::work() {while(true) {std::function<void()> task;{std::unique_lock<std::mutex> lock(this->mtx);while(tasks.empty()) {cond.wait(lock); // 如果沒有任務,等待}task = std::move(tasks.front());tasks.pop();}();task(); // 執行任務}
}
在這個示例中,ThreadPool
類管理一組工作線程。構造函數創建指定數量的線程,每個線程都調用work
方法等待并執行任務。enqueue
方法用于添加新任務到隊列,如果線程池已經停止,則拋出異常。每個工作線程在接收到任務后會出隊列并執行它
調用帶參的構造函數
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
fn
:可調用對象,比如函數指針、仿函數、lambda表達式、被包裝器包裝后的可調用對象等。args...
:調用可調用對象fn時所需要的若干參數
調用移動構造函數
用一個右值線程對象來構造一個線程對象
void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t3 = thread(func, 10);t3.join();return 0;
}
線程是操作系統中的一個概念,線程對象可以關聯一個線程,用來控制線程以及獲取線程的狀態。
如果創建線程對象時沒有提供線程函數,那么該線程對象實際沒有對應任何線程。
如果創建線程對象時提供了線程函數,那么就會啟動一個線程來執行這個線程函數,該線程與主線程一起運行。
thread類是防拷貝的,不允許拷貝構造和拷貝賦值,但是可以移動構造和移動賦值,可以將一個線程對象關聯線程的狀態轉移給其他線程對象,并且轉移期間不影響線程的執行
thread常用成員函數
join ,對該線程進行等待,在等待的線程返回之前,調用join函數的線程將會被阻塞
joinable , 判斷該線程是否已經執行完畢,如果是則返回true,否則返回false
detach ,將該線程與創建線程進行分離,被分離后的線程不再需要創建線程調用join函數對其進行等待
get_id , 獲取該線程的id
swap , 將兩個線程對象關聯線程的狀態進行交換
joinable函數還可以用于判定線程是否是有效的,
如果是以下任意情況,則線程無效:
采用無參構造函數構造的線程對象。(該線程對象沒有關聯任何線程)
線程對象的狀態已經轉移給其他線程對象。(已經將線程交給其他線程對象管理)
線程已經調用join或detach結束。(線程已經結束)
thread的成員函數get_id
可以獲取線程的id,但該方法必須通過線程對象來調用get_id
函數
如果要在線程對象關聯的線程函數中獲取線程id,調用this_thread
命名空間下的get_id
函數
void func()
{cout << this_thread::get_id() << endl; //獲取線程id
}
int main()
{thread t(func);t.join();return 0;
}
this_thread命名空間
yield | 當前線程“放棄”執行,讓操作系統調度另一線程繼續執行 |
---|---|
sleep_until | 讓當前線程休眠到一個具體時間點 |
sleep_for | 讓當前線程休眠一個時間段 |
線程傳參
1、使用lambda
#include<thread>
#include<iostream>
#include<vector>
#include<string>int main()
{size_t n1 = 5;size_t n2 = 5;/*std::cin >> n1 >> n2;*/std::thread t1( [n1](){for (size_t i = 0; i < n1; i++){//拿到該線程的線程idstd::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;}} );std::thread t2([n2](){for (size_t i = 0; i < n2; i++){//拿到該線程的線程idstd::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;}} );t2.join();t1.join();return 0;
}
join && detach
啟動一個線程后,當這個線程退出時,需要對該線程所使用的資源進行回收,否則可能會導致內存泄露等問題。thread庫提供了兩種回收線程資源的方式:
1、join
主線程創建新線程后,調用join函數等待新線程終止,當新線程終止時join
函數就會自動清理線程相關的資源
join
函數清理線程的相關資源后,thread對象與已銷毀的線程就沒有關系了,因此一個線程對象一般只會使用一次join
,如果一個線程對象使用多次join , 程序會崩潰
void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t(func, 20);t.join();t.join(); //程序崩潰return 0;
}
2、detach
主線程創建新線程后,也可以調用detach函數將新線程與主線程進行分離,分離后新線程會在后臺運行,其所有權和控制權將會交給C++運行庫,此時C++運行庫會保證當線程退出時,其相關資源能夠被正確回收。
使用detach的方式回收線程的資源,一般在線程對象創建好之后就立即調用detach函數。
否則線程對象可能會因為某些原因,在后續調用detach函數分離線程之前被銷毀掉,這時就會導致程序崩潰。
因為當線程對象被銷毀時會調用thread的析構函數,而在thread的析構函數中會通過joinable判斷這個線程是否需要被join,如果需要那么就會調用terminate終止當前程序(程序崩潰)
#include <iostream>
#include <thread>
#include <vector>void worker() {std::cout << "Working..." << std::endl;// 模擬耗時操作std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Finished" << std::endl;
}int main() {std::vector<std::thread> threads;// 創建并分離多個線程for (int i = 0; i < 5; ++i) {threads.emplace_back(std::thread(worker));threads.back().detach(); // 分離線程}std::cout << "Main thread continues to run..." << std::endl;// 主線程可以繼續執行其他任務,而不需要等待分離的線程// 等待所有線程完成(可選)for (auto& th : threads) {if (th.joinable()) {th.join();}}std::cout << "All threads finished" << std::endl;return 0;
}
過度使用分離線程可能會導致資源泄露,因為分離的線程將繼續運行,即使主線程已經結束。因此,通常建議在可能的情況下使用join
來管理線程的生命周期。
mutex
C++11中,mutex中總共包了四種互斥量
1、std::mute
mutex鎖是C++11提供的最基本的互斥量,mutex對象之間不能進行拷貝,也不能進行移動
mutex常用的成員函數:
lock | 對互斥量進行加鎖 |
---|---|
try_lock | 嘗試對互斥量進行加鎖 |
unlock | 對互斥量進行解鎖,釋放互斥量的所有權 |
線程函數調用lock時,可能會發生以下三種情況:
1、如果該互斥量當前沒有被其他線程鎖住,則調用線程將該互斥量鎖住,直到調用unlock之前,該線程一致擁有該鎖。
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx; // 全局互斥鎖void printBlock(int n) {mtx.lock(); // 獲取鎖,如果鎖已經被其他線程占用,則等待for (int i = 0; i < n; ++i) {std::cout << "Thread " << std::this_thread::get_id() << " says " << i << std::endl;}mtx.unlock(); // 釋放鎖,其他線程可以獲取該鎖
}int main() {std::thread t1(printBlock, 1);std::thread t2(printBlock, 2);t1.join();t2.join();return 0;
}
mtx
是一個全局互斥鎖,所以一次只能有一個線程執行printBlock
函數。這意味著即使兩個線程幾乎同時運行,輸出也將是交錯的,而不是同時打印,因為一個線程在打印時會鎖定互斥鎖,另一個線程必須等待
2、如果該互斥量已經被其他線程鎖住,則當前的調用線程會被阻塞。
互斥鎖(mutex)在多線程環境中的基本行為。互斥鎖是一種同步機制,用于防止多個線程同時訪問共享資源,從而避免數據競爭條件和不一致的狀態。當一個線程嘗試獲取已經被其他線程持有的互斥鎖時,該線程將被阻塞,直到互斥鎖被釋放
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::mutex mtx; // 全局互斥鎖void worker(int id) {// 嘗試獲取鎖mtx.lock();std::cout << "Thread " << id << " acquired the lock" << std::endl;// 模擬耗時的工作std::this_thread::sleep_for(std::chrono::milliseconds(500));std::cout << "Thread " << id << " released the lock" << std::endl;// 釋放鎖mtx.unlock();
}int main() {std::thread t1(worker, 1);std::thread t2(worker, 2);// 主線程稍作等待,以便 t1 有機會獲取鎖std::this_thread::sleep_for(std::chrono::milliseconds(100));// 嘗試在 t1 持有鎖時獲取鎖mtx.lock();std::cout << "Main thread acquired the lock" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模擬主線程持有鎖mtx.unlock();t1.join();t2.join();return 0;
}
3、如果該互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。
如何使用局部變量加鎖?
例1:用lambda
int main()
{size_t n1 = 10000;size_t n2 = 10000;/*std::cin >> n1 >> n2;*/int x = 0;std::mutex mtx;std::thread t1([&n1, &x ,&mtx]() {for (size_t i = 0; i < n1; i++){mtx.lock();x++;//拿到該線程的線程id//std::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;mtx.unlock();}});std::thread t2([&n2, &x,&mtx]() {for (size_t i = 0; i < n2; i++){mtx.lock();//拿到該線程的線程idx++;//std::cout << std::this_thread::get_id() << ":" << i << " " << std::endl;mtx.unlock();}});t2.join();t1.join();std::cout << x;return 0;
}
例2:
void Print1(size_t n,size_t j , const std::string& s ,std::mutex & mtx) //鎖必須傳引用 ,鎖不支持拷貝
{for (size_t i = 0; i < j+n; i++){mtx.lock();std::cout << std::this_thread::get_id() << ":" << i << std::endl;mtx.unlock();}
}
int main()
{int n = 10;// 創建n個線程執行Printstd::vector<std::thread> vthd(n);std::mutex mtx;std::thread t1(Print1, 100, 1, "hello", ref(mtx )); //必須加ref函數 ,否則鎖不能以傳引用的方式傳參傳過去std::thread t2(Print1, 100, 100000, "world", ref(mtx));t1.join();t2.join();return 0;}
例3
void Print1(size_t n, const std::string& s, std::mutex& mtx ,int & rx) //鎖必須傳引用 ,鎖不支持拷貝
{for (size_t i = 0; i < n; i++){mtx.lock();std::cout << std::this_thread::get_id() << ":" << i << std::endl;++rx;mtx.unlock();std::this_thread::sleep_for(std::chrono::microseconds(1000));}
}int main()
{std::mutex mtx;int x = 10;std::thread t1(Print1, 5, "hello", std::ref(mtx), std::ref(x));std::thread t2(Print1,5, "world", std::ref(mtx), std::ref(x));std::cout << "線程1: " << t1.get_id() << std::endl;std::cout << "線程2: " << t2.get_id() << std::endl;t1.join();t2.join();std::cout << x << std::endl;return 0;
}
線程調用try_lock時,類似也可能會發生以下三種情況:
1、如果該互斥量當前沒有被其他線程鎖住,則調用線程將該互斥量鎖住,直到調用unlock之前,該線程一致擁有該鎖。
2、如果該互斥量已經被其他線程鎖住,則try_lock調用返回false,當前的調用線程不會被阻塞。
3、如果該互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)