2 線程啓動、結束,創建線程方法,join、detach

2 線程啓動、結束,創建線程方法,join、detach


2.1 範例演示線程運行的開始和結束

​ 每個程序至少有一個線程:執行main()函數的線程(爲主線程),其餘線程有其各自的入口函數。線程與主線程(線程以main()爲入口函數的線程)同時運行。如同main()函數執行完會退出一樣,當線程執行完入口函數後,線程也會退出。在爲一個線程創建了一個std::thread對象後,需要等待這個線程結束;不過,線程需要先進行啓動。下面就來啓動線程。

  • 包含一個頭文件thread

    #include <thread>
    
  • 初始函數要寫

    void func() {
        cout << "子線程開始" << endl;
      	cout << "do something..." << endl;
      	cout << "子線程結束" << endl;
    }
    
  • main中寫開始代碼

    int main() {
      thread t(func);
      return 0;
    }
    

代碼2-1-1

#include <iostream>
#include <thread>
using namespace std;
void func() {
    cout << "子線程開始" << endl;
  	cout << "do something..." << endl;
  	cout << "子線程結束" << endl;
}
int main() {
  thread t(func);
  return 0;
}

運行結果

如果主線程執行速度快於子線程的,將會是以下結果

libc++abi.dylib: terminating
Abort trap: 6

2.1.1 居然直接報錯了,這是爲什麼呢?

​ 主線程執行完畢,thread對象消亡時(即代碼中的t對象),如果thread是joinable的(associated應該翻譯爲可關聯的,也就是joinable的) ,析構函數會調用terminate(),terminate()會調用abort(), abort()是非正常結束進程,不進行任何清理工作,直接終止程序,其典型實現是輸出標準錯誤流(即cerr使用的錯誤流)。如果thread被調用過join()或者detach(),那它就不是joinable的,所以不會引發terminate,進程正常終止。

2.1.2 “秀兒”的解決方法

修改代碼,在main()函數中加入while(1)

代碼2-1-2

#include <iostream>
#include <thread>
using namespace std;
void func() {
    cout << "子線程開始" << endl;
  	cout << "do something..." << endl;
  	cout << "子線程結束" << endl;
}
int main() {
  thread t(func);
  while(1);
  return 0;
}

這樣,子線程一定會先執行完成,主線程卡在死循環裏~,讓我們看看運行結果吧~

運行結果

子線程開始
do something...
子線程結束

這下終於不會報錯,且打出了我們想要的結果,但這樣的程序肯定不是我們所想要的。

2.1.3 如何解決報錯問題,利用detach()

代碼2-1-3

#include <iostream>
#include <thread>
using namespace std;
void func() {
    cout << "子線程開始" << endl;
  	cout << "do something..." << endl;
  	cout << "子線程結束" << endl;
}
int main() {
  thread t(func);
  t.detach();
  return 0;
}

運行結果

有時候這樣

子線程開始
do something...
子線程結束

有時候啥也沒有


2.1.5 有時候有,有時候無這是爲什麼呢?

​ 一旦detach()之後,與這個主線程關聯的thread對象就會失去與這個主線程的關聯,此時這個子線程就會駐留在後臺運行(主線程跟該子線程失去聯繫)這個子線程就相當於被C++運行時庫接管了,當這個子線程執行完畢後,由運行時庫負責清理該線程相關的資源。

2.1.6 怎麼才能讓程序正常運行,且輸出想要的結果?利用join()

  • 有關join()的解釋

    ​ 加入/匯合子線程,即阻塞主線程,讓主線程等待子線程執行完畢,然後子線程和主線程匯合,然後繼續執行以下代碼,如果主線程執行完畢,子線程沒有執行完畢,這種程序是不合格的,寫出來的程序也是不穩定的 一個書寫良好的程序,應該是主線程,等待子線程執行完畢後,自己才能最終退出。

代碼2-1-4

#include <iostream>
#include <thread>
using namespace std;
void func() {
    cout << "子線程開始" << endl;
  	cout << "do something..." << endl;
  	cout << "子線程結束" << endl;
}
int main() {
  thread t(func);
  t.join();
  return 0;
}

運行結果

子線程開始
do something...
子線程結束

2.1.7 如何判斷線程是否能夠join?利用joinable()

  • joinable()的解釋

    ​ 判斷是否可以成功調用join或者detach,返回true和false,true:可以join和detach,false:不可以join和detach,且detach的線程,不可以再次join,join過的線程也不可以join

2.2 其他創建線程的手法

2.2.1 用類,以及一個問題範例

如果用類作爲一個參數傳遞給入口函數,類必須重載operator()構造函數,即向入口函數傳遞一個factor實例。

Attention!!

有件事需要注意,當把函數對象傳入到線程構造函數中時,需要避免“最令人頭痛的語法解析”(C++’s most vexing parse)。如果你傳遞了一個臨時變量,而不是一個命名的變量;C++編譯器會將其解析爲函數聲明,而不是類型對象的定義。

thread t(A());

這裏相當與聲明瞭一個名爲A的函數,這個函數帶有一個參數(函數指針指向沒有參數並返回A對象的函數),返回一個std::thread對象的函數,而非啓動了一個線程。

使用在前面命名函數對象的方式,或使用多組括號①,或使用新統一的初始化語法②,可以避免這個問題。

thread t((A()));  // 1
thread t{A()};    // 2

代碼2-2-1

#include <iostream>
#include <thread>
using namespace std;

class A {
public:
    int &m_i;
    A(int &i):m_i(i) {
        cout << "A的構造函數" << endl;
    }
    void operator() () {
        cout << "線程開始" << endl;
        cout << "m_i的值爲:" << this->m_i << endl; // 產生不可預料的結果
        cout << "線程結束" << endl;
    }
};

int main() {
    int num = 6;
    A a(num);
    thread t(a);
    t.detach();
    return 0;
}

運行結果

A的構造函數

2.2.2 爲啥能安全地傳遞對象?

​ 雖然未打印出6的值,但我們的關注點並不是這個(線程在後臺已經執行了打印的語句,但是主線程先執行完畢,所以看不到輸出結果),而是a作爲主線程的局部變量,當主線程執行完畢,a變量釋放,按一般情況會出錯,真的會出錯嗎?

​ 答案是:不會的~。爲了進一步說明這個現象,在類中加入了拷貝構造函數和析構函數,並打印主線程對象的地址和子線程中this的值(即對象的地址)代碼如下:

代碼2-2-2

#include <iostream>
#include <thread>
using namespace std;

class A {
public:
    int &m_i;
    A(int &i):m_i(i) {
        cout << "A的構造函數" << endl;
      	cout << "this's addr is: " << this << endl;
    }
    A(const A& a):m_i(a.m_i) {
        cout << "A的拷貝構造函數" << " this's addr is: " << this << endl;
    }
    ~A() {
        cout << "A的析構函數" << " this's addr is: " << this << endl;
    }
    void operator() () {
        cout << "線程開始" << endl;
        cout << "m_i的值爲:" << this->m_i << " this's addr is: " << this << endl; 
        cout << "線程結束" << endl;
    }
};

int main() {
    int num = 6;
  	cout << "num's addr is : " << &num << endl;
    A a(num);
    thread t(a);
    t.join(); // 方便觀察
    return 0;
}

運行結果

A的構造函數
this's addr is: 0x7ffee0bb2710
a's addr is : 0x7ffee0bb2718
A的拷貝構造函數this's addr is: 0x7ffee0bb26f0
A的拷貝構造函數this's addr is: 0x7f8490402bd8
A的析構函數this's addr is: 0x7ffee0bb26f0
線程開始
m_i的值爲:6 this's addr is: 0x7f8490402bd8
線程結束
A的析構函數this's addr is: 0x7f8490402bd8
A的析構函數this's addr is: 0x7ffee0bb2710

​ 主線程中a對象的地址爲0x7ffee45d8718,最終執行輸出對象的地址爲0x7f8490402bd8,**結論:由主線程傳遞給子線程的對象實際上是被是被複制到線程中去的。

2.2.3 用lambda表達式

代碼2-2-3

#include <iostream>
#include <thread>
using namespace std;

int main() {
    auto annoymous_func = [] {
        cout << "線程開始" << endl;
      	cout << "do something" << endl;
        cout << "線程結束" << endl;
    };
    thread t(annoymous_func);
    t.join();
    return 0;
}

運行結果

線程開始
do something...
線程結束
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章