C++并发编程实战—第一章

第一章

什么是并发

计算机系统中的并发

  • 并发:计算机术语中的"并发",指的是在单个系统里同时执行多个独立的活动,而不是顺序的一个接一个的执行。对于单核CPU来说,在某个时刻只可能处理一个任务,但它却不是完全执行完一个任务再执行一个下一任务,而是一直在任务间切换,每个任务完成一点就去执行下一个任务,看起来就像任务在并行发生,虽然不是严格的同时执行多个任务,但是我们仍然称之为并发(concurrency)。真正的并发是在在多核CPU上,能够真正的同时执行多个任务,称为硬件并发(hardware concurrency)。
  • 并发并非没有代价,在单核CPU并发执行两个任务需要付出上下文切换的时间代价。
    双核并发VS单核并发- 说明:系统从一个任务切换到另一个任务需要执行一次上下文切换,这是需要时间的(图中的灰色块)。上下文切换需要操作系统为当前运行的任务保存CPU的状态和指令指针,算出要切换到哪个任务,并为要切换的任务重新加载处理器状态。然后将新任务的指令和数据载入到缓存中。

并发的方法

  • 多进程并发
    使用并发的第一种方法,是将应用程序拆分为多个独立的单线程的进程,它们在同一时刻运行,就像同时运行网页浏览器和文字处理器一样。这些独立的进程可以通过常规的进程间通信(如管道、信号、消息队列、共享内存、存储映射I/O、信号量、套接字等)通道给彼此传递消息
    多进程并发
    缺点:
     进程间通信较为复杂,速度相对线程间的通信更慢。
     启动进程的开销比线程大,使用的系统资源也更多。
    优点:
     进程间通信的机制相对于线程更加安全。
     能够很容易的将一台机器上的多进程程序部署在不同的机器上(如果通信机制选取的是套接字的话)。
  • 多线程并发
    并发的另一个方法是在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独立,且每个线程可以运行不同的指令序列。但是,进程中的所有线程都共享相同的地址空间,并且大部分数据可以被所有线程直接访问一一全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。尽管在进程之间共享内存通常是可能的,但是这种机制比较复杂并且难于管理。因为同一数据的内存地址在不同的进程中可能不相同。
    多线程
    优点:
     由于可以共享数据,多线程间的通信开销比进程小的多。
     线程启动的比进程快,占用的资源更少。
    缺点:
     共享数据太过于灵活,为了维护正确的共享,代码写起来比较复杂。
     无法部署在分布式系统上。

并发与并行

对于多线程代码来说,这两个概念有很大部分是重叠的。实际上,很多时候它们的意思没有什么区别。其区别主要在于细微之处,关注点和意图方面。这两个术语都是关于使用硬件同时运行多个任务,不过并行更加面向性能。提到并行,主要关注使用可用硬件来提高批量数据处理的性能,然而谈到并发,关注的重点在于关注点分离( separation ofconcerns)或响应能力。这种区分并非一刀切,而且在含义上还是有很大的重叠,但了解这个区别有助于澄清讨论。本书中,这两方面的例子都有。

为什么要用并发

  • 主要目的:提高性能、任务拆分
  • 任务拆分:
      在编写软件的时候,将相关的代码放在一起,将无关的代码分开,能够让程序更加容易理解和测试。将程序划分成不同的任务,每个线程执行一个任务或者多个任务,可以将整个程序的逻辑变得更加简单。
      例子:考虑一个有用户界面的处理密集型应用,比如台式机上的DVD播放程序。这样的应用程序,应具备两种基本功能:不光要从磁盘中读出数据,解码图像和声音,然后把它们及时地发送到图形和声音硬件,从而实现DVD的无误播放,它还必须接受来自用户的输入,当用户点击“暂停”或“返回菜单”或“退出”按键的时候必须有所反应。当应用程序是单个线程时,应用需要在播放期间定期检查用户的输入,这需要把用户界面代码合并到DVD播放代码。如果使用多线程来分隔这些关注点,用户界面代码和DVD播放代码就不必交错在一起:一个线程可以处理用户界面,另一个进行DVD播放。它们之间会有交互,比如当用户点击暂停键时,但现在这些交互和手边的任务直接相关。
  • 提高性能:(两种方式:任务并行、数据并行)
     任务并行:将单个任务分成几部分,且各自并行运行,从而降低总运行时间。这就是任务并行(task parallelism)。虽然这听起来很直观,但它处理起来相当复杂,因为在各个部分之间可能存在大量的依赖。拆分根据处理来进行,一个线程执行算法的一部分,而另一个线程执行算法的另一个部分,就是任务并行。
     数据并行:拆分基于数据来进行一一每个线程在不同的数据部分上执行相同的操作。

什么时候不使用并发

不使用并发的唯一原因是:收益比不上成本
  1、使用并发的代码在很多情况下较难理解,因此编写和维护多线程代码会直接产生脑力成本,而额外的复杂性也可能会引发更多的 bug。除非潜在的性能增益足够大或任务分离的足够清晰,能抵消为确保代码逻辑正确所需的额外开发时间以及维护多线程代码相关的额外成本;否则,不要用并发。
  2、性能增益可能会小于预期;启动线程时存在固定开销,因为操作系统需要分配相关的内核资源和堆栈空间,然后把新线程添加给调度器,这都需要时间。如果在线程上的任务完成得很快,那么实际执行任务的时间要比启动线程的时间更短,这就会导致应用程序的整体性能还不如直接使用主线程(spawning thread)直接执行。
  3、线程是有限的资源。如果太多的线程同时运行,则会消耗很多操作系统资源,从而使得操作系统整体上运行得更慢。不仅如此,运行太多的线程也会耗光进程的可用内存或地址空间,因为每个线程都需要一个独立的堆栈空间。对于一个可用地址空间为4GB的32位处理器来说,这的确是个问题:如果每个线程都有一个1MB的堆栈(很多系统这么干),那么4096个线程将会用尽所有地址空间,不会给代码、静态数据或者堆数据留有任何空间。
  4、当客户端/服务器(C/S)应用在服务器端为每一个连接启动一个独立的线程,对于少量的连接是可以正常工作的,但当同样的技术用于需要处理大量连接的高需求服务器时,也会因为线程太多而耗光系统资源。在这种场景下,小心使用线程池可以优化性能。
  5、运行越多的线程,操作系统就需要越多的上下文切换,每次上下文切换都需要耗费本可以花在有用工作上的时间。所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能。为此,如果你尝试获得系统的最佳性能,调整运行线程的数量需要考虑可用的硬件并发。
  总结:为了提升性能而使用并发就像所有其他优化策略一样:它拥有大幅度提高应用性能的潜力,但它也可能让代码更加复杂,更难理解,并且更容易出 bug。因此,应用中只有性能悠关并且有潜在重大收益的部分,才值得进行并发化。当然,如果潜在性能收益仅次于设计清晰或关注点分离,可能也值得使用多线程。

一个简单的多线程

多线程库对应的头文件是#include <thread>,类名为std::thread。
一个简单的串行程序如下:

#include <iostream>
#include <thread>

void fun_1() {
    std::cout << "一个单线程" << std::endl;
}

int main() {
    fun_1();
    return 0;
}

这是一个典型的单线程的单进程程序,任何程序都是一个进程,main()函数就是其中的主线程,单个线程都是顺序执行。
将上面的程序改造成多线程程序,让fun_1()函数在另外的线程中执行:

#include <iostream>
#include <thread>

void fun_1() {
    std::cout << "多线程" << std::endl;
}

int main() {
    std::thread t1(fun_1);
    // do other things
    t1.join();
    return 0;
}

  初始线程始于main(),新线程始于fun_1()
分析:
  1、 首先,构建一个std::thread对象t1,构造的时候传递了一个参数,这个参数是一个函数,这个函数就是这个线程的入口函数(在上例中,fun_1就是新线程的入口函数),函数执行完了,整个线程也就执行完了。
  2、线程创建成功后,就会立即启动,并没有一个类似start的函数来显式的启动线程。
  3、一旦线程开始运行, 就需要显式的决定是要等待它完成(join),或者分离它让它自行运行(detach)。注意:只需要在std::thread对象被销毁之前做出这个决定。这个例子中,对象t1是栈上变量,在main函数执行结束后就会被销毁,所以需要在main函数结束之前做决定。
  4、这个例子中选择了使用t1.join(),主线程会一直阻塞着,直到子线程完成,join()函数的另一个任务是回收该线程中使用的资源。

参考链接
书籍:C++并发编程实战

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值