【C++11 多线程】仔细地将参数传递给线程(三)

一、构造函数的参数

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

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

  • 函数指针
  • 重载了operator()运算符的类对象,即仿函数
  • lambda表达式(匿名函数)
  • std::function

二、函数指针

#include <iostream>
#include <thread>

// 普通函数 无参
void function_1() {
}

// 普通函数 1个参数
void function_2(int i) {
}

// 普通函数 2个参数
void function_3(int i, std::string m) {
}

int main() {
	std::thread t1(function_1);
	std::thread t2(function_2, 1);
	std::thread t3(function_3, 1, "hello");
	t1.join();
	t2.join();
	t3.join();

	return 0;
}

实验的时候还发现一个问题,如果将重载的函数作为线程的入口函数,会发生编译错误!编译器搞不清楚是哪个函数,如下面的代码:

#include <iostream>
#include <thread>

// 普通函数 无参
void function_1() {
}

// 普通函数 1个参数
void function_1(int i) {
}

int main() {	
	// 编译错误: error C2665 : “std::thread::thread” : 3 个重载中没有一个可以转换所有参数类型
	//std::thread t1(function_1);
	//t1.join();

	return 0;
}

注意不要使用这种错误用法:std::thread t1(function_1(5));,函数参数要在后面用,隔开。


三、仿函数

#include <iostream>
#include <thread>

// 仿函数
class Fctor {
public:
	// 具有一个参数
	void operator() () {

	}
};

int main() {	
	Fctor f;
	std::thread t1(f);
	// std::thread t2(Fctor()); // 编译错误 
	std::thread t3((Fctor())); // ok
	std::thread t4{ Fctor() }; // ok

	t1.join();
	t3.join();
	t4.join();

	return 0;
}

一个仿函数类生成的对象,使用起来就像一个函数一样,比如上面的对象f,当使用f()时就调用operator()运算符。所以也可以让它成为线程类的第一个参数,如果这个仿函数有参数,同样的可以写在线程类的后几个参数上。

t2之所以编译错误,是因为编译器并没有将Fctor()解释为一个临时对象,而是将其解释为一个函数声明,编译器认为你声明了一个函数,这个函数不接受参数,同时返回一个Factor对象。解决办法就是在Factor()外包一层小括号(),或者在调用std::thread的构造函数时使用{},这是c++11中的新的列表初始化语法。

但是,如果重载的operator()运算符有参数,就不会发生上面的错误。


四、匿名函数

#include <iostream>
#include <thread>

int main() {	
	std::thread t1([]() {std::cout << "hello "; });
	t1.join();

	std::thread t2([](std::string str) {std::cout << str << std::endl; }, "world");
	t2.join();

	return 0;
}

五、类成员函数

#include <iostream>
#include <thread>
#include <functional>

// 类
class TestClass {
public:
    void func1() {
    }

    void func2(int i) {
        std::cout << "fun2: " << i << std::endl;
    }

    void func3(int i, int j) {
        std::cout << "fun3: " << i+j << std::endl;
    }
};

int main() {
    // 使用std::function存储类成员函数
    TestClass classObj;
    std::function<void()> f1 = std::bind(&TestClass::func1, classObj);
    std::function<void(int)> f2 = std::bind(&TestClass::func2, classObj, std::placeholders::_1);
    std::function<void(int, int)> f3 = std::bind(&TestClass::func3, classObj, std::placeholders::_1, std::placeholders::_2);

    // 构建线程
    std::thread t1(f1);
    std::thread t2(f2, 1);
    std::thread t3(f3, 1, 2);
    t1.join();
    t2.join();
    t3.join();

	return 0;
}

六、传值还是引用

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

代码如下:

#include <iostream>
#include <thread>

// 仿函数
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;
}

VS2019下: 编译报错,需要修改std::string& msgstd::string msg

我是这么认为的:std::thread类,内部也有若干个变量,当使用构造函数创建对象的时候,是将参数先赋值给这些变量,所以这些变量只是个副本,然后在线程启动并调用线程入口函数时,传递的参数只是这些副本,所以内部怎么操作都是改变副本,而不影响外面的变量。VS2019 可能是比较严格,这种写法可能会导致程序发生严重的错误,索性禁止了。

如果可以想不报错,真正传引用,可以在调用线程类构造函数的时候,用std::ref()包装一下。如下面修改后的代码:

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

然后就可以成功编译,而且子线程可以修改外部变量的值,打印的是world。当然这样并不好,子线程和主线程同时修改同一个变量,会发生数据竞争。同理,构造函数的第一个参数是可调用对象,默认情况下其实传递的还是一个副本。


七、线程对象只能移动不可复制

线程对象之间是不能复制的,只能移动,移动的意思是,将线程的所有权在std::thread实例间进行转移。

#include <iostream>
#include <thread>

void some_function() {}
void some_other_function() {}

int main() {
    std::thread t1(some_function);
    // std::thread t2 = t1; // 编译错误
    std::thread t2 = std::move(t1); // 只能移动 t1内部已经没有线程了
    t1 = std::thread(some_other_function); // 临时对象赋值 默认就是移动操作

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

    return 0;
}

posted @ 2021-03-25 11:15  fengMisaka  阅读(2365)  评论(1编辑  收藏  举报