并行思维 [III]

书接上回:并行思维 [I]并行思维 [II]

线程是什么玩意

 

对于并行程序设计来说,线程的重要性不言而喻。

 

现代操作系统是典型的基于抢占式调度机制的多任务操作系统。

 

所谓多任务,指同一时刻,允许操作系统内有多个应用程序运行。比如,我们可以在同一时刻,一边收听音乐,一边浏览网页。当然,计算机能做到的远不止于此。

 

所谓抢占式调度机制,指在操作系统强制让另外的应用程序运行之前,正在运行的应用程序究竟可以占用 CPU 多少时间。这正是为什么我们感觉多个应用程序同时运行的真正原因,即使在单核处理器上。举例来说,Windows 操作系统任务调度的时间间隔大约在 20 毫秒左右(注:这个极短的时间,人眼无法感知,而人眼又欺骗了大脑,我们就感觉有多个程序在同时运行。实践所知,人眼能感知的最小时间大概是 100 毫秒。因此,Windows Client UI 开发人员应当注意,千万不要让你的程序界面响应大于 100 毫秒。假如大于此数,客户就会感到界面顿卡,带来槽糕的用户体验。Web UI 可以适当放宽界限。但无论怎样,快速的响应策略应当成为实现良好用户体验的首选)。

 

每个正在运行的应用程序实例都是一个进程。进程通常相对独立于其他进程运行。尤其是程序所使用的内存资源。举个例子,浏览器一般都不能直接访问音乐播放器的内存资源。如果有这个需求,则需要通过其他手段来达到该目的。比如文件映射,IPC,Socket 等机制。当然,这些机制通常比直接访问内存资源花费的时间要多。

 

拿最广泛应用的 Windows 操作系统而言,从软件开发人员的角度来看,进程其实就是个计算机资源集合。它是 Windows 操作系统分配和使用系统资源的基本单位。

 

Windows 进程均包含以下资源:

  1. 一个或多个线程。Linux 操作系统早期使用进程来模拟线程。 
  2. 一个虚拟地址空间,该空间独立于其他进程地址空间。当然显式共享的内存除外。请注意文件映射共享物理内存,但共享进程使用不同的虚拟地址来访问这些映射文件。
  3. 一个或多个代码段,包括 DLL 中的代码。
  4. 一个或多个包含全局变量的数据段。
  5. 环境字符串,包含环境变量信息。
  6. 进程堆。
  7. 其他资源,比如打开的句柄和其他的堆。

而进程的每个线程则共享代码,全局变量,环境字符串等资源。每个线程都独立进行调度,是最基本的可执行单元,并包含以下资源:

  1. 为过程调用、中断、异常处理器和自动存储建立的堆栈。
  2. 线程本地存储(TLS),这是个指针数组,可以让线程分配存储以创建其特有的数据环境。
  3. 堆栈参数,在创建线程时生成,对每个线程来说通常唯一。
  4. 上下文结构,由系统内核通过机器注册表值来维护。

对于 .NET 开发人员来说,情况又有些不同。CLR 保留了与 Windows 线程分离的权利,而且在某些寄宿情形中,CLR 线程并没有与 Windows 线程准确匹配。比如,宿主可以告诉 CLR 将每个 CLR 线程表示为一个 Windows 纤程(Fiber)(注:纤程是轻量级的线程。Windows 对纤程一无所知)。

 

图 1 展示了一个拥有多线程的 Windows 进程。注意,该图只是示意图,没有展示实际内存地址,也没有按照比例绘制。

 

F1

图 1 Windows 进程示意图

 

将问题分解进多个进程,并使用一个严格守信的通信机制来协调解决有很多好处。好处之一就是假如某个进程发生错误很少能影响到其他进程。由此,操作系统的健壮性有了极大提高,即使牺牲些许效率也可以接受。在多任务操作系统出现之前,单个程序引起整个系统崩溃是相当普遍的事情。

 

多进程工作适合于那些并不需要频繁同步的粗粒度并行任务,并且并行任务需要良好的协调。并行的粒度越细,则任务间通信所花费的时间越少。但是如果并行的粒度太细,则任务间通信的数量急剧增多,得不偿失。

 

因此,所有的现代操作系统都支持把多进程的任务细分到多线程来执行。线程跟进程一样,独立运行。没有线程知晓其他线程正在运行什么,也不知道它们在程序中的位置,除非进行显式的同步措施。进程和线程的主要区别在于同一进程的所有线程共享该进程的所有资源。很明显,这让工作变得更快速。因为线程间上下文切换更快,而且可以在同一地址空间内访问内存。

线程编程

 

一旦我们开始使用线程来编程,很多问题便接踵而来。我们应当如何划分及分配任务以便保持每个可用处理器核心忙碌?我们是否应当在每次拥有新任务时创建一个新线程,还是创建并管理一个线程池?线程数量应当依赖于处理器核心数量吗?线程处理完任务后,应当再干些什么?

 

这些都是实现多任务很重要的问题。但我们并不需要过多担心,Microsoft 为此做了大量的研究工作。诚如当年的汇编语言开发人员不得不考虑内存分配,内存布局,堆栈指针,寄存器分配等细节,而 C/C++ 则依靠编译器和库抽象隔离了这种细节。到了现今的 Java/.NET 开发人员通过 VM 和 GC 已经不再考虑此问题。Microsoft 的 .NET 并行扩展库正试图让开发人员远离线程管理,站在一个更高的抽象层次上,从而直接利用并行。

 

但是了解细节仍然必要,它可以让你编写的代码更加高效,以及优化代码时有的放矢。更详细可以参考《Programming Applications for Microsoft Windows》、《CLR via C#》、《Patterns for Parallel Programming》等经典著作。

 

当然楼主以后也会详细阐述。

线程安全

 

当编写的代码在并行/并发运行时会引发问题,那就不能说是线程安全的。单线程程序仅包含一个控制流,所以也就无所谓线程安全。但在多线程程序中,同一个函数和资源或许被多个控制流并发使用。因此,为多线程编写的代码必须要线程安全。

 

任何变量状态在被请求写入时都要确保线程安全。一般来说,我们会使用互斥来确保此刻只有一个线程执行代码,而把所有其他线程排斥在外。当然还要使用线程安全的库。如果你要使用某个库中的函数,请检查文档,看是否有线程安全的版本。比如,.NET 的泛型集合类均非线程安全。

 

如何保证线程安全正是下篇所要阐述的内容。

To be continued...

posted @ 2008-09-02 13:15  Angel Lucifer  阅读(3305)  评论(6编辑  收藏  举报