线程基本知识

1. 基本概念

1.1进程/线程

1.1.1 进程

进程指一个应用程序所运行的操作系统单元,它是操作系统环境中的基本成分、是系统进行资源分配的基本单位。

进程是执行程序的实例。
当运行一个应用程序后,就生成了一个进程,这个进程拥有自己的独立内存空间。
每一个进程对应一个活动的程序,当进程激活时,操作系统就将系统的资源包括内存、I/O和CPU等分配给它,使它执行。

  • 进程在运行时创建的资源随着进程的终止而死亡。

  • 进程间获得专用数据或内存的唯一途径就是通过协议来共享内存块,这是一种协作策略。

  • 一个进程可以创建多个线程及子进程(启动外部程序)。

  • 一个进程内部的线程可以共享该进程所分配的资源。

由于进程之间的切换非常消耗资源和时间,为了提高操作系统的并发性,提高CPU的利用率,在进程下面又加入了线程的概念。
线程的创建与撤销、线程之间的切换所占用的资源比进程少很多。

1.1.2 线程

进程可以分为若干个独立执行流(路径),这些执行流被称为线程。

线程是指进程内的一个执行单元,也是进程内的可调度实体。
线程是进程的一个实体,是CPU调度和分配时间的基本单位。

线程基本不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同一进程的其它线程共享进程所拥有的全部资源。
所以线程间共享内存空间很容易做到,多线程协作也很容易和便捷。

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程间可以并发执行。

线程提供了多任务处理的能力。

1.1.3 线程与进程的异同

  • 地址空间:

    • 进程拥有自己独立的内存地址空间;

    • 线程共享进程的地址空间;

      换句话说就是进程间彼此是完全隔绝的,同一进程的所有线程共享(堆heap)内存;

  • 资源拥有:

    • 进程是资源分配和拥有的单位
    • 同一进程内的线程共享进程的资源;
  • 系统粒度:

    • 进程是分配资源的基本单位
    • 线程则是系统(处理器)调度的基本单位;
  • 执行过程:

    • 每个独立的进程都有一个程序运行的入口、顺序执行序列和程序的出口;
    • 线程不能独立执行,必须依存于进程中;
  • 系统开销:

    创建或撤销进程时,系统都要为之分配或回收资源(如内存空间、IO设备)
    进程间的切换也要消耗远大于线程切换的开销。

二者均可并发执行。

一个程序至少有一个进程,一个进程至少有一个线程(主线程)。
主线程以函数地址的形式,如Main或WinMain函数,提供程序的启动点,当主线程终止时,进程也随之终止。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。

1.2 并发/并行

在单CPU系统中,系统调度在某一刻只能让一个线程运行,虽然这种调度机制有多种形式(时分/频分),但无论如何,要通过不断切换需要运行的线程,这种运行模式称为并发(Concurrent)。

而在多CPU系统中,可以让两个以上的线程同时运行,这种运行模式称为并行(Parallel)。

1.3 同步/异步操作

1.3.1 同步与异步的定义

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)

  • 同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。

  • 异步,和同步相反 调用方不会立即得到结果,而是在调用发出后调用者可用继续执行后续操作,被调用者通过状态来通知调用者,或者通过回掉函数来处理这个调用。

所有的程序最终都会由计算机硬件来执行,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。
这些无须消耗CPU时间的I/O操作是异步操作的硬件基础。
硬盘、光驱、网卡、声卡、显卡都具有DMA功能。

DMA(DirectMemory Access)是直接内存访问的意思,它是不经过CPU而直接进行内存数据存储的数据交换模式。

I/O操作包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.Net Remoting等跨进程的调用。

异步操作可达到避免调用线程堵塞的目的,从而提高软件的可响应性。

1.3.2 同步与异步的对比

线程不是一个计算机的硬件功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。

异步模式无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必共享变量,减少了死锁的可能。

不过,编写异步操作的复杂程度比较高,程序主要使用回调方式进行处理,与人的思维方式有出入,而且难以调试。

计算密集型工作使用多线程(如图形处理、算法);IO密集型工作使用异步机制。

1.4 任务管理器

映射名称列:
进程并不拥有独立于其所属实例的映射名称;

如果运行5个Notepad拷贝,你会看到5个称为Notepad.exe的进程;

它们是根据进程ID进行区分的,该进程ID是由系统维护,并可以循环使用。

CPU列:
它是进程中线程所占用的CPU时间百分比

每个任务管理器中的进程,其实内部都包含若干个线程,每个时间点都是某个程序进程中的某个线程在运行。

1.5 线程是如何工作的

线程被一个线程协调程序管理着——一个CLR委托给操作系统的函数。
线程协调程序确保将所有活动的线程被分配适当的执行时间;
并且那些等待或阻止的线程——比如说在排它锁中、或在用户输入——都是不消耗CPU时间的。

在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。
这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。
在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即通常在几微秒区间)

在多核的电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。
但这仍然会出现一些时间切片,因为操作系统的服务线程、以及一些其他的应用程序都会争夺对CPU的使用权。

线程由于外部因素(比如时间片)被中断被称为被抢占,在大多数情况下,一个线程在被抢占的那一刻就失去了对它的控制权。

1.6 线程安全

当使用线程(Thread)时,程序员必须注意同步处理的问题

理论上每个Thread都是独立运行的个体,由CLR来主导排程,视Thread的优先权的设置,每个Thread会分到特定的运行时间,当某个Thread的运行时间用完时,CLR就会强制将运行权由该Thread取回,转交给下个Thread
这也就意味着Thread本身无法得知自己何时会丧失运行权,所以会发生所谓的race condition(竞速状态)。

当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。
在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以”Done”只被打印了1次。代码以如此方式在不确定的多线程环境中被叫做线程安全。

临时暂停,或阻止是多线程的协同工作,同步活动的本质特征。
等待一个排它锁被释放是一个线程被阻止的原因,另一个原因是线程想要暂停或Sleep一段时间:

Thread.Sleep (TimeSpan.FromSeconds (30)); // 阻止30秒

一个线程也可以使用它的Join方法来等待另一个线程结束:

Threadt = new Thread(Go); // 假设Go是某个静态方法

t.Start();

t.Join(); // 等待(阻止)直到线程t结束

2. 使用多线程的情况分析

2.1 为什么要使用多线程

  • 并发需要
    在C/S或B/S模式下的服务端需要处理来自不同终端的并发请求,使用单线程是不可思议的。
  • 提高应用程序的响应速度
    当一个耗时的操作进行时,当前程序都会等待这个操作结束
    此时程序不会响应键盘、鼠标、菜单等操作,程序处于假死状态;
    使用多线程可将耗时长的操作(Time Consuming)置于一个新的线程,此时程序仍能响应用户的其它操作
  • 提高CPU利用率
    在多CPU体系中,操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  • 改善程序结构
    一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分
    这样的程序会利于理解和修改。
  • 花销小、切换快
    线程间的切换时间很小,可以忽略不计
  • 方便的通信机制
    线程间共享内存,互相间交换数据很简单。

多线程的意义在于
一个应用程序中,有多个执行部分可以同时执行:
一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。

C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行。
一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并具有多线程创建额外的线程。

2.2 何时使用多线程

多线程程序一般被用来在后台执行耗时的任务:主线程保持运行,而工作线程执行后台工作。

  • 对于Windows Forms程序来说,如果主线程执行了一个冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应,进入假死的状态,可能导致用户强制结束程序进程而出现错误。
    有鉴于此,应该在主线程运行一个耗时任务时另外添加一个工作线程,同时在主线程上有一个友好的提示“处理中…”,允许继续接收事件(比如响应鼠标、键盘操作)。
    同时程序还应该实现“取消”功能,允许取消/结束当前工作线程。
    BackgroundWorker类就提供这一功能。

  • 在没有用户界面的程序里,比如说WindowsService中使用多线程特别的有意义。
    当一个任务有潜在的耗时(在等待被请求方的响应——比如应用服务器,数据库服务器),用工作线程完成任务意味着主线程可以在发送请求后立即做其它的事情。

  • 另一个多线程的用途是在需要完成一个复杂的计算工作时,它会在多核的电脑上运行得更快,如果工作量被多个线程分开的话(C#中可使用Environment.ProcessorCount属性来侦测处理芯片的数量)。

远程服务器,或WebServices或ASP.NET程序将别无选择,必须使用多线程;
一个单线程的ASP.NET Web Service是不可想象的;
幸运的是,应用服务器中多线程是相当普遍的;
唯一值得关心的是提供适当锁机制的静态变量问题。

2.3 何时不用多线程

多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂
拥有多线程本身并不复杂,复杂是的线程的交互作用
无论交互是否是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的Bugs。
因此,要么多线程的交互设计简单一些,要么就根本不使用多线程,除非你有强烈的重写和调试欲望。

当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。
在某些情况下,太多的I/O操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务快的多。

3. C#中的线程

3.1 程序域

在.Net中Process由AppDomain对象所取代。

虽然AppDomain在CLR中被视为Process的替代品,但实际上AppDomain跟Process是属于主从关系的,AppDomain被放置在一个Process中,
每个Process可以拥有多个AppDomain,
每个AppDomain又可拥有多个Thread对象。

Process、AppDomain、Thread的关系如下图所示:

进程、域、线程关系图

AppDomain定义了一些事件供程序员使用。

事件 说明
AssemblyLoad 触发于AppDomain载入一个Assembly时
DomainUnLoad 触发于AppDomain卸载时,也就是Unload函数被调用或是该AppDomain被消灭前
ProcessExit 当默认的AppDomain被卸载时触发,多半是应用程序退出时

各AppDomain间互不影响。

3.2 C#中实现线程的方法

一个C#程序成为多线程可以通过2种方式来实现:

  • 明确地创建和运行多线程

  • 使用.NET Framework中封装了多线程的类
    比如:

    • BackgroundWorker类

    • 线程池

    • Threading Timer

更多C#中实现线程的方法请看
C#中实现线程的方法