Cpp Concurrency In Action 读书笔记 第一章

时间:2022-12-28 23:07:23

并发种类


多进程并发


将应用程序分为多个独立的进程,它们在同一时刻运行。独立的进程可以通过进程间常规的通信渠道传递讯息(信号、套接字、文件、管道等等)。

缺点:
1、进程之间的通信通常不是设置复杂,就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以避免一个进程去修改另一个进程的数据。
2、运行多个进程所需的固定开销:需要时间启动进程,操作系统需要内部资源来管理进程等等。


优点:
1、操作系统在进程间提供的附加保护操作和更高级别的通信机制,意味着可以更容易编写安全(safe)的并发代码。
2、可以使用远程连接(可能需要联网)的方式,在不同的机器上运行独立的进程。虽然,这增加了通信成本,但在设计精良的系统上,这可能是一个提高并行可用行和性能的低成本方式。


多线程并发


线程很像轻量级的进程:每个线程相互独立运行,且线程可以在不同的指令序列中运行。进程中的所有线程都共享地址空间,并且所有线程访问到大部分数据———全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。虽然,进程之间通常共享内存,但这种共享通常也是难以建立,且难以管理。因为,同一数据的内存地址在不同的进程中是不相同。

缺点:
1、共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么程序员必须确保每个线程所访问到的数据是一致的。

优点:
1、地址空间共享,以及缺少线程间数据的保护,使得操作系统的记录工作量减小,所以使用多线程相关的开销远远小于使用多个进程。
2、多个单线程/进程间的通信(包含启动)要比单一进程中的多线程间的通信(包括启动)的开销大,若不考虑共享内存可能会带来的问题,多线程将会成为主流语言(包括C++)更青睐的并发途径。

本书只关注使用多线程的并发。

Cpp Concurrency In Action 读书笔记 第一章                                                                       Cpp Concurrency In Action 读书笔记 第一章
图 1.1 一对并发运行的进程之间的通信图                        1.2 同一进程中的一对并发运行的线程之间的通信

什么时候不应该使用并发?


基本上,不使用并发的唯一原因就是,收益比不上成本。使用并发的代码在很多情况下难以理解,因此编写和维护的多线程代码就会产生直接的脑力成本,同时额外的复杂性也可能引起更多的错误。

1:潜在的性能增益足够大或关注点分离地足够清晰,能抵消所需的额外的开发时间以及与维护多线程代码相关的额外成本(代码正确的前提下);否则,别用并发。

2:性能增益可能会小于预期;因为操作系统需要分配内核相关资源和堆栈空间,所以在启动线程时存在固有的开销,然后才能把新线程加入调度器中,所有这一切都需要时间。如果在线程上的任务完成得很快,那么任务实际执行的时间要比启动线程的时间小很多,这就会导致应用程序的整体性能还不如直接使用“产生线程”的方式。

3:线程是有限的资源。如果让太多的线程同时运行,则会消耗很多操作系统资源,从而使得操作系统整体上运行得更加缓慢。

4:运行越多的线程,操作系统就需要做越多的上下文切换,每个上下文切换都需要耗费本可以花在有价值工作上的时间。所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能。为此,如果你试图得到系统的最佳性能,可以考虑使用硬件并发(或不用),并调整运行线程的数量。

第一个并发示例


#include <iostream>
#include <thread>  //①
void hello()  //②
{
  std::cout << "Hello Concurrent World\n";
}
int main()
{
  std::thread t(hello);  //③
  t.join();  //④
}

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

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

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

新的线程启动之后③,初始线程继续执行。如果它不等待新线程结束,它就将自顾自地继续运行到main()的结束,从而结束程序。所以在④这里调用join(),这会导致调用线程(在main()中)等待与std::thread对象相关联的线程,即这个例子中的t