C#并行编程(1):理解并行

什么是并行

并行是指两个或者多个事件在同一时刻发生。

在程序运行中,并行指多个CPU核心同时执行不同的任务;对于单核心CPU,严格来说是没有程序并行的。并行是为了提高任务执行效率,更快的获取结果。

与并发的区别:

并发是指两个或者多个事件在同一时段发生。

相对于并行,并发强调的是同一时段,是宏观上的同时发生。实际上,同一时刻只有一个任务在被执行,多个任务是分时地交替执行的。并发是为了更合理地分配资源。

C#并行编程(1):理解并行

如何实现并行

并行编程中我们只关注应用层面的并行,CPU的指令并行技术(指令流水等)不在我们的考虑范围。

从并行的意义来看,并行编程的目的无非是让多个CPU核心同时执行不同业务逻辑,获取优良的性能。但是,要怎样实现并行呢?实现并行,我们要借助进程线程

为了更好地管理计算机中运行的程序,计算机操作系统引入进程

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。

——百度百科

由于进程拥有计算机资源,在创建、切换和撤销的过程中开销较大,这就限制了进程的并发程度;多核CPU的日渐普及的环境下,为提高并行粒度和并行计算的效率,引入了一种轻型的进程——线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

——百度百科

线程包含于进程,同一进程的线程共享该进程的资源。线程出现后,线程取代进程作为操作系统调度和分派的基本单位,极大地减少了进程切换带来的性能损失,使得更细粒度和更高性能的并行得以实现。

进程的调度

一台计算机会运行很多程序,这些程序进程的数量多会大于CPU的核心数量。每个CPU核心同一时间只能执行一个进程,那操作系统是如何管理这些进程的呢?

当启动一个程序的实例时,操作系统将创建一个进程用来调度该程序实例。一个进程主要包含以下的信息:

  • 进程控制块PCB,用于操作系统控制该程序实例

    • 进程标识信息,如PID、名称等
    • 现场信息,存放进程运行时处理器现场信息
    • 控制信息,存放操作系统用于管理和调度进程的信息
  • 专有的虚拟地址空间
  • 句柄列表
  • 程序实例的代码和数据,被映射到进程私有虚拟地址空间
  • 程序状态字信息

进程的状态模型,如下图:

C#并行编程(1):理解并行

操作系统按照进程状态进行程序调度。

  • 启动程序时,操作系统创建进程,此时进程为新建
    • 运行资源充足时,操作系统提交进程到就绪状态,等待CPU选择或者抢占CPU执行
    • 运行资源不足,如主存不够,操作系统会挂起进程,进程状态改为就绪挂起,等待操作系统的恢复
  • 就绪状态的进程
    • CPU空闲时,会选择执行就绪状态的进程,被选中的进程进入运行状态
    • 进程优先级高时,将抢占当前正在执行进程的CPU资源,自身进入运行状态
    • 操作系统会根据当前的可用资源,把就绪状态的进程挂起
  • 就绪挂起的进程
    • 当前没有就绪的进程,或者就绪挂起的某个进程具有较高的优先级,操作系统会将就绪挂起的进程恢复到就绪状态
  • 运行状态的进程
    • 进程自然结束、被强制终结或者出现无法解决的异常,将进入终止状态,终止的线程不再参与进程调度
    • 进程到达运行的时间片或者出现优先级高的进程抢占了CPU,进程会回到就绪状态等待调度
    • 进程等待资源、I/O或者信号时,会进入阻塞状态
    • 优先级较高的进程抢占CPU,而此时系统资源不足,则正在运行的线程会被转入就绪挂起状态
  • 阻塞状态的进程
    • 进程阻塞的条件被满足,如等待的资源到位、I/O完成或收到信号,会进入就绪状态
    • 进程在等待资源、I/O或者信号时,若系统检测到运行资源不足,会将阻塞的进程挂起进入阻塞挂起状态
  • 阻塞挂起的进程
    • 当被挂起的进程具有较高优先级,同时由于其他进程的退出使资源充裕,进程会被转为阻塞状态
    • 挂起的阻塞进程得到资源、I/O完成或者收到信号后,被转入就绪挂起状态

上述便是进程的调度过程,其中挂起的进程不占有任何资源。进程的调度很大程度是依赖于运行资源的;进程的优先级也是影响进程调度的重要因素;此外进程的调度还会涉及进程间的通信和同步问题,这里不做展开。

实际上,相对于进程,在并行编程中我们更关心线程,因为线程才是系统调度的基本单位。

线程的调度

在Windows系统中,每个进程至少有一个线程,每个线程都包含下面的内容:

  • 线程内核对象,包含线程上下文(包含CPU寄存器信息的内存块)
  • 线程环境块,包含线程的异常处理链首、本地存储数据等
  • 用户模式栈,存储传给方法的局部变量和实参
  • 内核模式栈,线程调用操作系统内核函数时,所传实参从用户模式栈复制到内核模式栈
  • DLL线程连接和分离,线程创建和销毁时,所依赖的DLL需要收到通知才能执行相关资源的初始化和清理

从线程所含内容,我们可以知道线程的创建和销毁是有着时间和空间开销的,虽然这些开销相较于进程来说小了很多,但仍是影响程序效率的重要因素。特别是在并行处理的时候,线程的频繁创建和销毁将对并行性能产生极为严重的影响。

系统同一时间只给一个CPU核心分配一个线程,CPU执行该线程达一个时间片后,系统会给该CPU核心分配另一个线程。系统分配线程至CPU核心的过程就是线程的上下文切换过程,此间,系统将执行3个动作:

  1. 把CPU寄存器的值保存到正在运行的线程上下文中
  2. 从现有线程集合中选取一个线程准备分配
  3. 把选中线程上下文中保存的CPU寄存器值加载到CPU寄存器中

线程上下文切换会对程序性能带来很严重的影响,特别是切换到一个新进程的新线程时,很可能需要从RAM中加载代码和数据,大家知道RAM相对于CPU高速缓存太慢了。

线程的创建、切换及销毁都是有着不可忽视的开销,在追求高性能的程序中,我们应尽量少地线程,最优性能的线程数是机器CPU的核心数。当然,性能只是程序的一个方面,响应性和可靠性也是要关注的重点。

小结

并行在进程层面依赖于系统可用系统资源和CPU核心数,单核CPU的程序并行,实质上是并发;在线程层面则主要依赖于CPU核心数以及我们安排线程的方式。

后续将以.NET为例总结并发编程。

:本文关于进程和线程的相关内容以Windows操作系统为参考。

上一篇:react+redux教程(一)connect、applyMiddleware、thunk、webpackHotMiddleware


下一篇:Linux安装Tomcat-Nginx-FastDFS-Redis-Solr-集群——【第十一集之安装FastDFS】