漫谈多线程(下)
接着上一篇继续学习多线程。
死锁(DeadLock)
当多线程共享资源时,各占一部分资源,而又在等待对方释放资源,这样的情况我们称为死锁。下面通过一个生动的程序来理解死锁。
class Program { private static object knife = new object(); //临界资源:刀子 private static object fork = new object(); //临界资源:叉子 //方法:拿起刀子 static void GetKnife() { Console.WriteLine(Thread.CurrentThread.Name + "拿起刀子. "); } //方法:拿起叉子 static void GetFork() { Console.WriteLine(Thread.CurrentThread.Name + "拿起叉子. "); } //方法:吃东西 static void Eat() { Console.WriteLine(Thread.CurrentThread.Name + "吃东西. "); } static void Main(string[] args) { //线程:女孩的行为 Thread girlThread = new Thread(delegate() { Console.WriteLine("今天的月亮好美啊````"); //过了一会儿,女孩子饿了,就去拿刀子和叉子 lock (knife) { GetKnife(); //* (待会儿会在这里添加一条语句) Thread.Sleep(20); lock (fork) { GetFork(); Eat(); //同时拿到刀子和叉子后开始吃东西 Console.WriteLine("女孩子放下叉子"); Monitor.Pulse(fork); } Console.WriteLine("女孩放下刀子"); Monitor.Pulse(knife); } }); girlThread.Name = "女孩子"; //定义线程的名称 //线程:男孩子的行为 Thread boyThread = new Thread(delegate() { //男孩和女孩聊天 Console.WriteLine("\n你更美!"); lock (fork) { GetKnife(); lock (knife) { GetKnife(); Eat(); //同时拿到刀子和叉子后开始吃东西 Console.WriteLine("男孩子放下刀"); Monitor.Pulse(knife); } Console.WriteLine("男孩子放下刀子"); Monitor.Pulse(fork); } }); boyThread.Name = "男孩子"; //定义线程的名称 //启动线程 girlThread.Start(); boyThread.Start(); } }
当同时满足叉子、刀子的情况下才可以吃饭。正常情况下,这个程序是没有问题的。但是,有时候会出现死锁现象。例如,当女孩子拿起刀子,准备去拿叉子的时候,线程切换到了男孩子,男孩子也想吃饭,就拿起了叉子,当去拿刀子的时候,发现刀子被女孩子占有。所以,就等待女孩子释放刀子。此时,线程切换到女孩子。女孩子去拿叉子的时候,发现叉子被男孩子占有。所以就等待男孩子释放叉子。他们互相等待对方释放资源,这就造成了死锁。因为这个程序很多,一般不会出现死锁的现象,越是比骄长时间的交替执行线程,越容易造成死锁。我们在//*添加的Thread.Sleep(20),就是为了延长交替执行时间,让其出现死锁现象。运行程序,效果如下图:
卡在这里不动了,这就是死锁现象。
由此,我们可以发现,出现死锁的前提是:1.线程之间出现交替 2.交替过程中各占一部分资源
那么应该如何决解这个死锁问题呢?解决方案非常简单,其实不难想象。出现死锁的原因是,当线程A获取资源a后,准备获取资源b。但是此时资源被线程B获取。那么解决方案就是,让线程A、B顺序获取资源。就是说,如果线程B获取不到资源a就不允许它获取资源b.这样,就不会出现死锁的现象了。让我们把上面的代码重新修改一下:
class Program { private static object knife = new object(); //临界资源:刀子 private static object fork = new object(); //临界资源:叉子 //方法:拿起刀子 static void GetKnife() { Console.WriteLine(Thread.CurrentThread.Name + "拿起刀子. "); } //方法:拿起叉子 static void GetFork() { Console.WriteLine(Thread.CurrentThread.Name + "拿起叉子. "); } //方法:吃东西 static void Eat() { Console.WriteLine(Thread.CurrentThread.Name + "吃东西. "); } static void Main(string[] args) { //线程:女孩的行为 Thread girlThread = new Thread(delegate() { Console.WriteLine("今天的月亮好美啊````"); //过了一会儿,女孩子饿了,就去拿刀子和叉子 lock (knife) { GetKnife(); //* (待会儿会在这里添加一条语句) Thread.Sleep(20); lock (fork) { GetFork(); Eat(); //同时拿到刀子和叉子后开始吃东西 Console.WriteLine("女孩子放下叉子"); Monitor.Pulse(fork); } Console.WriteLine("女孩放下刀子"); Monitor.Pulse(knife); } }); girlThread.Name = "女孩子"; //定义线程的名称 //线程:男孩子的行为 Thread boyThread = new Thread(delegate() { //男孩和女孩聊天 Console.WriteLine("\n你更美!"); lock (knife) { GetKnife(); lock (fork) { GetFork(); Eat(); //同时拿到刀子和叉子后开始吃东西 Console.WriteLine("男孩子放叉子"); Monitor.Pulse(fork); } Console.WriteLine("男孩子放下刀子"); Monitor.Pulse(knife); } }); boyThread.Name = "男孩子"; //定义线程的名称 //启动线程 girlThread.Start(); boyThread.Start(); } }
线程池
我们通过Thread类来创建线程,并通过它控制线程,对线程进行一些操作。但是过多的创建线程,销毁线程,会消耗内存与CPU的资源。例如,同一时间,创建了100个线程。那么创建与销毁这些线程所需的时间,可能远远大于线程本身执行的时间。好在C#为我们提供了线程池(Thread Pool)的技术。线程池为我们创建若干个线程,当一个线程执行完任务时不会理解销毁,而是接收别的任务。线程池内的线程轮流工作。这样解决了,创建、销毁线程所带来的消耗了。线程池由命名空间System,Threading下的ThreadPool实现。ThreadPool是一个静态类,不用实例化对象,可以直接使用。一个程序中只能有一个线程池,它会在首次向线程池中排入工作函数时自动创建。下面我们看一段程序:
class Program { public static void ThreadPoolTest() { //向线程池中添加100个工作线程 for (int i = 1; i <= 100; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(WorkFunction), i); } } //工作函数 public static void WorkFunction(object n) { Console.Write(n + "\t"); } static void Main(string[] args) { ThreadPoolTest(); Console.ReadKey(); //按下任意键结束程序 } }
我们通过ThreadPool的QueueUserWorkItem()方法,向线程池中排入工作函数。线程池中的线程会轮流执行这些函数。QueueUserWorkItem()方法的参数是一个waitCallback类型的委托。
Public delegate void WaitCallback(object dataForFunctionj);
下面,我们通过研究线程池中的线程数量,来深一步的了解一下线程池。我们假设线程池内线程数量的上限为30,下限为10.当我们向线程池中排入工作函数时。线程池会为我们创建10个空线程,这10个空线程来处理工作函数。随着工作函数的数量大于下限10时,线程池不是立即创建新的线程。而是先检查一下这10个线程有没有空闲,如果有,就去接新的工作。50毫秒后,如果检查没有发现空闲线程,那么线程池就会创建新的线程。随着工作函数的增加,线程池内的线程也会增加,直到达到上限30.如果工作函数的数量超过上限,线程池内的线程也不会增加,一直使用30个线程工作。比如,排入100个任务,只有30个进入线程池,另外70个在池外等候。随着任务低于上限30,空闲的线程会在2分钟后回收释放。直到达到下限10为止。
由此,我们可以发现线程池提高效率的关键是,线程执行完任务后,不会马上回收,而是继续接其他任务。
在一下情况不宜使用线程池:
1.需要为线程设置优先级(线程池内的线程不受程序员控制)
2.在执行过程中需要对线程进行操作,例如睡眠,挂起等。
3.线程执行需要很长时间。(如果有些线程长时间占用线程池,那么对于线程池外排队的任务来说就是灾难)。
好了,多线程学习至此学完了。感觉一口气写下三篇技术文章,内心很有成就感。但是,有些话语组织的还是不好,技术讲解的也不是特别清楚。这些都需要改进,学如逆水行舟,不进则退。要坚持,要踏实。勤能补拙是良训,学习技术,一定不能手懒。要敲代码,要跑程序,要写博客,要善于总结。