[c++11]多線程編程(二)——理解線程類的構造函數

構造函數的參數

std::thread類的構造函數是使用可變參數模板實現的,也就是說,能夠傳遞任意個參數,第一個參數是線程的入口函數,然後面的若干個參數是該函數的參數ios

第一參數的類型並非c語言中的函數指針(c語言傳遞函數都是使用函數指針),在c++11中,增長了可調用對象(Callable Objects)的概念,總的來講,可調用對象能夠是如下幾種狀況:c++

  • 函數指針
  • 重載了operator()運算符的類對象,即仿函數
  • lambda表達式(匿名函數)
  • std::function

函數指針示例

// 普通函數 無參
void function_1() {
}

// 普通函數 1個參數
void function_2(int i) {
}

// 普通函數 2個參數
void function_3(int i, std::string m) {
}

std::thread t1(function_1);
std::thread t2(function_2, 1);
std::thread t3(function_3, 1, "hello");

t1.join();
t2.join();
t3.join();

實驗的時候還發現一個問題,若是將重載的函數做爲線程的入口函數,會發生編譯錯誤!編譯器搞不清楚是哪一個函數,以下面的代碼:編程

// 普通函數 無參
void function_1() {
}

// 普通函數 1個參數
void function_1(int i) {
}
std::thread t1(function_1);
t1.join();
// 編譯錯誤
/*
C:\Users\Administrator\Documents\untitled\main.cpp:39: 
error: no matching function for call to 'std::thread::thread(<unresolved overloaded function type>)'
     std::thread t1(function_1);
                              ^
*/

仿函數

// 仿函數
class Fctor {
public:
    // 具備一個參數
    void operator() () {

    }
};
Fctor f;
std::thread t1(f);  
// std::thread t2(Fctor()); // 編譯錯誤 
std::thread t3((Fctor())); // ok
std::thread t4{Fctor()}; // ok

一個仿函數類生成的對象,使用起來就像一個函數同樣,好比上面的對象f,當使用f()時就調用operator()運算符。因此也可讓它成爲線程類的第一個參數,若是這個仿函數有參數,一樣的能夠寫在線程類的後幾個參數上。併發

t2之因此編譯錯誤,是由於編譯器並無將Fctor()解釋爲一個臨時對象,而是將其解釋爲一個函數聲明,編譯器認爲你聲明瞭一個函數,這個函數不接受參數,同時返回一個Factor對象。解決辦法就是在Factor()外包一層小括號(),或者在調用std::thread的構造函數時使用{},這是c++11中的新的贊成初始化語法。函數

可是,若是重載的operator()運算符有參數,就不會發生上面的錯誤。線程

匿名函數

std::thread t1([](){
    std::cout << "hello" << std::endl;
});

std::thread t2([](std::string m){
    std::cout << "hello " << m << std::endl;
}, "world");

std::function

class A{
public:
    void func1(){
    }

    void func2(int i){
    }
    void func3(int i, int j){
    }
};

A a;
std::function<void(void)> f1 = std::bind(&A::func1, &a);
std::function<void(void)> f2 = std::bind(&A::func2, &a, 1);
std::function<void(int)> f3 = std::bind(&A::func2, &a, std::placeholders::_1);
std::function<void(int)> f4 = std::bind(&A::func3, &a, 1, std::placeholders::_1);
std::function<void(int, int)> f5 = std::bind(&A::func3, &a, std::placeholders::_1, std::placeholders::_2);

std::thread t1(f1);
std::thread t2(f2);
std::thread t3(f3, 1);
std::thread t4(f4, 1);
std::thread t5(f5, 1, 2);

傳值仍是引用

先提出一個問題:若是線程入口函數的的參數是引用類型,在線程內部修改該變量,主線程的變量會改變嗎?指針

代碼以下:c++11

#include <iostream>
#include <thread>
#include <string>

// 仿函數
class Fctor {
public:
    // 具備一個參數 是引用
    void operator() (std::string& msg) {
        msg = "wolrd";
    }
};



int main() {
    Fctor f;
    std::string m = "hello";
    std::thread t1(f, m);

    t1.join();
    std::cout << m << std::endl;
    return 0;
}

// vs下: 最終是:"hello"
// g++編譯器: 編譯報錯

事實上,該代碼使用g++編譯會報錯,而使用vs2015並不會報錯,可是子線程並無成功改變外面的變量mcode

我是這麼認爲的:std::thread類,內部也有若干個變量,當使用構造函數建立對象的時候,是將參數先賦值給這些變量,因此這些變量只是個副本,而後在線程啓動並調用線程入口函數時,傳遞的參數只是這些副本,因此內部怎麼操做都是改變副本,而不影響外面的變量。g++多是比較嚴格,這種寫法可能會致使程序發生嚴重的錯誤,索性禁止了。對象

而若是能夠想真正傳引用,能夠在調用線程類構造函數的時候,用std::ref()包裝一下。以下面修改後的代碼:

std::thread t1(f, std::ref(m));

而後vsg++均可以成功編譯,並且子線程能夠修改外部變量的值。

固然這樣並很差,多個線程同時修改同一個變量,會發生數據競爭。

同理,構造函數的第一個參數是可調用對象,默認狀況下其實傳遞的仍是一個副本。

#include <iostream>
#include <thread>
#include <string>

class A {
public:
    void f(int x, char c) {}
    int g(double x) {return 0;}
    int operator()(int N) {return 0;}
};

void foo(int x) {}

int main() {
    A a;
    std::thread t1(a, 6); // 1. 調用的是 copy_of_a()
    std::thread t2(std::ref(a), 6); // 2. a()
    std::thread t3(A(), 6); // 3. 調用的是 臨時對象 temp_a()
    std::thread t4(&A::f, a, 8, 'w'); // 4. 調用的是 copy_of_a.f()
    std::thread t5(&A::f, &a, 8, 'w'); //5.  調用的是 a.f()
    std::thread t6(std::move(a), 6); // 6. 調用的是 a.f(), a不可以再被使用了
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    t6.join();
    return 0;
}

對於線程t1來講,內部調用的線程函數實際上是一個副本,因此若是在函數內部修改了類成員,並不會影響到外面的對象。只有傳遞引用的時候纔會修改。因此在這個時候就必須想清楚,究竟是傳值仍是傳引用!

線程對象只能移動不可複製

線程對象之間是不能複製的,只能移動,移動的意思是,將線程的全部權在std::thread實例間進行轉移。

void some_function();
void some_other_function();
std::thread t1(some_function);
// std::thread t2 = t1; // 編譯錯誤
std::thread t2 = std::move(t1); //只能移動 t1內部已經沒有線程了
t1 = std::thread(some_other_function); // 臨時對象賦值 默認就是移動操做
std::thread t3;
t3 = std::move(t2); // t2內部已經沒有線程了
t1 = std::move(t3); // 程序將會終止,由於t1內部已經有一個線程在管理了

參考

  1. C++併發編程實戰
  2. C++ Threading #8: Using Callable Objects
相關文章
相關標籤/搜索