线程和进程的区别

  1. 进程是应用程序的一个实例要使用的资源的一个集合。进程通过虚拟内存地址空间进行隔离,确保各个进程之间不会相互影响。同一个进程中的各个线程之间共享进程拥有的所有资源。
  2. 线程是系统调度的基本单位。时间片和线程相关,和进程无关。
  3. 一个进程至少要拥有一个前台线程。

线程开销

当我们创建了一个线程后,线程里面主要包括线程内核对象、线程环境块、1M大小的用户模式栈和内核模式栈。

  1. 线程内核对象:如果是内核模式构造的线程,则存在一个线程内核对象,包含一组对线程进行描述的属性,以及线程上下文(包含了CPU寄存器中的数据,用于上下文切换)。
  2. 线程环境块:用户模式中分配和初始化的一个内存块。
  3. 用户模式栈:对于用户模式构造的线程,应用程序可以直接和用户模式栈沟通。
  4. 内核模式栈:如果是内核模式构造的线程进行上下文切换和其他操作时,需要调用操作系统的函数。此时需要使用内核模式栈向操作系统的函数传递参数。应用程序代码无法直接访问内核模式栈,它需要借助用户模式的代码。

线程有自己的线程栈,大小为1M,所以它可以维护自己的变量。线程是一个新的对象,它会增加系统上下文切换的次数,所以过多的线程将导致系统开销很大。例如outlook会创建38个线程,但大部分时候他什么都不做。所以我们白白浪费了38M的内存。

单核CPU一次只能做一件事,所以系统必须不停的进行上下文切换,且所有的线程(逻辑CPU)之间共享物理CPU。在某一时刻,系统只将一个线程分配给一个CPU。然后,该线程可以运行一个时间片(大约30毫秒),过了这段时间,就发生上下文切换到另一个线程。

假设某个应用程序的线程进入无限循环,系统会定期抢占他(不让他再次运行)而允许新线程运行一会。如果新线程恰好是任务管理器的线程(此时将会发现任务管理器可以响应,而任务管理器之外屏幕其他地方则仍然无响应),则用户可以利用任务管理器杀死包含了其他已经冻结的线程的进程。通过这种做法,上下文切换开销并不会带来任何性能增益,但换来了好得多的用户体验(很难死机,用户可以用任务管理器杀死其他的进程)。

当某个线程一直空闲(例如一个开启的记事本但长时间无输入)时,他可以提前终止属于他的时间片。线程也可以进入挂起状态,此时之后任何时间片,都不会分配到这个线程,除非发生了某个事件(例如用户进行了输入)。节省出来的时间可以让CPU调度其他线程,增强系统性能。

线程的状态

可以用下图表示:

.NET面试题系列[17] - 多线程概念(2)

线程的主要状态有四种:就绪(Unstarted),运行(Running),阻塞(WaitSleepJoin)和停止(Stopped),还有一种Aborted就是被杀死了。通常,强制获得线程执行任务的结果,或者通过锁等同步工具,会令线程进入阻塞状态。当得到结果之后,线程就解除阻塞,回到就绪状态。

当建立一个线程时,它的状态为就绪。使用Start方法令线程进入运行状态。此时线程就开始执行方法。如果没有遇到任何问题,则线程执行完方法之后,就进入停止状态。

阻塞(WaitSleepJoin),顾名思义,是使线程进入阻塞状态。当一个线程被阻塞之后,它立刻用尽它的时间片(即使还有时间),然后CPU将永远不会调度时间片给它直到它解除阻塞为止(在未来的多少毫秒内我不参与CPU竞争)。主要方式有:Thread.Join(其他线程都运行完了之后就解除阻塞),Thread.Sleep(时间到了就解除阻塞),Task.Result(得到结果了就解除阻塞),遭遇锁而拿不到锁的控制权(等到其他线程释放锁,自己拿到锁,就解除阻塞)等。当然,自旋也是阻塞的一种。

Thread类中的方法对线程状态的影响

Start:使线程从就绪状态进入运行状态

Sleep:使线程从运行状态进入阻塞状态,持续若干时间,然后阻塞自动解除回到运行状态

Join:使线程从运行状态进入阻塞状态,当其他线程都结束时阻塞解除

Interrupt:当线程被阻塞时,即使阻塞解除的要求还没有达到,可以使用Interrupt方法强行唤醒线程使线程进入运行状态。这将会引发一个异常。(例如休息10000秒的线程可以被立刻唤醒)

Abort:使用Abort方法可以强行杀死一个处于任何状态的线程

时间片

当我们讨论多任务时,我们指出操作系统为每个程序分配一定时间,然后中断当前运行程序并允许另外一个程序执行。这并不完全准确。处理器实际上为进程分配时间。进程可以执行的时间被称作“时间片”或者“限量”。时间片的间隔对程序员和任何非操作系统内核的程序来说都是变化莫测的。程序员不应该在他们的程序中将时间片的值假定为一个常量。每个操作系统和每个处理器都可能设定一个不同的时间。

进程和线程优先级

Windows是一个抢占式的操作系统。在抢占式操作系统中,较高优先级的进程总是抢占(preempt较低优先级的进程(即使时间片没有用完)。用户不能保证自己的线程一直运行,也不能阻止其他线程的运行。 

每一个进程有一个优先级类,每一个线程有一个优先级(0-31)。较高优先级的进程中的较高优先级的线程获得优先分配时间片的权利。

只要存在可以调度的高优先级的线程,系统就永远不会将低优先级的现场分配给CPU,这种情况称为饥饿。饥饿应该尽量避免,可以使用不同的调度方式,而不是仅仅看优先级的高低。在多处理器机器上饥饿发生的可能性较小些,因为这种机器上,高优先级的线程和低优先级的线程可以同时运行。

Thread类中的Priority允许用户改变线程的优先级(但不是直接指定1-31之间的数字,而是指定几个层级,每个层级最终mapping到数字,例如层级normal会映射到4)

前台和后台线程

一个进程可以有任意个前台和后台线程。前台线程使得整个进程得以继续下去。一个进程的所有前台线程都结束了,进程也就结束了。当该进程的所有前台线程终止时,CLR将强制终止该进程的所有后台线程,这将会导致finally可能没来得及执行(从而导致一些垃圾回收的问题)。解决的方法是使用join等待。例如你在main函数中设置了一个后台线程,然后让其运行,假设它将运行较长的时间,而此后main函数就没有代码了,那么程序将立刻终止,因为main函数是后台线程。

使用thread类创建的线程默认都是前台线程。Thread的IsBackground类允许用户将一个线程置为后台线程。

多线程有什么好处和坏处?

好处:

  1. 更大限度的利用CPU和其他计算机资源。
  2. 当一条线程冻结时,其他线程仍然可以运行。
  3. 在后台执行长任务时,保持用户界面良好的响应。
  4. 并行计算(仅当这么做的好处大于对资源的损耗时)

坏处:

  1. 线程的创建和维护需要消耗计算机资源。(使用线程池,任务来抵消一部分损失)。一条线程至少需要耗费1M内存。
  2. 多个线程之间如果不同步,结果将会难以预料。(使用锁和互斥)
  3. 线程的启动和运行时间是不确定的,由系统进行调度,所以可能会造成资源争用,同样造成难以预料的结果。(使用锁和互斥,或者进行原子操作)

为了避免2和3,需要开发者更精细的测试代码,增加了开发时间。

System.Threading类的基本使用

创建线程

可以使用Thread的构造函数创建线程。我们要传递一个方法作为构造函数的参数。通常我们可以传递ThreadStart委托或者ParameterizedThreadStart委托。后者是一个可以传递输入参数的委托。两个委托都没有返回值。ThreadStart委托的签名是:public delegate void ThreadStart();

1 基本例子:通过Thread构造函数建立一个线程。传递的方法WriteY没有返回值,也没有输入。之后使用Start方法使线程开始执行任务WriteY。

class ThreadTest
{
  static void Main()
  {
    Thread t = new Thread (WriteY);          
    t.Start();                            
 
    for (int i = 0; i < 1000; i++) Console.Write ("x");
  }
 
  static void WriteY()
  {
    for (int i = 0; i < 1000; i++) Console.Write ("y");
  }
}
View Code

这个例子中,主线程和次线程同时访问一个静态方法(静态方法是类级别的)。此时系统调度使得主线程和次线程轮流运行(但运行的顺序是随机的)。所以结果可能是

xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2 主线程和次线程分别维护各自的局部变量

static void Main()
{
  new Thread (Go).Start();    
  Go();                         
}
 
static void Go()
{
  // Declare and use a local variable - 'cycles'
  for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}
View Code

相关文章: