第二章 執行緒管控
std::thread 簡介
構造和解構式
/// 默認構造
/// 創建一個執行緒,什么也不做
thread() noexcept;
/// 帶參構造
/// 創建一個執行緒,以 A 為引數執行 F 函式
template <class Fn, class... Args>
explicit thread(Fn&& F, Args&&... A);
/// 拷貝構造(不可用)
thread(thread& Other) noexcept = delete;
/// 移動構造
/// 移交執行緒的歸屬權
thread(thread&& x) noexcept;
/// 解構式
~thread();
常用成員函式
/// 等待執行緒結束并清理資源(會阻塞)
void join();
/// 回傳執行緒是否可以執行 join() 成員函式
bool joinable();
/// 將執行緒與呼叫其的執行緒分離,彼此獨立執行(此函式必須在執行緒創建時立即呼叫,
/// 且呼叫此函式會使其不能被join)
void detach();
/// 獲取執行緒id
std::thread::id get_id();
/// 見移動建構式
/// 如果物件是 joinable 的,那么會呼叫 std::terminate() 結果程式
thread& operator=(thread&& Other) noexcept;
使用執行緒類完成執行緒的基本管控
發起執行緒
- 執行緒通過構建std::thread物件而啟動,該物件指明執行緒要運行的任務,
void do_some_work();
std::thread my_thread(do_some_work);
- 對應復雜的任務,可以使用函式物件,
class background_task{
public:
void operator()() const{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread1(f);
// 使用匿名函式物件
// std::thread my_thread2(background_task());這樣寫會出錯,
// 編譯器會解釋成函式宣告,回傳值為 std::thread, 函式名為 my_thread2,
// 函式引數為函式指標型別 background_task (*)(void), 因此改為如下兩種方式:
std::thread my_thread2((background_task()));
// 采用新式的統一初始化語法(uniform initialization syntax,又名串列初始化)
std::thread my_thread2{background_task()};
等待執行緒完成
一旦啟動了執行緒,我們就需明確是要等待它結束(與之匯合 join()),還是任由它獨自運行(與之分離 detach())
std::thread my_thread1(f);
// ...
my_thread1.join();// 等待執行緒結束并清理資源
??? 同一個執行緒的
.join()方法不能被重復呼叫,否則程式會abort()
對 thread 封裝——thread_guard
基于 RAII 原理,對 std::thread 進行封裝
// std::thread 封裝(基于RAII)
class thread_guard{
thread& m_thread;
public:
explicit thread_guard(thread& t):m_thread(t){}
~thread_guard() {
// 檢查是否joinable是有必要的,重復 join() 會出錯
if (m_thread.joinable()){
m_thread.join();
}
}
// C++11 "=delete" 標記,宣告拷貝構造和復制賦值操作為被洗掉的函式
// 防止拷貝導致重復呼叫 join()
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
};
int main(){
thread t([] {cout << "hello" << endl; });
thread_guard g(t);
}
這樣執行緒,將隨著類的析構,自動被回收,
執行緒后臺運行
- 呼叫
std::thread物件的成員函式detach(),會令執行緒在后臺運行,遂無法與之直接通信, - 執行緒被分離,就無法等待它完結,也不可能獲得與它關聯的
std::thread物件,因而無法匯合該執行緒, - 分離的執行緒確實仍在后臺運行,其歸屬權和控制權都轉移給C++運行時庫(runtime library,又名運行庫),由此保證,一旦執行緒退出,與之關聯的資源都會被正確回收,
- UNIX作業系統中,有些行程叫作守護行程(daemon process),它們在后臺運行且沒有對外的用戶界面;沿襲這一概念,分離出去的執行緒常常被稱為守護執行緒(daemon thread)
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());// C++ 斷言,運算式為假就輸出診斷訊息并呼叫abort()函式中止程式
向執行緒函式傳遞引數
void f(int i,std::string const& s);
std::thread t(f,3,"hello");
- thread 執行帶有參考型別引數的函式
int b = 10;
// 必須使用 std::ref 指定引數按參考傳遞,否則引數默認以右值參考傳遞
// 右值參考無法轉換為參考型別引數
thread t([](int& a) { cout << a << endl; }, ref(b));
t.join();
- thread 執行類中的成員函式
class Person {
public:
void show(string const& name, unsigned int const& age) {
cout << "name: " << name << endl;
cout << "age: " << age << endl;
}
};
int main(){
Person p;
// 每個物件的成員函式都有一個默認的引數,就是該物件的指標(this 指標)
// 因此,這里第一個引數要傳入物件的地址
thread t(&Person::show, &p, "Tom", 22);
t.join();
}
- thread 執行的函式中,引數使用了移動構造
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));// 必須使用 std::move() 將左值轉換為右值
在C++標準庫中,有幾個類的歸屬權語意與 std::unique_ptr 一樣,std::thread 類就是其中之一,
它們沒有拷貝構造和復制賦值(=delete),只能移動不能復制,保證了任意時刻對于唯一的資源,只有唯一的物件與其對應,
對于 std::thread 只允許將執行緒歸屬權從一個物件移動給另一個物件,
移交執行緒的歸屬權
/// 移動構造
/// 移交執行緒的歸屬權
thread(thread&& x) noexcept;
???
只能從一個已關聯執行執行緒的
thread物件將執行緒歸屬權移交給還未關聯任何執行執行緒的thread物件,如果后者也關聯了某個執行執行緒,那么操作會使程式abort()因此重要原則是:只要std::thread物件正管控著一個執行緒,就不能簡單地向它賦新值,否則該執行緒會因此被遺棄,
std::thread 支持移動操作的意義是,函式可以便捷地向外部轉移執行緒的歸屬權
- 從函式內部回傳
std::thread物件
std::thread f(){
void some_function();
return std::thread(some_function);
}
std::thread g(){
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
- 執行緒歸屬權轉移到函式內部
void f(std::thread t);
void g(){
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}
thread_guard 升級
- 使得可以直接使用封裝好的類創建執行緒物件,并由類掌管,而不必現先在類外創建具名變數,
- 封裝好的類對執行緒有唯一歸屬權,不必擔心其他物件執行匯合或分離操作,
class soped_thread {
thread m_thread;
public:
explicit soped_thread(thread t) : m_thread(move(t)){
if (!m_thread.joinable()) throw logic_error("No thread");
}
~soped_thread() {
m_thread.join();
}
soped_thread(soped_thread const&) = delete;
soped_thread& operator=(soped_thread const&) = delete;
};
int main()
{
soped_thread t{ thread([] {
int i = 10;
while (i--)
{
cout << i << endl;
this_thread::sleep_for(chrono::seconds(2));
}
})};
}
使用示例
例1:thread 的基本使用
執行緒管控的自動化:若要為多個執行緒分別直接創建獨立變數,還不如將它們集結成組,統一處理,
void do_work(unsigned id) {
// 可以測驗一下 printf 和 cout << id << endl;兩種方式輸出
// 使用 cout << id << endl; 輸出id和輸出endl的連續動作可能被其他執行緒打斷,
printf("%d\n", id);
}
int main()
{
vector<thread> threads;
for (int i = 0; i < 10; i++){
threads.emplace_back(do_work, i);
}
for (auto& entry : threads) entry.join();
}
例2:執行緒分離的應用場景
場景描述:
一個文字處理的應用程式
同時編輯多個檔案,多個獨立的頂層視窗,分別與正在編輯的檔案逐一對應,視窗各有自己的選項單,
解決方案:使用多執行緒并行處理,
- 在同一應用程式的實體中運行,
- 相應的內部處理是,每個檔案的編輯視窗都在各自執行緒內運行;每個執行緒運行的代碼相同,而處理的資料有別,因為這些資料與各檔案和對應視窗的屬性關聯,
- 打開一個新檔案就需啟動一個新執行緒,新執行緒只處理打開新檔案的請求,并不牽涉等待其他執行緒的運行和結束,對其他執行緒而言,該檔案由新執行緒負責,與它們無關,
綜上,運行分離執行緒就成了首選方案,
void edit_document(std::string const& filename)
{
open_document_and_display_gui(filename);
while(!done_editing())
{
user_command cmd = get_user_input();// 獲取用戶輸入命令
// 如果用戶輸入命令是打開新檔案,就開啟一個新執行緒,在新執行緒下打開檔案
if(cmd.type == open_new_document)
{
std::string const new_name = get_filename_from_user();
// 開啟新執行緒打開該檔案
std::thread t(edit_document,new_name);
t.detach();// 分離新執行緒
}
else
{
// 執行用戶命令
process_user_input(cmd);
}
}
}
例3:thread 類的封裝 joining_thread 類
曾經有一份C++17標準的備選提案,主張引入新的類joining_thread,它與std::thread類似,但只要其執行解構式,執行緒即能自動匯合,這點與scoped_thread非常像,可惜C++標準委員會未能達成共識,結果C++17標準沒有引入這個類,后來它改名為std::jthread,依然進入了C++20標準的議程(現已被正式納入C++20標準),除去這些,實際上joining_thread類的代碼相對容易撰寫,代碼清單2.7展示了一個可行的實作,
class joining_thread
{
std::thread m_thread;
public:
joining_thread() noexcept = default;
template<typename Callable, typename ... Args>
explicit joining_thread(Callable&& func, Args&& ... args)
: m_thread(std::forward<Callable>(func), std::forward<Args>(args)...)
{}
explicit joining_thread(std::thread t) noexcept
: m_thread(std::move(t))
{}
joining_thread& operator=(joining_thread&& other) noexcept
{
if (joinable()) join();
m_thread = std::move(other.m_thread);
return *this;
}
joining_thread& operator=(std::thread other) noexcept
{
if (joinable()) join();
m_thread = std::move(other);
return *this;
}
~joining_thread() noexcept
{
if (joinable()) join();
}
void swap(joining_thread& other) noexcept
{
m_thread.swap(other.m_thread);
}
std::thread::id get_id() const noexcept
{
return m_thread.get_id();
}
bool joinable() const noexcept
{
return m_thread.joinable();
}
void join(){ m_thread.join(); }
void detach() { m_thread.detach(); }
std::thread& as_thread() noexcept { return m_thread; }
const std::thread& as_thread() const noexcept
{
return m_thread;
}
};
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549496.html
標籤:其他
上一篇:Python 元編程
