• 漫谈多线程(下)


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

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

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

  • 相关阅读:
    初始rt1052GPIO之输入输出操作
    i.MX RT驱动LCD详解
    GPIO输入—按键查询检测
    RT1052之IOMUXC
    常见和弦整理
    和声进行的基本框架
    为什么很多人会有“年龄焦虑”?
    vuex store 分模块化
    clickhouse集群部署方法和验证方法
    【TPCH】总结介绍
  • 原文地址:https://www.cnblogs.com/VitoCorleone/p/3930143.html
Copyright © 2020-2023  润新知