1.4 开始入门

ok!现在你有一个能与C++11标准兼容的编译器。接下来呢?一个C++多线程程序是什么样子呢?其实,它看上去和其他C++程序差不多,通常是变量、类以及函数的组合。唯一的区别在于某些函数可以并发运行,所以需要确保共享数据在并发访问时是安全的,详见第3章。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。

1.4.1 你好,并发世界

从一个经典的例子开始:一个打印“Hello World.”的程序。一个非常简单的在单线程中运行的Hello World程序如下所示,当我们谈到多线程时,它可以作为一个基准。

  1. #include <iostream>
  2. int main()
  3. {
  4. std::cout << "Hello World\n";
  5. }

这个程序所做的就是将“Hello World”写进标准输出流。让我们将它与下面清单所示的简单的“Hello, Concurrent World”程序做个比较,它启动了一个独立的线程来显示这个信息。

清单 1.1 一个简单的Hello, Concurrent World程序:

  1. #include <iostream>
  2. #include <thread> //①
  3. void hello() //②
  4. {
  5. std::cout << "Hello Concurrent World\n";
  6. }
  7. int main()
  8. {
  9. std::thread t(hello); //③
  10. t.join(); //④
  11. }

第一个区别是增加了#include <thread>①,标准C++库中对多线程支持的声明在新的头文件中:管理线程的函数和类在<thread>中声明,而保护共享数据的函数和类在其他头文件中声明。

其次,打印信息的代码被移动到了一个独立的函数中②。因为每个线程都必须具有一个初始函数(initial function),新线程的执行从这里开始。对于应用程序来说,初始线程是main(),但是对于其他线程,可以在std::thread对象的构造函数中指定——本例中,被命名为t③的std::thread对象拥有新函数hello()作为其初始函数。

下一个区别:与直接写入标准输出或是从main()调用hello()不同,该程序启动了一个全新的线程来实现,将线程数量一分为二——初始线程始于main(),而新线程始于hello()。

新的线程启动之后③,初始线程继续执行。如果它不等待新线程结束,它就将自顾自地继续运行到main()的结束,从而结束程序——有可能发生在新线程运行之前。这就是为什么在④这里调用join()的原因——详见第2章,这会导致调用线程(在main()中)等待与std::thread对象相关联的线程,即这个例子中的t。

这看起来仅仅为了将一条信息写入标准输出而做了大量的工作,确实如此——正如上文1.2.3节所描述的,一般来说并不值得为了如此简单的任务而使用多线程,尤其是在这期间初始线程并没做什么。本书后面的内容中,将通过实例来展示在哪些情景下使用多线程可以获得收益。