![a68d5dafb772148a5b3e061487edcf5a.png](https://i-blog.csdnimg.cn/blog_migrate/36d6127d174b92ca6e8b4929c7aa8118.png)
想要抢先看后面的章节?打赏本文10元,即可获得带插图全本下载地址!打赏完成记得私信我哦 :p
12.3 用<thread>进行多线程开发
小时候,老师总是教育我们上课要专心,“一心不可二用”。可是CPU这个不听话的“熊孩子”偏偏却在一个芯片中加入了两个甚至多个运算核心,想要一“芯”二用。从硬件厂商的角度,通过增加CPU的运算核心,突破了原来单核CPU的频率极限,确实可以很大程度上增加CPU的总频率。在他们看来,这简直就是一个天才的创意。可是从软件厂商的角度,CPU运算核心的增加,并没有显著地提高软件的性能表现,有时候甚至会降低软件的性能。在他们看来,这无疑是一场噩梦的开始。
在以往的计算机发展历史中,硬件技术的发展,特别是CPU频率的不断提升,总是同时会提升软件的性能。更重要的是,这种性能的提升是完全免费的,软件什么都不需要做,只要换上新出来的更高频率的CPU,软件就获得更好的性能表现。从286到486,从奔腾到酷睿,每次CPU频率的提升,无不给软件性能带来大幅提升。如果有用户抱怨我们的软件性能不佳,我们也无须着急,只需要坐等Intel或者AMD推出更高频率的CPU并让用户换上就可以了。
但是,当CPU的发展进入多核时代以后,程序员们沮丧地发现,CPU长期提供的这份“免费的午餐”消失了。这是因为当前大多数程序几乎都是针对一个运算核心的CPU而设计的单线程程序。虽然CPU有多个运算核心,但它却只能在其中的某一个运算核心上进行运算,而其他的运算核心并没有得到利用而白白浪费了。虽然CPU的运算核心增加了,总的频率增加了,但是单个运算核心的频率并没有太大变化,所以程序的性能并没有随着CPU运算核心的增加而得到显著的提升。
![45ff87fa8095657890719882769aeca2.png](https://i-blog.csdnimg.cn/blog_migrate/9d1fcdc31244e4671eefd2ccfd457747.jpeg)
图12-3 从单核CPU到多核CPU
浪费是可耻的,更何况我们浪费的是如此宝贵的CPU运算资源。为了把被浪费的CPU运算资源充分地利用起来,唯一的办法就是针对CPU的多个运算核心设计我们的程序,通过将原来由单个线程串行执行的程序并行化,用多线程代替原来的单线程,使其可以同时运行在多个运算核心上,以此来实现对CPU多个运算核心的充分利用。这样,程序的性能又会随着CPU频率的提高而得到提升。
可是,多线程程序的设计并不是一个轻松简单的活儿。在C++11之前,如果我们想要实现一个多线程程序,我们需要使用系统API创建线程,需要小心地维护对共享资源的访问等等。更折磨人的是,多线程增加了程序的设计与实现难度,如果设计错误,多线程不仅不会提升程序的性能,反而可能会降低性能,甚至引起资源互锁而导致程序失去响应。为了简化多线程程序的设计与实现,C++11的标准库专门提供了<thread>头文件以支持多线程程序的开发,而那份美味的“免费的午餐”也正在回到我们面前。
12.3.1 利用thread创建线程
C++11中的<thread>头文件提供了thread、mutex以及unique_lock等基本对象来对多线程开发中最常用的线程、互斥以及锁等基本概念进行抽象与表达,为多线程程序的实现提供了一个较低抽象层次的编程模型。其中,最基础也最重要的是与线程概念相对应的thread类。线程是对程序中的某个执行或者计算过程的一种表述,而所谓的多线程程序,就是将原来的一个执行过程分成多个过程去执行。由此可见,线程的创建,是实现多线程的基础。可是在以往,要想在程序中创建一个线程,我们不得不针对不同的操作系统调用不同的系统函数,然后还要提供复杂的参数才能完成线程的创建。幸运的是,thread类的出现,大大地简化了这一过程。
thread类对线程概念进行了很好的抽象与实现,从而使得我们可以非常简单地使用一个函数指针(也包括函数对象和Lambda表达式)来构建一个thread对象。而一旦拥有了thread对象,就意味着我们创建了一个线程,也就可以利用thread对象所提供的成员函数对这个线程进行调度,启动、挂起或者停止这个线程,以操作线程完成某个执行过程。例如:
#include <iostream>
// 引入定义thread类的头文件
#include <thread>
// 使用thread所在的名字空间
using namespace std;
// 定义需要线程执行的函数和函数对象
void ListenMusic()
{
cout<<"听音乐"<<endl;
}
struct ReadBook
{
void operator()()
{
cout<<"看书"<<endl;
}
};
int main()
{
// 使用函数对象创建线程对象readthread,并立即启动线程的执行
ReadBook read;
thread readthread(read);
// 使用函数指针创建线程对象listenthread,并立即启动线程的执行
thread listenthread(ListenMusic);
// 处理其他业务逻辑…
// 调用join()函数,等待分支线程执行完毕
readthread.join();
listenthread.join();
// 所有分支线程执行完毕,程序退出
return 0;
}
在这段代码中,我们利用函数对象类ReadBook的一个read函数对象和指向ListenMusic()函数的函数指针(也就是它的函数名)作为thread类构造函数的参数,分别创建了readthread和listenthread这两个thread对象。thread对象的创建,意味着它将创建新的线程,并开始执行作为构造函数参数传递给thread对象的函数对象或者是函数,通过简单的一个步骤,就完成了线程的创建与启动执行。在多线程环境下,我们将执行主函数并负责创建其它线程的线程称为主线程,而那些被创建的线程则相应地被称为分支线程或工作者线程,其执行的函数则被称为线程函数。在执行的时候,主线程开始进入主函数执行,通过创建两个thread对象而创建了两个分支线程并立即启动执行其线程函数,而与此同时,执行主函数的主线程将继续向下执行。这样,主线程和两个分支线程同时都在执行,操作系统会将它们调度到CPU的多个运算核心去执行,以此达到对CPU多个运算核心的充分利用。当主线程遇到thread对象调用的join()函数后,主线程将等待这个thread对象执行完毕之后,再继续往下执行,直到最后主函数执行完毕,退出整个程序。整个程序的执行流程如下图12-4所示:
![cd545f6334ab31ad3e04b7c7a5089725.png](https://i-blog.csdnimg.cn/blog_migrate/5abcf02bd2edac18b791546467b00c55.jpeg)
图12-4 多线程程序的执行流程
除了利用thread对象创建新的线程简单地执行某个线程函数之外,就像我们常常需要通过参数与普通函数进行数据传递一样,更多时候,我们也需要向线程函数内传入数据以供其进行处理,或者是从线程函数中传出结果数据。要做到这一点,我们同样需要给线程函数加上参数,跟普通函数类似,如果只是需要向线程函数内传入数据,那就加上传值形式的参数,而如果加上传指针和传引用形式的参数,则既可以传入也可以传出数据。与普通函数在调用时将实际参数复制给函数的形式参数所不同的是,线程函数的形式参数的赋值是在这个函数被用于创建thread对象时完成的。当我们在使用某个带有参数的线程函数创建thread对象时,在thread构造函数的实际参数中,我们不仅要用这个函数指针或者函数对象做第一个参数,同时其后还要依次加上线程函数所需要的各个实际参数。在创建thread对象的时候,这些实际参数会被拷贝复制给线程函数相应的形式参数,以此来实现数据的传递。这里需要注意的是,如果线程函数的参数是传引用形式,那么在创建thread对象的时候,我们需要使用ref()函数获得实际参数的引用才行,否则,即使这个参数是引用形式,它也会被拷贝复制而在线程函数和本地函数间形成两个副本,起不到传出数据的效果。例如,我们需要向上面的ListenMusic()线程函数传入歌曲名并从中传出结果数据:
// 需要传递数据的线程函数
// 传值形式的strSong负责向线程函数内传入数据
// 传引用形式的vecEar负责向线程函数外传出数据
void ListenMusic(string strSong,vector<string>& vecEar)
{
cout<<"我正在听"<<strSong<<endl; // 使用外部数据
vecEar.push_back(strSong); // 将结果数据存入外部容器
}
// …
int main()
{
// 准备需要传递的数据
vector<string> vecEar; // 用于保存结果数据的容器
string strSong = "歌唱祖国"; // 传入线程函数的数据
// 在创建thread对象时传递数据
// 第一个参数是线程函数指针,其后依次是线程函数所需要的参数
thread listenthread(ListenMusic,strSong,ref(vecEar));
// …
listenthread.join();
// 输出结果数据
for(string strName : vecEar)
cout<<strName<<endl;
return 0;
}
利用thread对象,我们可以像调用普通函数一样简单地用thread对象创建另外一个线程来执行我们的线程函数,也可以像与普通函数传递数据一样简单地与线程函数传递数据,从此,一边听着歌还可以一边看着书,轻松开启我们惬意的“一芯二用”的并行生活。
知道更多:线程中的瞌睡虫
在利用线程执行某个任务的时候,我们往往要对线程的执行时间进行控制,让线程在等待一定时间之后再继续执行,或者是在某个事先设定的固定时间点之后执行。这时,我们就需要用到std::this_thread名字空间下的sleep_for()函数和sleep_until()函数来完成对线程执行状态的时间控制了。
sleep_for()函数可以让当前线程(也就是调用这个函数的线程)暂停执行一段时间,等过了这段时间之后再继续恢复执行;而sleep_until()函数则是让当前线程一直暂停,直到某个固定时间点的到来才会继续恢复执行。它们就像两条瞌睡虫,一条可以让线程瞌睡一整天(固定时间段),而另一条更厉害,可以让线程一直瞌睡到天明(固定时间点)。对于我们来说,瞌睡虫很是讨厌,可是对于线程来说,瞌睡虫却是大有用处。
比如,我们想要模拟一下传说中的2012世界末日,就需要这两条瞌睡虫来让线程瞌睡瞌睡:
#include <iostream>
#include <thread> // 引入线程相关的头文件
#include <chrono> // 引入时间相关的头文件
using namespace std;
using namespace std::chrono; // 使用时间相关的名字空间
int main()
{
// 构造一个固定时间点:2012年 12月21日零时
tm timeinfo = tm();
timeinfo.tm_year = 112; // 年: 2012 = 1900 + 112
timeinfo.tm_mon = 11; // 月:12 = 1 + 11
timeinfo.tm_mday = 21; // 21日
time_t tt = mktime(&timeinfo);
// 利用time_t类型的变量tt创建一个表示世界末日固定时间点的time_point对象tp
system_clock::time_point tp = system_clock::from_time_t (tt);
// 当前线程一直瞌睡到tp表示的2012年12月21日零时
this_thread::sleep_until(tp);
// 世界末日到了,程序继续恢复执行,响铃10次发出警报
for(int i = 0; i < 10; ++i)
{
cout<<'a'; // 输出一个计算机响铃
// 当前线程瞌睡一秒钟,然后继续恢复执行下一次循环
this_thread::sleep_for(seconds(1));
}
return 0;
}
在两条瞌睡虫的合作下,我们的模拟程序会首先在sleep_until()的作用下,瞌睡(暂停执行)到tp所表示的固定时间点(2012年12月21日零时),等到了这个时间点后,程序才会恢复执行。紧接着,程序执行进入一个for循环,每次循环,它都会输出一个计算机响铃,然后又会在sleep_for()这条瞌睡虫的作用下,休眠一秒钟,然后再继续执行下一次循环。整个程序的效果就是到了世界末日,这个程序会发出滴滴滴的警报,告诉我们,世界末日来了,赶紧逃命吧!可是,可是,2012早都过去了,虽然世界末日没来,可这个程序却忠实地准时发出了警报。由此可见,玛雅人忽悠人,还是C++程序更可信。