Electronic Joint Business

Solution for E-Business

Multi-Threading

Windows 下的多线程同步机制

广义上的进程是程序在计算机上的一次执行活动。当运行一个程序,你就启动了一个进程。显然,程序是静态的,进程是动态的。进程也称为任务。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多任务。现代的操作系统几乎都是多任务操作系统,能够同时管理多个进程的运行。多任务带来的好处是明显的,比如你可以边听音乐边上网,而这些任务之间丝毫不会相互干扰。

对计算机来说,原则上一个 CPU 只能分配给一个进程,以便运行这个进程。而对于只有一个 CPU 的计算机来说,要想同时运行多个进程,就必须使用并发技术。对于并发技术最容易理解的是“时间片轮转进程调度算法”,即在操作系统的管理下,所有正在运行的进程轮流使用 CPU,每个进程允许占用 CPU 的时间非常短(比如 10 毫秒),这样用户根本感觉不出来 CPU 是在轮流为多个进程服务,就好象所有的进程都在不间断地运行一样。但实际上在任一时间点有且仅有一个进程占有 CPU。如果一台计算机有多个CPU,情况就不同了,如果进程数小于 CPU 数,则不同的进程可以分配给不同的CPU来运行,这样,多个进程就是真正同时运行的,这便是并行。但如果进程数大于 CPU 数,则仍然需要使用并发技术。

狭义上的进程是操作系统的基本执行单位。在 Windows 中,线程是基本执行单位,而进程是一个容纳线程的容器。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间、文件描述符和信号处理等等。但同一进程中的多个线程各自拥有有名义上私有的调用栈(call stack)、寄存器上下文(register context) 和线程本地存储(thread-local storage)。

由于同一进程内的所有线程共享全部系统资源,所以线程间通信不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。

与多进程相比,多线程具有以下特点:1

>>> 阅读全文

 

,

I/O模型:阻塞、非阻塞 & 同步、异步

在进行 I/O 编程时,我们常常见到同步、异步、阻塞和非阻塞等术语,这些术语常常彼此交叉且不是易于理解。

同步
所谓同步,就是在发出一个函数调用时,在没有得到结果之前,该调用就不返回。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,提到同步、异步的时候,特指那些需要一定时间完成的任务,最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。

异步
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知或回调来通知调用者。以 CAsycSocket 类为例(注意,CSocket 从 CAsyncSocket 派生,但是起功能已经由异步转化为同步),当一个客户端通过调用 Connect 函数发出一个连接请求后,调用者线程立刻可以朝下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行部件可以通过三种途径返回结果:状态、通知和回调。使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。不过如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,和通知区别不大。

另外你也常常会看到用阻塞模式和非阻塞模式来实现 I/O 操作。所谓阻塞,是指当前线程在运行中也会因为某些原因而阻塞。 所有处于阻塞状态的线程的共同特征是: 放弃CPU, 暂停运行, 只有等到导致阻塞的原因消除, 才能恢复运行;

阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数 据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操 作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回

>>> 阅读全文

 

, , , ,

C++ 11 中的多线程

文章评价:

第一个 C++ 标准发布至今已经过去十三年了,随着新的 C++11(或称 C++0X)标准的推出,C++ 标准委员会所做出的显著改变之一就是支持多线程编程。这是首次在 C++ 语言中为并行编程提供支持,而且与平台无关。

在 C++11 标准之前,编写多线程应用需要依赖于特定的平台扩展,如 Intel TBB、OpenMP、Pthreads 等等。现在有了 C++11 的标准线程库就方便了应用移植(例如 Windows 上的 C++11 多线程应用将很容易移植到 Linux 平台),另外对于熟知 Boost 线程库的用户来说,由于 C++11 标准库中许多命名与结构都和 Boost 相同,上手非常容易。1

C++11 标准库中的类可用于线程操作与同步、公用保护数据及低层次的原子操作。

C++11 标准中涉及多线程编程的主要有四个头文件,分别是:<atomic> ,<thread>,<mutex>,<condition_variable>和<future>:

>>> 阅读全文

 

, , ,

并行程序中的同步与通讯

在任何程序中,总会有些执行阶段要等待操作系统或者其他进程完成 I/O 操作或者同步操作。这时候,程序就无法处理其它工作,因为实际上是操作系统或者其它进程占用了宝贵的 CPU 时间。这就是滥用文件读/写操作的程序会如此之慢的原因。1

这时候进程不得不等待操作系统服务例程将 CPU 从其他进程那里释放出来。这种等待也被称为进程处于阻塞状态。诸如 EDA 这类串行计算,整个算法作为单个任务被执行,如果该进程在等待任一种阻塞操作,整个算法就会完全停顿。在处于阻塞状态的这段时间里,另一个进程占用了 CPU 的所有权,所以算法根本没办法进行。

并行程序提供了一种手段,能在同一计算中调用另一个进程以提高 CPU 利用率,当一个进程被阻塞的时候(如等待 I/O 操作或者同步操作),另一个进程则继续处理共同工作的其它部分。这种情况在单 CPU 或者多 CPU 机器上皆可发生。

反之,对于串行程序(即非并行算法)来说,而无论有多少可用的 CPU ,在任何情况下都只能用到单个 CPU。除非有编译器将源程序从串行转成并行,不过能实现这一目标的实属特例。 因此如果想同时利用多个 CPU ,编码时要手工运用某种并行技术。(在源代码中采用特殊指令,如 OpenMP)。

并行程序能将工作划分为较小的部分,因此多个进程可以一起工作并互相协作以便能在更短的时间内计算出同样的结果。这时,在等待 I/O 设备完成操作的同时,并行程序能可以继续处理另一部分工作。此外,并行程序还可以同时使用系统所有可用的 CPU。

>>> 阅读全文

 

, , , , , , ,

Windows 内部探秘之线程

文章评价:
在当今的编程世界中,多线程已经成为 .NET、Java 或 C++ 等各种编程语言的一个必要组成部分。要编写具备高响应且可扩展的应用程序,就必须借、助多线程编程的力量。你也许用过 TPL、PLINQ、Task Factories、线程池、异步编程等等基础类库(Framework Class Libraries –FCL)进行过多任务处理,所有的这些在背后都依赖于 Windows 线程的力量来实现并行的场景。了解 Windows 线程的基本结构对于更好地运用和理解诸如 TPL、PLINQ 这些高级特性总是非常有帮助,并帮助你以可视化的方式了解在系统中多个线程是如何协同工作的,特别是当你在调试多线程应用程序的时候。在这篇文章中,我想和大家分享一些关于Windows线程的基础知识,它可以帮助您了解操作系统如何实现线程的。

Windows 线程由什么组成?
让我们看看 Windows 线程的一些基本组件。在 Windows 线程中有三个基本组件:

  • 线程内核对象
  • TEB

所有这三个组件一起创造了 Windows 线程。我会尝试逐一解释这些组件,但在这之前,让我们先对 Windows 内核和内核对象做简要介绍,因为这些都是Windows 操作系统最重要的部分

操作系统内核
内核是任何操作系统的主要组件。它是应用程序和硬件之间的桥梁。内核提供了抽象层,使得应用程序可以由此与硬件交互。

内核是操作系统最先加载的部分之一,它会一直保留在物理内存中。内核的主要功能是管理计算机的硬件和资源,让其他程序可以运行并使用这些资源。要更多地了解内核,请访问此链接

>>> 阅读全文

 

, ,

用OpenMP开发并行程序

OpenMP 是一套可于共享内存并行系统的多线程程序设计的指导语句(Compiler Directive),它最早是由 OpenMP Architecture Review Board 牵头提出的,现在OpenMP为许多厂商所支持,因此具有较高的可移植性。支持 OpenMP 的编程语言包括 C 语言、C++ 和 Fortran 等等,支持 OpenMP 的编译器包括 Sun Compiler,GNU Compiler 和 Intel Compiler,MS VC++ 等等。

OpenMP 提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的 pragma 来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。如果选择忽略这些 pragma,或者编译器不支持 OpenMP 时,程序又可退化为通常的程序(一般为串行),代码仍然可以正常运作,只是不能利用多线程来加速程序执行。

OpenMP 提供的这种对于并行描述的高层抽象降低了并行编程的难度和复杂度,这样程序员可以把更多的精力投入到并行算法本身,而非其具体实现细节。对基于数据分集的多线程程序设计,OpenMP 是一个很好的选择。同时,使用 OpenMP 也提供了更强的灵活性,可以较容易的适应不同的并行系统配置。线程粒度和负载平衡等是传统多线程程序设计中的难题,但在 OpenMP 中,库函数从程序员手中接管了这两方面的部分工作。

但是,作为高层抽象,OpenMP 并不适合需要复杂的线程间同步和互斥的场合。OpenMP 的另一个缺点是不能在非共享内存系统(如计算机集群)上使用。在这样的系统上,MPI 使用较多。

OpenMP 的架构
OpenMP 是作为共享内存系统的标准问世的。它是为在多处理器机上编写并行程序而设计的一个应用编程接口。它包括一套编译指导语句和一个用来支持它的函数库。

>>> 阅读全文

 

, , ,

Windows 7 用户模式调度机制 UMS

在 Windows7 x64 版本中,微软公开了用户模式调度这种有趣的机制。这种机制允许我们脱离内核模式线程调度,而在用户模式下编写自己的调度程序。在这段视频中, Windows 内核架构师 – Dave Probert – 解释了 UMS 的产生动机:

  • 上下文切换在用户模式会更快,因为它不需要在用户模式和内核模式之间频繁切换。
  • 用户模式调度引入了可提升性能的协作式多任务。现在你可以构想一种线程,除非在内核中被阻塞(例如等待IO操作完成),否则不会其他线程(来自同一组的 UMS)抢占。这有利于如创建更高效的锁。
  • 自定义的调度程序可以更好地控制线程的执行。例如,我们可以编写轮循调度程序以公平的方式逐个选择线程(这对有实时性要求的程序非常有用)。或者调度程序可以针对应用程序的特定事件作出反应 — 这些事件内核模式调度程序一无所知。

有关 UMS API 的详细内容可以在MSDN上找到
概括地说:用户模式调度( User Mode Scheduling – UMS) 是种轻量级的机制,应用程序用它来调度自己的线程。如果 UMS 线程在内核中被阻塞的话,应用程序不需要调用系统调度,就能切换用户态下的 UMS 线程,重新获得处理器控制权 。与纤程不同的是,每个 UMS 线程都有它自己的线程上下文而不是共享单一线程的上下文。对于管理那些数量巨大、不太需要系统调用而且工作时间较短的工作单元,这种在用户态下切换线程的能力使 UMS 比线程池要高效得多。

请注意:目前 UMS API 仅存在于 64 位 Windows 7和 Windows Server 2008 R2 及以上版本。任何调用了 UMS API 的源代码,编译时请记得把目标机器设置为 X64

游戏规则
使用 UMS API 需要注意以下细节:

  • 应用程序通过创建工作线程来执行实际工作。只有工作线程在内核模式下被阻塞时(比如页面错误或者线程在等待 I/O 操作结束),才可以被抢占。它们也可以调用 UmsThreadYield 来释放 CPU 处理时间。
  • 应用程序需要为运行工作线程的 CPU 核心创建用户模式调度程序。每当工作线程在内核中被阻塞或者调用了 UmsThreadYield ,用户模式调度程序会收到通知。当执行绪返回到调度程序时,它需要负责选择下一个将要运行的线程。
  • 调度程序所执行的代码是相当受限的。收到通知时,它不能触及任何工作线程所获得的锁,否则会导致死锁。(想象一下:线程等待被调度,而调度程序又等待该线程所获得的锁)。这意味着除非确信工作线程不会获得同样的锁,否则调度程序不能调用任何需要加锁的函数(比如全局堆分配函数)。

收到内核阻塞的通知似乎是把双刃剑。 好的方面是,这提供了更多的控制并允许更复杂的调度策略。例如,可以在某个逻辑处理器上创建线程池。如果执行中的线程被阻塞了,调度程序可以选择下一个线程(被分配到同一处理器)来执行以提高吞吐率。此时处理器缓存被污染的情况减至最小,因为一个任务只让一个核心来完成,能够带来更好的性能。 坏的方面是,通知必须从内核模式传递到用户模式,这当然需要一定的时间。而原有系统调度程序来根本无需这一步,因为调度发生在内核模式。所以实际上通知会降低性能。

>>> 阅读全文