1.4 开始入门

OK!准备一个能与C++11/C++14/C++17标准兼容的编译器。C++多线程程序是什么样子呢?其实,和其他C++程序差不多。唯一的区别在于某些函数可以并发运行,所以需要确保共享数据在并发访问时是安全的。当然,为了并发地运行,必须使用特定函数以及对象来管理各个线程。

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> // 1
  3. void hello() // 2
  4. {
  5. std::cout << "Hello Concurrent World\n";
  6. }
  7. int main()
  8. {
  9. std::thread t(hello); // 3
  10. t.join(); // 4
  11. }

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

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

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

新的线程启动之后③,初始线程继续执行。如果它不等待新线程结束,就运行到main()函数结束——有可能发生在新线程运行之前。这就是为什么在④这里调用join()的原因——详见第2章,这会让创建线程等待std::thread对象创建的线程。

这看起来仅是为了将一条信息写入标准输出,确实如此——正如上文1.2.3节所描述的,一般来说并不值得为了如此简单的任务而使用多线程,尤其是在这期间初始线程并没做什么。后面的章节中,将通过更加复杂的实例来展示,在哪些情景下使用多线程更有意义。