前话
还记得上一节中main线程与子线程共同打印数据时候的那个运行结果吗。
你一定会很奇怪这个结果,我分明写了回车符的打印为什么有的语句没有打印回车符呢?这就涉及到操作系统在运行多线程时候的机制问题。
多线程的运行机制
在多线程程序运行的时候并不会等待一个主线程的语句执行完再执行子线程的语句,相反也不会等待一个子线程的语句执行完再执行主线程的语句,而是二者抢占式执行,谁抢到执行的权限谁就先执行,也就是说可能主线程的某个语句刚执行到一半,权限就被子线程抢去了,那么就执行子线程的语句,然后二者继续抢占式执行。所以就会产生上图所示的结果
正是因为这个机制所以导致了多线程在传输数据时的安全问题。
std::thread::join()
来看下面的代码:
#include
#include
void testThread(int a) {
while (true) {
std::cout << "传入的数据是:" << a << std::endl;
}
}
int main() {
int a = 0;
std::thread myThread(testThread, a);
myThread.join();
return 0;
}
完全没有问题,因为以值传入本身就是非常安全的。
让人头疼的时候开始了,当你兴致勃勃的改为以引用传入时,编译器毫不犹豫的报错了。
#include
#include
void testThread(int &a) {
while (true) {
std::cout << "传入的数据是:" << a << std::endl;
}
}
int main() {
int a = 0;
std::thread myThread(testThread, a);
myThread.join();
return 0;
}
报错如下:
C2893 未能使函数模板“unknown-type std::invoke(_Callable &&,_Types &&...) noexcept()”专用化
很费解的错误,但是当你看见“&&”的时候,你一定会想起右值引用,没错,问题就出在右值引用上面,来看看std::thread的源码吧。
template>, thread>>>
explicit thread(_Fn&& _Fx, _Args&&... _Ax)
{ // construct with _Fx(_Ax...)
_Launch(&_Thr,
_STD make_unique, decay_t<_Args>...> >(
_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...));
}
很明显,std::thread的构造函数传入的是一个函数的右值引用和一个右值引用的模板参数包,而我们的testThread函数的参数是一个左值引用,所以在左值引用向右值引用绑定的时候,编译器会报出错误,因为这种情况是不允许的,那么怎么让左值引用绑定到右值引用上面呢,很简单使用const关键字。更改代码如下:
#include
#include
void testThread(const int &a) { //在左值引用前加上const关键字,使得其可以绑定到右值引用
while (true) {
std::cout << "传入的数据是:" << a << std::endl;
}
}
int main() {
int a = 0;
std::thread myThread(testThread, a);
myThread.join();
return 0;
}
但是使用了const之后传入的值在子线程中将无法更改,那么可能会得不到预期的结果,这时候std::ref就排上用场了,由于std::thread和std::bind一样,它无法得知你传入的数据是否有效,所以它规定了你只可以按值传递,但是std::ref可以打破这个规则,它强制使数据按引用传递。
#include
#include
void testThread(int &a) {
for (int i = 0; i < 10; i++) {
std::cout << "传入的数据是:" << a << std::endl;
}
a = 5; //在这里修改a的值
}
int main() {
int a = 6;
std::thread myThread(testThread, std::ref(a)); //使用std::ref强制使参数按引用传入
myThread.join();
std::cout << "a最后等于:" << a << std::endl; //测试a的值是否被改变
return 0;
}
当然,对于一个const类型的对象,你要想改变它的成员变量,可以将其成员变量声明为mutable.
#include
#include
class A {
public:
mutable int num;
};
void testThread(const A &a) {
for (int i = 0; i < 10; i++) {
std::cout << "传入的数据是:" << a.num << std::endl;
}
a.num = 5;
std::cout << "a在子线程中被修改后为:" << a.num << std::endl;
}
int main() {
A a;
a.num = 6;
std::thread myThread(testThread, a); //使用std::ref强制使参数按引用传入
myThread.join();
std::cout << "a最后等于:" << a.num << std::endl; //测试a的值是否被改变
return 0;
}
最后的结果令人很惊讶,为什么在子线程中是被修改成功的,但是回到主线程又变回去了呢?不是按引用传递的吗?其实造成这个结果的原因其实是因为std::thread传入的并不是你声明的类的引用,即使你声明的是引用类型。它在内部会拷贝一个对象,并将拷贝的对象传入,所以子线程中操作的其实是传入对象的拷贝,来看下面的代码。
#include
#include
class A {
public:
mutable int num;
A() {
std::cout << "构造函数A()执行了" << std::endl;
}
A(const A& a) : num(a.num) {
std::cout << "拷贝构造A(const A& a)执行了" << std::endl;
}
~A() {
std::cout << "析构函数~A()执行了" << std::endl;
}
};
void testThread(const A &a) {
std::cout << "传入的数据是:" << a.num << std::endl;
a.num = 5;
std::cout << "a在子线程中被修改后为:" << a.num << std::endl;
}
int main() {
A a;
a.num = 6;
std::thread myThread(testThread, a);
myThread.join();
std::cout << "a最后等于:" << a.num << std::endl;
return 0;
}
那么怎么修改代码使其可以以引用传入呢,前面说过使用std::ref,这时你就可以去除const和mutable了。修改后如下:
#include
#include
class A {
public:
mutable int num;
A() {
std::cout << "构造函数A()执行了" << std::endl;
}
A(const A& a) : num(a.num) {
std::cout << "拷贝构造A(const A& a)执行了" << std::endl;
}
~A() {
std::cout << "析构函数~A()执行了" << std::endl;
}
};
void testThread(A &a) {
std::cout << "传入的数据是:" << a.num << std::endl;
a.num = 5;
std::cout << "a在子线程中被修改后为:" << a.num << std::endl;
}
int main() {
A a;
a.num = 6;
std::thread myThread(testThread, std::ref(a));
myThread.join();
std::cout << "a最后等于:" << a.num << std::endl;
return 0;
}
运行之后,少了一次拷贝构造并且成员变量被成功修改。
std::thread::detach()
以上都是多线程中一些基本的常识,到了detach这里,事情就变得麻烦了起来。 因为detach会把子线程分离,也就是说,可能在数据还没有传送的时候,主线程就销毁了,就会导致子线程接收不到数据。