漫谈多线程(下)

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

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

 

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

posted @ 2014-08-22 19:23  VitoCorleone  阅读(278)  评论(0编辑  收藏  举报