我的CSDN博客:http://blog.csdn.net/bitfan我的新浪微博:http://t.sina.com.cn/jinxuliang

金旭亮

让技术变得有趣,将学习升级为探索
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

一个多线程示例程序的BUG修复

Posted on 2011-01-14 13:41  金旭亮  阅读(1388)  评论(3编辑  收藏  举报
一个多线程示例程序的BUG修复



    在《.NET 4.0面向对象编程漫谈 》的《应用篇》一书中,我介绍了一个使用“信号量(Semaphore) ”同步对象模拟多人使用图书馆公共计算机的UseLibraryComputer示例(参看17.3.3节《管理多个共享资源--Semaphore》):



 

图 1 UseLibraryComputer示例

    此示例程序定义了一个Computer类代表计算机,使用一个Computer对象数组(取名LibraryComputers)保存图书馆所拥有的多台公共计算机(即Computer对象)。
    示例程序动态创建多个线程模拟多用户(每个线程代表一个用户)同时访问3个Computer对象,为了同步这些线程,创建了一个Semaphore对象(取名sp),以下为示例中的线程函数:

        static void UseComputer(Object UserName)
        {
            sp.WaitOne();//等待计算机可用
            //查找可用的计算机
            Computer cp=null;
            for (int i = 0; i < ComputerNum; i++)
                if (LibraryComputers[i].IsOccupied == false)
                {
                    cp = LibraryComputers[i];
                    break;
                }
            //使用计算机工作
            cp.Use(UserName.ToString());
            //不再使用计算机,让出来给其他人使用
            sp.Release();
        }

    Computer类中定义了一个IsOccupied字段用于标识此计算机是否被占用,其Use方法模拟表示用户正在使用计算机的过程:

    //是否被占用
    public  bool IsOccupied = false;
    //读者在使用计算机
    public  void Use(String userName)
    {
        Console.WriteLine("{0}开始使用计算机{1}", userName,ComputerName);
        IsOccupied = true;  //设置占用标记
        Thread.Sleep(new Random().Next(1, 2000)); //随机休眠,以模拟人使用计算机
        Console.WriteLine("{0}结束使用计算机{1}", userName,ComputerName);
        IsOccupied = false; //设置空闲标记
    }

    前述示例代码中隐藏着一个BUG,请看图2:


 

 

 图 2

    BUG隐藏于何处?
    请读者先仔细阅读原示例程序 ,尝试着自己定位并修正此BUG。
    以下是BUG分析:
    这里面的关键是Semaphore对象可能会一次允许多个线程投入运行,而这些投入运行的线程都需要访问LibraryComputers数组(请参看前面的UseComputer静态方法)。
    假设有3台计算机空闲,当示例程序第1次运行时,会有3个线程同时访问LibraryComputers数组,完全有可能两个线程都发现第1台计算机是“空闲”的,于是它们将调用此Computer对象的Use()方法。
    这就是BUG所在。
    定位BUG之后,要修正它就很容易了--互斥访问LibraryComputers数组即可
    修改后的线程函数如下:

        static void UseComputer(Object UserName)
        {
            sp.WaitOne();//等待计算机可用
            Computer cp=null; //查找可用的计算机
            lock (LibraryComputers)
            {
                for (int i = 0; i < ComputerNum; i++)
                    if (LibraryComputers[i].IsOccupied == false)
                    {
                        cp = LibraryComputers[i];
                        cp.IsOccupied = true;  //设置已被使用标记
                        break;
                    }
            }
            cp.Use(UserName.ToString());//使用计算机工作
            sp.Release();//不再使用计算机,让出来给其他人使用
        }

    注意:上述代码中不仅添加了锁,还将修改Computer对象占用标记的代码(原先在Computer.Use方法中)也移到这里来了。
    以下修改后的Computer.Use()方法:

    public  void Use(String userName)
    {
        Console.WriteLine("{0}开始使用计算机{1}", userName,ComputerName);
        Thread.Sleep(new Random().Next(1, 2000)); //随机休眠,以模拟人使用计算机
        Console.WriteLine("{0}结束使用计算机{1}", userName,ComputerName);
        IsOccupied = false; 
    }

    最后留给读者一个思考题:
    Computer.Use()方法中修改IsOccupied字段的那句代码需要锁定本对象吗?


============================================================================
   


附注:
    此示例程序中的BUG由CSDN网友“笑傲江湖 ”发现,是本人在设计示例时的疏漏,在此特别感谢“笑傲江湖" 网友的热心!也欢迎各位读者或业界朋友多指正本人拙著中的错误,互相探讨技术,共同进步。

我的CSDN博客:http://blog.csdn.net/bitfan我的新浪微博:http://t.sina.com.cn/jinxuliang