5

构造函数的参数

std::thread 类的构造函数是使用 可变参数模板 实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口 函数 ,而后面的若干个参数是该函数的 参数

第一参数的类型并不是 c 语言中的函数指针( c 语言传递函数都是使用函数指针),在 c++11 中,增加了 可调用对象(Callable Objects) 的概念,总的来说,可调用对象可以是以下几种情况:

  • 重载了 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){
    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);

    传值还是引用

    先提出一个问题:如果线程入口函数的的参数是引用类型,在线程内部修改该变量,主线程的变量会改变吗?

    代码如下:

    #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并不会报错,但是子线程并没有成功改变外面的变量m

    我是这么认为的: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() {
        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内部已经有一个线程在管理了