首页 > 代码库 > 漫谈多线程(下)

漫谈多线程(下)

接着上一篇继续学习多线程。

死锁(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),就是为了延长交替执行时间,让其出现死锁现象。运行程序,效果如下图:

QQ Photo20140822183650

卡在这里不动了,这就是死锁现象。

由此,我们可以发现,出现死锁的前提是: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();        }    }
QQ Photo20140822184439

 

 

线程池

我们通过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.线程执行需要很长时间。(如果有些线程长时间占用线程池,那么对于线程池外排队的任务来说就是灾难)。

 

好了,多线程学习至此学完了。感觉一口气写下三篇技术文章,内心很有成就感。但是,有些话语组织的还是不好,技术讲解的也不是特别清楚。这些都需要改进,学如逆水行舟,不进则退。要坚持,要踏实。勤能补拙是良训,学习技术,一定不能手懒。要敲代码,要跑程序,要写博客,要善于总结。

漫谈多线程(下)