零、注意事项
- 程序运行起来,生成一个进程,该进程所属的主线程开始自动运行;当主线程从main()函数返回,则整个进程执行完毕。
- 主线程从main()开始执行,那么我们自己创建的线程,也需要从一个函数开始运行,一旦这个函数运行完毕,线程也结束运行。
- 整个进程是否执行完毕的标志是:主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了,此时如果其他子线程还没有执行完,也会被强行终止(此条有例外,以后会解释)。
一、创建线程的一般方法
首先要引入头文件#include
,C++11中管理线程的函数和类在该头文件中声明,其中包括std::thread
类。
创建线程的格式是std::thread mythread(function);
,意思是创建了一个名为mythread
的线程,并且线程mythread
开始执行。其中可调用对象名(即括号中的function
)作为第一个参数,第二个参数为该函数的第一个参数,如果该函数接收多个参数就依次写在后面。此时线程开始执行。
需要注意,线程类参数是一个可调用对象,C++中的可调用对象可以是函数、函数指针、lambda表达式、bind创建的对象或者重载了函数调用运算符的类对象。
当线程启动后,一定要在thread
对象销毁前,对线程运用join()
或者detach()
方法。这是两种线程阻塞的方法,两者的区别在于是否等待子线程执行结束:
- join():主线程等待子线程运行完毕,子线程运行完后,主线程才可继续运行。
- detach():主线程与子线程分离,即主线程不必等待子线程运行完毕,子线程也不必理会主线程是否运行完毕。即使主线程运行完毕,子线程也可在后台继续运行。
以下程序展示了子线程的创建及执行过程:
#include <iostream>
#include <thread>
using namespace std;
void test()
{
int i = 0;
for(; i < 15; i++)
cout << "test " << i << endl;
}
int main()
{
int i = 0;
//创建了线程,线程执行起点(入口)是test,并开始执行线程
thread mytobj(test);
//阻塞主线程并等待test执行完,当test执行完毕,join()就执行完毕,主线程继续往下执行
//join意为汇合,子线程和主线程汇合
mytobj.join();
//一般在多线程程序中,主线程要等待子线程执行完毕,然后主线程才能向下执行
for (; i < 10; i++)
cout << "main " << i << endl;
return 0;
}
程序输出的结果是可预料的:先是15个test(0-14),然后是10个main(0-9)。从程序中我们已经看到,join()
方法其实更像有等待的意思,而不是加入的意思。如果我们忘记使用join()
方法,编译虽然不会报错,但是执行过程中会报错,从而导致程序中止运行(abort)。
除此之外还有另外一种线程阻塞的方法:detach()
方法,它有分离的意思,主线程不再和与子线程汇合,不再等待子线程。此时子线程和主线程失去关联,驻留在后台,由C++运行时库接管。
对于上面的程序,注意以下三个问题:
-
如果我们把上面程序中的
mytobj.join();
改为mytobj.detach();
,那么输出结果将会是不可预料的,因为你无法预知CPU在哪个时刻切换到哪个线程执行,因此很可能会出现输出内容打印不全的问题。因此,我们初学时一般不用detach,它违背了传统多线程程序的规律。 -
如果主线程比子线程先结束,那子线程剩余未输出的内容将不会显示在控制台窗口里,因为控制台窗口受主线程控制,主线程结束后控制台的输出也就结束了。所以,每次输出结果都会不一样。
-
在创建线程时,如果
thread
类传入的参数含有引用或指针,则子线程中的数据依赖于主线程中的内存,主线程结束后会释放掉自身的内存空间,则子线程会出现错误。例如以下程序:
#include <iostream>
#include <thread>
using namespace std;
void print(int &x)
{
cout << "print" << x << endl;
}
int main()
{
int a = 10;
thread mythread(print, a);
mythread.join();
cout << "main" << endl;
return 0;
}
在VS2019环境中可以编译通过,但是运行时会发生错误。初步原因应该是main线程结束后释放了内存空间,使得print线程无法引用main线程的内存。解决方法:(1)去掉&后恢复正常;(2)将语句thread mythread(print, a);
修改为thread mythread(print,ref(a));
。关于ref函数的问题我以后再去研究研究。
二、判断线程是否可以join或detach
上面我们说了,detach()
方法使用后,子线程与主线程独立运行,互相不受影响。join()和detach()两者中只能调用其中一个且只能调用一次,如果子线程分离(detach)后,再想调用join,是不行的;相反地,如果子线程join了以后再想detach也是不行的,这两种情况程序运行的会报错。幸好C++11为我们提供了一种判断方法。
thread
类提供了joinable()
方法,用以判断某个线程是否可以join或detach。若可以使用其中一个,则返回true
;若都不可以使用,则返回false
。
下面为一个例子:
#include <iostream>
#include <thread>
using namespace std;
void test()
{
int i = 0;
for(; i < 15; i++)
cout << "test " << i << endl;
}
int main()
{
int i = 0;
thread mytobj(test);
mytobj.detach();
//joinable()判断是否可以成功使用join()或者detach()
//如果返回true,证明可以调用join()或者detach()
//如果返回false,证明调用过join()或者detach(),join()和detach()都不能再调用了
if (mytobj.joinable() == true)
cout << "join success" << endl;
else
cout << "join fail" << endl;
for (; i < 10; i++)
cout << "main " << i << endl;
return 0;
}
程序的输出结果会有join fail
。
三、创建线程的其他方法
1.类创建线程
使用该方法之前,首先需要重载括号运算符,否则在创建线程时圆括号不能被编译器解释。下面是一个例子:
#include <iostream>
#include <thread>
using namespace std;
class TA {
public:
int &i;
TA(int &mi) :i(mi) {
cout << "构造函数!" << this << endl;
}
TA(const TA &ta) :i(ta.i) {
cout << "拷贝构造函数!" << this <<endl;
}
~TA()
{
cout << "析构函数!" << endl;
}
void operator ()() const
{
for (int i = 0; i < 15; i++)
cout << "重载运算符" << i << endl;
}
};
int main()
{
int i = 6;
TA a(i);
//创建线程时会调用拷贝构造函数
thread mytobj(a);
mytobj.join();
for (int i = 0; i < 10; i++)
cout << "main " << i << endl;
return 0;
}
容易发现,主线程中的对象和子线程中的对象的地址是不一样的。
2.lambda(匿名函数)表达式创建线程
下面是一个例子:
auto lambdaThread = [] {
cout << "我的线程开始执行了" << endl;
//。。。
//。。。
cout << "我的线程开始执行了" << endl;
};
thread myThread(lambdaThread);
myThread.join();