17.1 C++并发与多线程-基础概念与实现
17.2 C++并发与多线程-线程启动、结束与创建线程写法
17.3 C++并发与多线程-线程传参详解、detach坑与成员函数作为线程函数
17.4 C++并发与多线程-创建多个线程、数据共享问题分析与案例代码
17.5 C++并发与多线程-互斥量的概念、用法、死锁演示与解决详解
17.6 C++并发与多线程-unique_lock详解
17.7 C++并发与多线程-单例设计模式共享数据分析、解决与call_once
17.8 C++并发与多线程-condition_variable、wait、notify_one与notify_all
17.9 C++并发与多线程-async、future、packaged_task与promise
17.10 C++并发与多线程-future其他成员函数、shared_future与atomic
17.11 C++并发与多线程-Windows临界区与其他各种mutex互斥量
17.12 C++并发与多线程-补充知识、线程池浅谈、数量谈与总结
文章目录
2.线程启动、结束与创建线程写法
2.1 范例演示线程运行的开始和结束
void myprint()
{
cout << "我的线程开始执行了" << endl;
//...
//Sleep(1000); //休息1000毫秒(1秒)
cout << "我的线程执行完毕了" << endl;
return;
}
{
std::thread mytobj(myprint); //这就是创建线程的代码 ,显然这是个线程对象,然后给的参数是个函数名,代表这个线程是从myprint这个函数(初始函数)开始运行
mytobj.join(); //join会卡在这里,等待myprint线程执行完毕,程序流程才会继续往下走
cout << "main主函数执行结束!" << endl; //这行由主线程执行,主线程从main返回,则整个进程执行完毕
}
观察这个结果,仔细查看输出的信息顺序:先执行所创建线程对应的函数,然后执行main主函数中的cout语句输出“main主函数执行结束!”,最后整个程序结束。
(1)thread
thread是C++标准库里面的类,这个类就是用来创建线程的。可以看到,用这个类生成一个对象,名字为mytobj,里面是一个可调用对象(此处的可调用对象是函数myprint)作为thread构造函数的实参来构造这个thread对象。
(2)join
从字面翻译来看,join的意思是“加入/汇合”。换句话说,就是“阻塞”的意思——主线程等待子线程执行完毕,执行流程最终汇合到一起(子线程执行完毕,执行流程回归主线程并执行完main主函数)。
所以,join成员函数的功能是:用来等待myprint函数(线程的入口函数,也就是代表自己创建的这个线程)运行完成。一旦执行了join这行代码,主线程就阻塞到这一行,等待mytobj对象所代表的线程执行完毕,也就是等待myprint函数执行完毕。
如果把mytobj.join();代码行注释掉,那么程序运行起来会报异常,而且程序的输出结果也是乱序的,例如可能是如下的输出
通过这个乱序结果不难发现,还没等子线程执行完毕,主线程先执行完毕了。这问题就来了,请想一想:子线程正在执行中(没执行完),主线程执行完了,这会导致整个进程退出了。这样的程序代码是不稳定、不合格的,编写这样代码的程序员也是不称职的。
所以,一个书写良好的程序,应该是主线程等待子线程执行完毕后,自己才能最终退出。这就是上面这条join语句的必要性。现在把join代码行的注释取消,再次看看结果。如下的结果顺序才是正确的:
(3)detach
detach是“分离”的意思。所谓分离,就是主线程不和子线程汇合了,主线程执行主线程的,子线程执行子线程的,主线程不必等子线程运行结束,可以先执行结束,这并不影响子线程的执行。
范例改造一下
void myprint()
{
cout << "我的线程执行完毕了1" << endl;
cout << "我的线程执行完毕了2" << endl;
cout << "我的线程执行完毕了3" << endl;
cout << "我的线程执行完毕了4" << endl;
cout << "我的线程执行完毕了5" << endl;
cout << "我的线程执行完毕了6" << endl;
cout << "我的线程执行完毕了7" << endl;
cout << "我的线程执行完毕了8" << endl;
cout << "我的线程执行完毕了9" << endl;
cout << "我的线程执行完毕了10" << endl;
cout << "我的线程执行完毕了11" << endl;
cout << "我的线程执行完毕了12" << endl;
return;
}
{
thread mytobj(myprint); //创建一个线程,也可以称为创建一个子线程
mytobj.detach();
cout << "main主函数执行结束!" << endl;
}
这里多次执行,看一看结果,其实每次结果可能都有差别,不一样:
可以看到,有时候看不到线程myprint输出的任何结果,有时候能看到myprint输出了多行结果,然后,因为主线程执行完毕,可执行程序(进程)退出执行,所以,myprint显示的结果也中断了。
针对一个线程,一旦调用了detach,就不可以再调用join了,否则会导致程序运行异常。
detach会导致程序员失去对线程的控制。所以在多数实际项目中,join更为常用,因为毕竟多数情况下程序员需要控制线程的生命周期,而创建一个线程并扔到后台不管的情况比较少。
(4)joinable。
判断是否可以成功使用join或者detach。
如果调用了join或者detach,那么joinable会变成false;否则,joinable返回的是true。
{
thread mytobj(myprint);
if (mytobj.joinable())
{
cout << "1:joinable() == true" << endl; //成立
}
else
{
cout << "1:joinable() == false" << endl;
}
mytobj.join(); //无论这里调用join()还是detach(),后续的joinable()都会返回false
if (mytobj.joinable())
{
cout << "2:joinable() == true" << endl;
}
else
{
cout << "2:joinable() == false" << endl; //成立
}
}
所以,joinable有一定的用处——判断针对某个线程是否调用过join或者detach:
if(mytobj.joinable())
{
mytobj.join();
}
2.2 其他创建线程的写法
(1)用类来创建线程。
class TA
{
public:
void operator()() //不带参数
{
cout << " TA::operator()开始执行了" << endl;
//....
cout << " TA::operator()执行结束了" << endl;
}
};
//在main主函数中,加入如下代码
{
TA ta;
thread mytobj3(ta); //ta,可调用对象,ta:这里不可以是临时对象thread mytobj3(TA()); 否则编译无法通过
mytobj3.join(); //为保证等待线程执行结束,这里使用join.
cout << "main主函数执行结束!" << endl;
}
执行起来,结果一切正常。另外,类与detach结合使用可能会带来意外问题。
继续修改TA类:
class TA
{
public:
TA(int& i) :m_i(i) {}
void operator()()
{
cout << "mi1的值为:" << m_i << endl; //隐患,m_i可能没有有效值
cout << "mi2的值为:" << m_i << endl;
cout << "mi3的值为:" << m_i << endl;
cout << "mi4的值为:" << m_i << endl;
cout << "mi5的值为:" << m_i << endl;
cout << "mi6的值为:" << m_i << endl;
}
int& m_i; //引入一个引用类型的成员变量
};
//在main主函数中,加入如下代码
{
int myi = 6;
TA ta(myi);
thread mytobj3(ta); //创建并执行子线程
mytobj3.join();
//mytobj3.detach();
cout << "main主函数执行结束!" << endl;
}
请注意,在类TA中,成员变量m_i是一个引用,绑定的是main主函数中的myi变量。所以,当主线程执行结束,很可能子线程在后台在继续运行,但是主线程结束时,myi会被销毁,子线程仍旧使用已经销毁的myi,产生不可预料的后果。这里读者可以通过打印类TA中m_i的地址来确定和main主函数中的myi地址相同来证明类TA中的成员变量m_i绑定的是main主函数中的myi。
当然,还有个疑问:一旦在main主函数中detach,那么主线程执行结束后,main主函数中的ta对象会被销毁,那么,子线程中看起来正在使用这个ta对象,如果被主线程销毁,是否会出现问题呢?其实,ta对象是会被复制到子线程中。所以,虽然执行完主线程后,ta对象被销毁,但复制到子线程中的对象依旧存在,所以这不是问题。但是,这个对象中如果有引用或者指针,那就另当别论了,那就可能产生问题。
为了进一步演示,给TA类的构造函数增加一行输出语句,并增加public修饰的析构函数和拷贝构造函数:
{
public:
TA(int i) :m_i(i) {
printf("TA()构造函数执行,m_i=%d,this=%p\n", m_i, this);
}
~TA() {
printf("~TA()析构函数执行,m_i=%d,this=%p\n", m_i, this);
}
TA(const TA& ta) :m_i(ta.m_i) {
printf("TA()拷贝构造函数执行,m_i=%d,this=%p\n", m_i, this);
}
}
从结果不难看到,先释放复制到线程里面去的ta对象(注意this值),因为main中的代码一直在join行等待子线程执行完毕,子线程执行完,当然会先把子线程的对象释放(析构),然后最后一行释放的才是主线程的ta对象。请读者注意比较构造和析构函数输出结果中的this值,这样就能够正确地匹配构造和析构函数的输出结果行。
(2)用lambda表达式来创建线程。
{
auto mylamthread = [] {
cout << "我的线程开始执行了" << endl;
//...
cout << "我的线程执行完毕了" << endl;
};
thread mytobj4(mylamthread);
mytobj4.join();
cout << "main主函数执行结束!" << endl;
}