C++11提供了線程庫,下面我們來看一下如何使用。
線程的創建
頭文件
要創建一個線程需要包一個線程頭文件:#include <thread>
我們先來看看thread支持的構造方式。
支持默認構造,直接使用thread創建一個空的線程對象。
也支持帶參的構造,參數就是可執行的對象,可以使函數指針、仿函數、lamdba表達式、包裝器,參數是可執行對象需要傳的參數,因為是可變參數,因此根據實際的可執行對象傳遞參數個數即可。
線程是不支持拷貝構造的,不可以用一個線程對象拷貝另一個線程對象,但是支持移動構造。
無參構造 + 移動賦值
我們創建線程的時候可以先不給該線程關聯函數,創建一個空線程,等到后續有需要的時候在關聯函數,比如實現線程池就可以這樣做,線程池的線程是不知道要執行啥函數的。
直接使用 thread 這個類型,thread是一個類,里面實現了線程的各種方法,使用thread然后后面跟上變量名,這樣創建的線程是沒有啟動的線程,還沒有給該線程關聯要執行的函數,可以使用移動賦值來給該線程關聯要執行的函數,使用匿名構造,第一個參數傳遞要執行的函數,后續的參數是函數要傳遞的參數,如果函數有一個參數就傳遞1個有兩個參數就傳遞2個。線程的構造函數是一個函數對象和可變參數列表。
我們創建好線程之后需要進行等待,直接使用thread的join函數即可,該函數沒有返回值和參數。
get_id()可以獲取線程的id,這個函數在this_thread這個命名空間里。因此使用時需要使用這個空間域。
#include <iostream>
#include <thread>
using namespace std;void func(int x)
{cout << this_thread::get_id() << endl;for(int i = 1; i <= x; i++)cout << "music" << endl;
}
int main()
{// 無參構造 + 移動賦值thread t1; // 沒啟動的線程t1 = thread(func, 9);// 線程等待t1.join();return 0;
}
總結:
1.帶參構造,創建可執行對象
2.創建空線程對象,移動構造或者移動賦值,把右值線程對象轉移過去?
帶參構造
第二種方式是直接在創建線程的時候就給該線程關聯上要執行的函數
#include <iostream>
#include <thread>
using namespace std;void func(int x)
{cout << this_thread::get_id() << endl;for(int i = 1; i <= x; i++)cout << "music" << endl;
}
int main()
{// 2.帶參構造thread t2(func, 9);// 線程等待t2.join();return 0;
}
移動構造
第三種方式是使用移動構造
#include <iostream>
#include <thread>
using namespace std;void func(int x)
{cout << this_thread::get_id() << endl;for(int i = 1; i <= x; i++)cout << "music" << endl;
}
int main()
{// 3.移動構造thread t3 = thread(func, 9);// 線程等待t3.join();return 0;
}
鎖
ref
如果我們現在要使用兩個線程對一個局部變量進行++操作,那么我們應該如何寫代碼呢?我們肯定是創建兩個線程,然后把該局部變量以引用的方式傳遞過去,這樣線程執行的函數就會修改我們傳遞的局部變量,這里當然是有線程安全問題需要加鎖,但我們先不考慮加鎖,我們看下面的代碼。
#include <iostream>
#include <thread>
using namespace std;void add(int& x)
{for (int i = 0; i < 100; i++){x++;}
}int main()
{int num = 0;// 錯誤寫法thread t1(add, num);// 這樣寫會報錯,看似是引用的方式接收,但實際上接收的是num的拷貝// 正確寫法thread t1(add, ref(num)); // 如果是引用方式接收,需要使用ref轉一下,這樣寫才正確// 錯誤寫法thread t2(add, num);// 這樣寫會報錯,看似是引用的方式接收,但實際上接收的是num的拷貝// 正確寫法thread t2(add, ref(num));// 如果是引用方式接收,需要使用ref轉一下,這樣寫才正確this_thread::sleep_for(chrono::seconds(2));cout << num << endl;t1.join();t2.join();return 0;
}
上面的代碼看上去是正確的,但是實際我們編譯的時候發現編譯不過去,原因在于add的參數是引用的方式接收的,但是由于num是先傳給了t1的構造函數,然后在給add傳遞過去,相當于不是直接傳過去,而是中間轉了一層,所以這里看似是傳遞的num但實際傳遞的是num的拷貝,因此如果要傳遞num的引用,需要加上ref()。
休眠可以使用this_thread::sleep_for這個函數,chrono是時鐘的意思,seconds是按秒休眠,也可以按毫秒milliseconds休眠。
mutex
這里毫無疑問是有線程安全的問題的,因此是需要加鎖的。
要使用鎖需要包含頭文件,#include <mutex>
然后直接就可以使用mutex定義一把鎖,如果要加鎖調用lock方法,解鎖調用unlock方法即可。
此時的代碼就可以這樣更改。
void add(mutex& mtx, int& x)
{for (int i = 0; i < 100; i++){mtx.lock();x++;mtx.unlock();}
}int main()
{int num = 0;mutex mt; //定義一把鎖thread t1(add, ref(mt), ref(num));thread t2(add, ref(mt), ref(num));this_thread::sleep_for(chrono::seconds(2));cout << num << endl;t1.join();t2.join();return 0;
}
unique_lock和lock_guard
但是有的時候我們可能加鎖的時候忘記解鎖了,就會導致死鎖,那么我們可以使用智能指針把鎖管理起來,創建鎖的時候直接加鎖,出了作用域變量銷毀在析構的時候直接解鎖,此時我們就不需要手動的加鎖解鎖了,那么我們要自己實現嗎?C++已經給我們提供了對鎖進行管理的類,unique_lock和lock_guard,我們直接使用即可。
我們先來看unique_lock。
unique_lock是一個模板類,是專門用來管理所的類,類型直接傳遞鎖的類型mutex即可,然后構造的時候把鎖傳遞過去即可,一下是他的一些構造函數。
因此我們在線程函數里可以使用unique_lock,對傳遞過來的鎖進行管理。
void add(mutex& mtx, int& x)
{for (int i = 0; i < 100; i++){unique_lock<mutex> lock(mtx);x++;}
}
lock_guard和unique_lock的使用方法是一樣的。
void add(mutex& mtx, int& x)
{for (int i = 0; i < 100; i++){//unique_lock<mutex> lock(mtx);lock_guard<mutex> lock(mtx);x++;}
}
lock_guard相比unique_lock來說更輕量一些,unique_lock則更加靈活,可以支持延遲鎖定、嘗試鎖定等,但開銷會比較大。這兩個都是基于RAII的,頭文件都是<mutex>,它們兩個可以用來配合條件變量。
atomic
我們加鎖是因為對一個數++操作不是原子的,C++給我們們提供了atomic這個類,我們可以直接使用這個類定義變量,定義的變量++和--操作是原子的操作,此時就可以不需要加鎖。
使用非常簡單,atomic是一個類,如果要定義一個int類型的變量,直接模版參數傳遞int即可,比如說atomic<int> nums, 此時對nums++和--就是原子的操作,也可以對他進行打印和賦值,使用起來和int類型是一樣的。
void add(mutex& mtx, atomic<int>& x)
{for (int i = 0; i < 100; i++){x++;}
}int main()
{atomic<int> num = 0;mutex mt;thread t1(add, ref(mt), ref(num));thread t2(add, ref(mt), ref(num));this_thread::sleep_for(chrono::seconds(2));cout << num << endl;t1.join();t2.join();return 0;
}
條件變量
頭文件:#include <condition_variable>
要使用條件變量需要包含上面的頭文件,創建條件變量直接使用condition_variable創建一個變量即可。
wait方法:將該線程加入到阻塞隊列中
我們可以直接調用wait就可以將線程加入到阻塞隊列,傳遞的參數是一個RAII的鎖,也就是unique_lock管理的鎖,加入到阻塞隊列后鎖會被釋放。
?喚醒線程:將在阻塞隊列的函數喚醒
喚醒線程有兩個,一個是喚醒阻塞隊列中的全部線程,一個是喚醒阻塞隊列中的一個線程。這兩個喚醒函數都不需要傳遞參數。
接下來我們用條件變量來實現一個兩個線程交替打印奇偶數
兩個線程交替打印奇偶數
首先要創建兩個線程,當然也需要創建鎖和條件變量,也需要一個變量number,number初始化為1,我們規定,讓線程1先來打印number,線程1打印完畢后對number進行+1操作,此時number就變成了偶數,然后讓線程2打印number,此時打印的就是偶數,然后+1,讓線程1打印,如此交替執行即可。
既然要讓線程1先打印,那么線程2一定要被阻塞住,否則是無法保證打印順序的。一開始線程1打印,然后線程2被阻塞,線程1打印完畢后要喚醒線程2去打印,然后把自己阻塞住,線程2打印完畢后喚醒線程1,然后讓自己阻塞,這樣就可以控制打印順序。
那么我們如何控制誰先打印和阻塞?我們可以使用一個flag標記。
剛開始flag是假,線程1的判斷條件是while(flag),因此,一開始線程1不會被阻塞會直接打印,而線程2條件是while(!flag),會直接進入阻塞隊列阻塞,當線程1打印完后,把flag置為true,然后喚醒線程2,線程2的判斷條件不成立不會進入阻塞隊列會打印線程1,然而線程1條件成立會進入阻塞隊列,線程2打印完畢后把flag置為false,自己會進入阻塞隊列,而線程1條件不成立會直接打印,此時就完成了打印順序的控制。
當然我們也可以不需要加鎖,用atomic創建變量num即可。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>using namespace std;
int main()
{int num = 1;bool flag = false;mutex mtx; // 鎖condition_variable cond; // 條件變量thread t1([&]() {for (int i = 0; i < 50; ++i){unique_lock<mutex> lock(mtx);while (flag)cond.wait(lock);cout << "線程1:" << num << endl;num++;flag = true;cond.notify_one();}});thread t2([&]() {for (int i = 0; i < 50; i++){unique_lock<mutex> lock(mtx);while (!flag) cond.wait(lock);cout << "線程2:" << num << endl;num++;flag = false;cond.notify_one();}//std::this_thread::sleep_for(std::chrono::seconds(1));});t1.join();t2.join();return 0;
}