并发简介

作者: 杨尹一 | 来源:发表于2018-05-25 20:43 被阅读0次

    并发基本概念


    并发与并行

    并发指的是程序的结构,而并行指的是程序运行时的状态。

    并发的两种形式:多核机器上的真正并行和单核机器上的任务切换
    在并发环境时,多线程不可能真正充分利用CPU,节约运行时间,它只是以“挂起->执行->挂起“”的方式以很小的时间片分别运行各个线程,给用户以每个线程都在运行的错觉。在这种环境中,多线程程序真正改善的是系统的响应性能和程序的友好性。
    在并行环境中,,一个时刻允许多个线程运行,这时多线程程序才真正充分利用了多CPU的处理能力,,节省了整体的运行时间。在这种环境中,多线程程序能体现出它的四大优势:充分利用CPU,节省时间,改善响应和增加程序的友好性。
    1. 何谓并发(concurrency)

    并发是指两种或两种以上的行为在系统中同时存在,至于是否是在某一时刻同时执行,在并发的概念里并不考虑。
    当我们说一个程序是并发的,实际上是指该程序“支持并发的设计”,所以问题就变成了怎样的程序结构才叫做支持并发的设计?
    并发设计的标准是:使过个操作可以再重叠的时间段内运行。并行同样是在重叠的时间段内运行,因此并行是并发的一种模式。并发的另一种模式为协程,也就是任务切换(task switch)。
    系统每次从一个任务切换到另一个时都需要切换一次上下文(context switch),任务切换也有时间开销。进行上下文的切换时,操作系统必须为当前运行的任务保存CPU的状态和指令指针,并计算出要切换到哪个任务,并为即将切换到的任务重新加载处理器状态。然后,CPU可能要将新任务的指令和数据的内存载入到缓存中,这会阻止CPU执行任何指令,从而造成的更多的延迟。
    并发设计的程序在实际执行过程中并不一定会多个任务执行时间段重叠的现象(可能当前只有一个任务在运行)。并发并不是在描述程序执行的状态,而是程序的结构。

    多核之间的任务切换
    在单核处理器上的并发可以说是伪并发,多核处理器上,才可以做到真正的并发。
    1. 何谓并行(parallelism)

    并行是指多个动作在某一时刻同时执行
    判断一个程序是否处于并行状态,只需要看同一个时刻是否有超过一个工作单位在运行。单线程永远无法达到并行状态。

    1. 并发与并行的关系
    Different concurrent designs enable different ways to parallelize.
    并发设计让并发执行成为可能,而并行是并发的一种模式。
    

    并发只是一种逻辑思想昂,而并行是真正在物理上达到并行运行。


    如何向一个五岁小孩解释并发与并行

    并发的实质是单个物理 CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发可以对有限物理资源强制行使多用户共享以提高效率。
    并行(Parallelism)指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。
    并发是一个处理器同时处理多个任务,而并行多个处理器或者是多核的处理器同时处理多个不同的任务。前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生。

    并发的方式

    1. 多进程并发

    多进程并发是指将应用户程序分为多个独立的进程,在同一时刻运行,就像此刻我的电脑同时在执行文本处理又在使用网易云播放歌曲。

    • 多进程并发缺点
      独立的进程可以通过进程间的通信渠道(如信号、套接字、文件、管道等)。但是进程之间的通信要么复杂、要么低效,并且运行多个进程需要固定开销:启动进程耗时、操作系统需要调用内部资源管理进程。
    • 多进程并发优点
      操作系统在进程间提供附加的保护操作和高级别的通信机制,意味着比较容易编写安全的代码。另外还可以通过远程连接的方式在不同的机器上执行不同的进程,如分布式系统。
    1. 多线程并发

    在单个进程中运行多个线程,线程像是轻量级的进程。进程中的线程共享地址空间,共享数据。

    • 多线程并发缺点
      线程间共享数据时可能会遇到问题,如条件竞争。开发者必须保证每个线
      程所访问的数据时一致额。
    • 多线程并发优点
      由于地址空间共享,加上缺少线程间数据的保护,使得操作系统的工作量减少,所以开销较小。

    由于C++并未对进程间通信提供原生支持,想要实现多进程并发需要依赖平台相关的API。因此,本文主要关注多线程的并发编程。

    并发的效率问题

    1. 单核处理器上的并发

    多线程在单核处理器上也是顺序执行的,只是操作系统可以帮助你在很短的时间内进行了很多上下文切换。在同一时间点只有一个线程在执行,系统给每个线程分配时间片执行,每个时间片大概10ms左右,在操作系统进行上下文切换时,也是需要花费时间的,实际效率并不会提高,反而会因为上下文切换增加开销。
    因此在单核处理器上,尽量不要使用并发编程,控制线程数量,这样可以再一定程度上提高程序性能,另外还避免了不必要的线程同步问题。

    1. 多核处理器上的并发

    在双核处理器上,三线程反而比双线程效率更低。在使用CPU占用较高的算法运算时,单核单线程、双核双线程、四核四线程比较合适。但是有时候会发现线程数量超过CPU内核数反而效率更高。这是因为这种程序的单线程运算不足以占满整个CPU的一个内核。(比如存在大量IO操作,IO操作比较慢,是程序瓶颈)。
    多线程的用处在于,做某个耗时的操作时,需要等待返回结果,这时用多线程可以提高程序并发程度。如果一个不需要任何等待并且顺序执行能够完成的任务,用多线程简直是浪费。

    1. 设么时候使用并发编程

    线程必然不是越多越好,线程切换也是要开销的,当你增加一个线程的时候,增加的额外开销要小于该线程能够消除的阻塞时间,这才叫物有所值。
    Linux自从2.6内核开始,就会把不同的线程交给不同的核心去处理。Windows也从NT.4.0开始支持这一特性。
    什么时候该使用多线程呢?这要分四种情况讨论:

    • 多核CPU——计算密集型任务
      此时要尽量使用多线程,可以提高任务执行效率,例如加密解密,数据压缩解压缩(视频、音频、普通数据),否则只能使一个核心满载,而其他核心闲置。
    • 单核CPU——计算密集型任务
      此时的任务已经把CPU资源100%消耗了,就没必要也不可能使用多线程来提高计算效率了;相反,如果要做人机交互,最好还是要用多线程,避免用户没法对计算机进行操作。
    • 单核CPU——IO密集型任务
      使用多线程还是为了人机交互方便,
    • 多核CPU——IO密集型任务
      这就更不用说了,跟单核时候原因一样。

    C++中的并发

    1. 起源

    C++98(1998)标准不承认线程的存在,并且各种语言要素的操作效果都以顺序抽象机的形式编写。不仅如此,内存模型也没有正式定义,所以在C++98标准下,没办法在缺少编译器相关扩展的情况下编写多线程应用程序。
    当然,编译器供应商可以自由地向语言添加扩展,添加C语言中流行的多线程API。POSIX标准中的C标准和Microsoft Windows API中的那些。这就使得很多C++编译器供应商通过各种平台相关扩展来支持多线程。这种编译器支持一般受限于只能使用平台相关的C语言API,并且该C++运行库(例如,异常处理机制的代码)能在多线程情况下正常工作。因为编译器和处理器的实际表现很不错了,所以在少数编译器供应商提供正式的多线程感知内存模型之前,程序员们已经编写了大量的C++多线程程序了。
    由于不满足于使用平台相关的C语言API来处理多线程,C++程序员们希望使用的类库能提供面向对象的多线程工具。像MFC这样的应用框架,如同Boost和ACE这样的已积累了多组类的通用C++类库,这些类封装了底层的平台相关API,并提供用来简化任务的高级多线程工具。各种类和库在细节方面差异很大,但在启动新线程的方面,总体构造却大同小异。一个为许多C++类和库共有的设计,同时也是为程序员提供很大便利的设计,也就是使用带锁的获取资源即初始化(RAII, Resource Acquisition Is Initialization)的习惯,来确保当退出相关作用域时互斥元解锁。
    编写多线程代码需要坚实的编程基础,当前的很多C++编译器为多线程编程者提供了对应(平台相关)的API;当然,还有一些与平台无关的C++类库(例如:Boost和ACE)。正因为如此,程序员们可以通过这些API来实现多线程应用。不过,由于缺乏统一标准的支持,缺少统一的线程内存模型,进而导致一些问题,这些问题在跨硬件或跨平台相关的多线程应用上表现得尤为明显。

    1. C++11对并发的支持

    所有的这些随着C++11标准的发布而改变了,新标准中不仅有了一个全新的线程感知内存模型,C++标准库也扩展了:包含了用于管理线程、保护共享数据、线程间同步操作,以及低级原子操作的各种类。
    新C++线程库很大程度上,是基于上文提到的C++类库的经验积累。特别是,Boost线程库作为新类库的主要模型,很多类与Boost库中的相关类有着相同名称和结构。随着C++标准的进步,Boost线程库也配合着C++标准在许多方面做出改变,因此之前使用Boost的用户将会发现自己非常熟悉C++11的线程库。
    如本章起始提到的那样,支持并发仅仅是C++标准的变化之一,此外还有很多对于编程语言自身的改善,就是为了让程序员们的工作变得更加轻松。这些内容在本书的论述范围之外,但是其中的一些变化对于线程库本身及其使用方式产生了很大的影响。附录A会对这些特性做一些介绍。
    新的C++标准直接支持原子操作,允许程序员通过定义语义的方式编写高效的代码,从而无需了解与平台相关的汇编指令。这对于试图编写高效、可移植代码的程序员们来说是一个好消息;编译器不仅可以搞定具体平台,还可以编写优化器来解释操作语义,从而让程序整体得到更好的优化。

    1. C++线程库的效率

    通常情况下,这是高性能计算开发者对C++的担忧之一。为了效率,C++类整合了一些底层工具。这样就需要了解相关使用高级工具和使用低级工具的开销差,这个开销差就是抽象代价(abstraction penalty)。
    C++标准委员会在设计标准库时,特别是设计标准线程库的时候,就已经注意到了这点;目的就是在实现相同功能的前提下,直接使用底层API并不会带来过多的性能收益。因此,该类库在大部分主流平台上都能实现高效(带有非常低的抽象代价)。
    C++标准委员会为了达到终极性能,需要确保C++能给那些要与硬件打交道的程序员,提供足够多的的底层工具。为了这个目的,伴随着新的内存模型,出现了一个综合的原子操作库,可用于直接控制单个位、字节、内部线程间同步,以及所有变化的可见性。原子类型和相应的操作现在可以在很多地方使用,而这些地方以前可能使用的是平台相关的汇编代码。使用了新标准的代码会具有更好的可移植性,而且更容易维护。
    C++标准库也提供了更高级别的抽象和工具,使得编写多线程代码更加简单,并且不易出错。有时运用这些工具确实会带来性能开销,因为有额外的代码必须执行。但是,这种性能成本并不一定意味着更高的抽象代价;总体来看,这种性能开销并不比手工编写等效函数高,而且编译器可能会很好地内联大部分额外代码。
    某些情况下,高级工具会提供一些额外的功能。大部分情况下这都不是问题,因为你没有为你不使用的那部分买单。在罕见的情况下,这些未使用的功能会影响其他代码的性能。如果你很看重程序的性能,并且高级工具带来的开销过高,你最好是通过较低级别的工具来实现你需要的功能。绝大多数情况下,额外增加的复杂性和出错几率都远大于性能的小幅提升带来的收益。即便是有证据确实表明瓶颈出现在C++标准库的工具中,也可能会归咎于低劣的应用设计,而非低劣的类库实现。

    相关文章

      网友评论

        本文标题:并发简介

        本文链接:https://www.haomeiwen.com/subject/expujftx.html