代码改变世界

抓虫系列(一) 从简单程序开始 线程安全

2011-10-19 12:03  熬夜的虫子  阅读(2722)  评论(12编辑  收藏  举报

简单的程序也可以存在很多值得思考的地方,作为一名程序员或者架构师,首先要具备的就是追根和追新的心态。抓虫系列的代码我想大部分人都接触过或者犯过这样的错误,有些可能涉及的知识面很基础很浅,留个烂文在此引导新手、路人。虫子尽量将问题放大,追的深一点偏一点,如果大家有其他自己的想法或者补充也可以留爪印。

PS一下:看了下面的评论,大家有点误会虫子的意思了,此系列的博文旨在抓虫,从抓虫中关注我们的程序中容易出现的问题而并非是找寻更佳的解决方案 嘿嘿~ ~

先看原始bug程序

class testObj
    {     
        public object Result { get; set; }
        public int index { get; set; }       
    }
 public void Test()
        {
            ManualResetEvent[] MR = new ManualResetEvent[20];

            testObj qq = new testObj();

            for (int i = 0; i < 20; i++)
            {
                MR[i] = new ManualResetEvent(false);
                qq.index = i;
                ThreadPool.QueueUserWorkItem(o =>
                {
                    Console.WriteLine(qq.index.ToString());
                    MR[qq.index].Set();

                }, qq);
            }
            WaitHandle.WaitAll(MR);
        }

我们的目的是让程序输出0~19。看到这里可能老鸟已经发现程序的问题了。新鸟应该还是继续查虫。老鸟们先卖个关子,虫子把问题引偏,这样好拓展更多的问题。让我们来看看运行结果。

当时我就震惊了... 虫子开始胡思乱想了 为嘛会是这样。堆栈问题?好吧巩固一下堆栈,值类型是放在栈中的,值类型的拷贝是深拷贝,当我们传递一个值类型参数时,栈上被分配好一个新的空间,然后该参数的值被拷贝到此空间中。应该不会产生这样的现象吧。  不对!!! int已经被封装在testObj里了,这个地方它是引用类型是浅拷贝。Oh,MyGirlFriend,发现了!这20个线程用的是同一个副本,(⊙o⊙)… 虫子在干什么,你让20个线程在同时操作同一个数据。

这是个问题,。上面所说是一个问题但是不是这个现象产生的唯一问题。在这个异步程序中,线程在获取对象资源时,那for循环再主线程中已经跑完了。所以qq的index一直是20。读到这里可能有些老鸟们已经开始嗤之以鼻,这种错误他们可不会犯。知道了原因我们就要来分析解决方案了。一层一层来剥,

首先看这段修补01号程序

public void Test()
        {
            ManualResetEvent[] MR = new ManualResetEvent[20];
            EventWaitHandle EH = new AutoResetEvent(false);
            testObj qq = new testObj();
            EH.Set();
            for (int i = 0; i < 20; i++)
            {
                MR[i] = new ManualResetEvent(false);
                qq.index = i;
                EH.WaitOne();
                ThreadPool.QueueUserWorkItem(o =>
                {
                    Console.WriteLine(qq.index.ToString());
                    MR[qq.index].Set();
                    EH.Set();
                }, qq);
            }
            WaitHandle.WaitAll(MR);
        }

我们加了AutoResetEvent,这是一个自动Reset的事件通知方式。形象点说,相当于各位经常使用的门禁系统。一开始处于wait状态,只有有人set了它才放行。这里用来控制线程一个一个来完成,也就是说每次对象只有一个人能访问。这下咱们的线程安全了吧,哈哈哈哈。再看效果图。

神马!!! 貌似看好多了,居然还有重复的看那1、1,19、19这是多么的不和谐啊。好吧,继续抓虫、(⊙o⊙)…又发现了 主线程虽然被门禁控制住了,但是主线程和异步线程的节奏还是不一样,运气好点可能你出的结果是正确的。但是主线程在第一次的时候可能已经pass掉了 但是第一个异步线程还没结束。(⊙o⊙)… 虫子你骗我们 这个方案根本不是解决这个问题的,我们的问题在于我们的程序用了同一个资源qq。

被你发现了!!!

好吧修补02号程序如下

public void Test()
        {
            ManualResetEvent[] MR = new ManualResetEvent[20];
            testObj qq = new testObj();
            for (int i = 0; i < 20; i++)
            {
                qq = new testObj();
                MR[i] = new ManualResetEvent(false);
                qq.index = i;
                ThreadPool.QueueUserWorkItem(o =>
                {
                    Console.WriteLine(qq.index.ToString());
                    MR[qq.index].Set();
                }, qq);
            }
            WaitHandle.WaitAll(MR);        
        }

长叹一口气,这下没问题了吧。嘿嘿,这次我可以为每个线程独立分配了一个对象。事实如此吗?老鸟们应该开始偷着笑了,看效果图

╮(╯▽╰)╭ 你又被忽悠了,这个问题关键的地方在第四行,貌似你每个形成用的都是独立的资源,其实你还是粗心了点。

下面上正解方案了

 public class cnBlog
    {
        public void Test()
        {
            ManualResetEvent[] MR = new ManualResetEvent[20];
            for (int i = 0; i < 20; i++)
            {
                testObj qq = new testObj();
                MR[i] = new ManualResetEvent(false);
                qq.index = i;
                ThreadPool.QueueUserWorkItem(o =>
                {
                    Console.WriteLine(qq.index.ToString());
                    MR[qq.index].Set();
                }, qq);
            }
            WaitHandle.WaitAll(MR);        
        }
    }

和之前的方案只是将一行程序换了个地方,但是意义完全改变了,让我们看看真正的结果是如何。

oh yeah~ ~ 虽然排序不理想 正常!

题后话就不多说了,希望抓虫能给大家带来一些微不足道的收获。