代码改变世界

【线程呓语】与线程相关的一些概念

2011-01-18 21:55 by 横刀天笑, ... 阅读, ... 评论, 收藏, 编辑

上一篇文章已经介绍了,线程是对CPU的模拟和抽象,因为一台机器只有一个CPU,又要执行多个应用的代码,为了让上层应用不考虑这些细节,而使用线程这么个东西抽象一下,这样让上层应用觉得整个CPU都是它的。但CPU毕竟只有一个(或是有限的),那么就必定存在线程的切换。这也就涉及线程的状态转换:

image

主意这里的箭头有双向的,有单向的。

线程在运行时可能由于需要某种资源而暂时停止运行,这称之为阻塞,当需要的资源得到满足时线程的状态会变成就绪,如果这个时候正好有CPU空闲或者正好线程调度到这个线程上,那么该线程就会马上执行。线程在运行时还有可能因为自己的时间片用完了,线程调度程序安排别的线程执行(上下文切换),而从运行状态变成就绪状态。就绪状态也可以稍后调度为运行状态。线程调度的算法有很多种,这在操作系统领域有很多文献专门来描述这个。

死锁(deadlock)

在写多线程相关的书籍或者文章中,恐怕死锁这个词出现的频率最高。那死锁到底是一个什么意思呢?

为了解释这个概念,我们假设有两个线程T1和T2,有两个资源R1和R2。在某一时刻,T1拥有R1,T2拥有R2。但是这两个线程贪得无厌,T1还需要R2才能运行所以阻塞了,而T2需要R1才能运行所以也阻塞了。所以这两个线程总是不断地尝试获取永远也得不到的东西互不相让,这种状态除非有外力的介入,不然不会打破。这就是死锁。死锁的危害性非常大,有可能造成整个系统的宕机。

我们来看下面这段程序,来演示一下如何弄出个死锁出来(该程序仅仅为了演示目的,实际中不要如此编码):

   1: using System;
   2: using System.Threading;
   3: namespace DeadLock
   4: {
   5:     class Program
   6:     {
   7:         static void Main(string[] args)
   8:         {
   9:             Test t = new Test();
  10:             Thread t1 = new Thread(t.test1);
  11:             Thread t2 = new Thread(t.test2);
  12:  
  13:             t1.Start();
  14:             t2.Start();
  15:  
  16:             Console.ReadLine();
  17:  
  18:         }
  19:     }
  20:  
  21:     public class Test
  22:     {
  23:         private object resource1 = new object();
  24:         private object resource2 = new object();
  25:  
  26:         public void test1()
  27:         {
  28:  
  29:             Console.WriteLine(string.Format("test1:Thread{0} try to get resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
  30:             lock (resource1)
  31:             {
  32:                 Console.WriteLine(string.Format("test1:Thread{0} got resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
  33:                 Thread.Sleep(500);
  34:                 Console.WriteLine(string.Format("test1:Thread{0} try to get resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
  35:                 lock (resource2)
  36:                 {
  37:                     Console.WriteLine(string.Format("test1:Thread{0} got resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
  38:                 }
  39:             }
  40:         }
  41:  
  42:         public void test2()
  43:         {
  44:             Console.WriteLine(string.Format("test2:Thread{0} try to get resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
  45:             lock (resource2)
  46:             {
  47:                 Console.WriteLine(string.Format("test2:Thread{0} got resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
  48:                 Thread.Sleep(500);
  49:                 Console.WriteLine(string.Format("test2:Thread{0} try to get resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
  50:                 lock (resource1)
  51:                 {
  52:                     Console.WriteLine(string.Format("test2:Thread{0} got resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
  53:                 }
  54:             }
  55:         }
  56:     }
  57: }

thread1获取resource1后等待一会儿,让thread2获取到resource2,然后thread1“咬住”resource1死死不放,还去获取resource2,而thread2“咬住”resource2死死不放,去获取resource1,这样thread1和thread就像抱团一样了。

死锁形成的必要条件:

互斥条件:resource1和resource2只能被一个线程拥有

占有和等待条件:已经得到resource1的thread1可以再请求resource2,thread2也可以在获取到resource2后再获取resource1

不可抢占条件:已经分配给一个线程的资源不能强制性地被抢占(thread1已经得到了resource1,不能强制地剥夺它,除非thread1自己释放resource1,在这里就是退出lock块)

循环等待条件:死锁发生时,系统中肯定有两个或两个以上的线程组成一条环路,该环路中的每个线程都等待着下一个线程已经占有的资源。

与死锁相近的一个概念是活锁,或称之为饥饿

活锁(livelock)

说的就是某个线程或进程因为某些原因总是得不到自己需要的资源。假如现在排队买饭,饭堂规定年龄小的和年龄老的可以优先买饭,站在队伍的前面,如果总是有源源不断的人加进来,且总是 比你年龄小或年龄老,你就总是排在队伍的后头,最后饥饿而死~~

还有一个常见的情况是,编程时为了提高效率,常常将写操作和读操作分开,比如多个线程可以同时读某一资源,但只要有一个线程写那么其他线程就不能读也不能写了,那么如果现在有很多线程在读资源A,而有一个线程来写,这样就会造成总是有线程在读,而写线程却插不上去。

线程安全(thread safety)

在一些框架API文档中总是会出现这么一个句子:该接口是线程安全的。那么线程安全到底是什么意思呢?

意思就是说这个接口是否能在多线程环境下安全的调用。比如该接口可能会修改一些共享的数据,而又没有对这些共享数据加锁,那么就不能安全地在多线程环境下使用了。