zoukankan      html  css  js  c++  java
  • .Net 4.0并行库实用性演练(续)

      接着上一次说,即使用了新的线程安全的集合BlockingCollection,这段代码还是会有问题。

        static void testFillParallel()
        {
            var list = new BlockingCollection<Person>(9999);
            Enumerable.Range(1, 99999).AsParallel().ForAll(n =>
            {
                var name = "Person " + n % 9;
                if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });
            });
            Console.WriteLine("Person's count is {0}", list.Count);
        }
    

      代码逻辑就是根据序号生成一个名字,并将名字不重复的人加入集合中。显然,最后集合中应该有9个人,应该很简单吧。执行一下,请看结果:

      可见,绝大多数结果都是正确,只有两次执行出现了异常,在正式系统运行中,这可是要命的两条!难道是BlockingCollection的问题吗?细想不太可能,微软怎么也不会漏过这么明显的设计问题,实际上是自己对线程安全认识不准确。集合元素之所以偶尔会多一个,是这样的情况:线程A抢先一步,占住集合判断是否存在这个人名,线程B被BlockingCollection拦在外面;A发现集合中查无此人,正想加一个,然而不知怎么回事,它没马上继续,就好像龟兔赛跑,兔子要到终点了,做了个白日梦,梦一醒自己成了老二。B趁机赶上,正好锁也解了,碰巧它查的人也没有,一鼓作气跑完全程。这时A跑了一大半,岂肯甘心,赖皮着到终点,不管三七二十一,有没有和B刚才加的重复,硬把自己塞了进去。

      这样理解,也容易解释,第一次执行出现异常结果概率很高,因为开始时线程间步调几乎完全一致,刚才故事的前半段最有可能上演。另外条件判断时间越久,异常结果出现概率越小,比如把name = "Person " + n % 9改成name = "Person " + n % 50,这时A就是做白日梦,B也全力追赶,无奈前面被落后太多,一千次中只有一两次结果异常。

      其实System.Collections.ConCurrent命名空间底下的类,只对多线程环境下的某次访问保证健壮性,却不能保证多线程下,作为业务对象的业务操作的准确性,实际上也无法做到。然而话说回来,我们使用这些类,是让它们在凶险的多线程战场上,为我们奋勇杀敌,而不是明哲保身的。如果业务出错了,仗都打输了,集合再线程安全亦无济于事。所以,不要让线程安全误导我们,要着眼在业务上,业务安全实现了,自然线程安全自然不在话下。

      现在要解决结果异常,自然又想到了老办法—上锁,先保证万无一失。显然,并行集合也不必上场了,还是用List。为了接近真实场景,将集合最大元素数目提高到999,这样在判断新元素是否重复是要花较多时间,取代Thread.Sleep(1),代码如下:

        static void testFillParallel()
        {
            var list = new List<Person>(999);
            var L = new object();
            Enumerable.Range(1, 9999).AsParallel().ForAll(n =>
            {
                var name = "Person " + n % 999;
                lock (L)
                {
                    if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });
                }
            });
            Console.WriteLine("Person's count is {0}", list.Count);
        }
    

      测试结果如下:

     次数

     1

     2

     3

     4

     Fill 方法

     304

     292

     291

     292

     FillParallel 方法

     340

     298

     296

     297

      可见,虽然运用并行方式,也保证每个迭代有一定的执行时间,虽然加锁可以保证结果正确,但大多数时候,只有一个线程能进行工作,其他线程再多也只能等待,实际上还不如单线程,也失去了并行运算的意义。

      学习.Net4.0 的并行库并非我们的目的,我们目的是解决现实的问题。解决关键,还是在锁上。锁是把双刃剑,我们要让线程占用锁时间尽可能短。在填加集合时,加锁没办法避免,但只是为遍历集合而加锁,好像是种浪费。根据三级锁协议,应该允许多个只读操作同时进行,可是实现IEnumerable的集合,都不允许在遍历访问时修改集合,既然如此,我们自己搞一个集合副本,只作判断用,如果原集合更新,副本也随之更新,不是就能解决同时遍历的问题了吗?

      试验代码:

        static void testFill()
        {
            var list = new List<Person>(999);
            var L = new object();
            Enumerable.Range(1, 9999).ToList().ForEach(n =>
            {
                var name = "Person " + n % 999;
                if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });
            });
            Console.WriteLine("Person's count is {0}", list.Count);
        }
    
        static void testFillParallel()
        {
            var list = new List<Person>(999);
            var L = new object();
            var resultCopy = new Person[999];
            bool hasNewMember = false;
            Enumerable.Range(1, 9999).AsParallel().ForAll(n =>
            {
                var name = "Person " + n % 999;
                if (resultCopy.Count(p => p!=null && p.Name == name) < 1)
                {
                    lock (L)
                    {
                        // 如果有新成员,要再判断一遍
                        if (hasNewMember)
                        {
                            if (resultCopy.Count(p => p != null && p.Name == name) > 0) return;
                            hasNewMember = false;
                        }
                        list.Add(new Person { Id = n, Name = name });
                        list.CopyTo(resultCopy);
                        hasNewMember = true;
                    }
                }
            });
            Console.WriteLine("Person's count is {0}", list.Count);
        }
    

      试验结果(单位ms):

     次数

     1

     2

     3

     4

     Fill 方法(和上次一样)

     309

     291

     292

     292

     FillParallel 方法

     210

     166

     165

     166

      看来,自己总算写了第一段能发挥并行运算的代码。

      当然,最后代码还有很多可改进的地方,比如list.CopyTo方法,还有发现有新成员后第二次判断,向让性能与CPU核数正比的目标努力。

      还有,那个if(...){ lock(...) { if(...) { 的语句,跟单例模式的一种普遍实现很像,单例模式也是多线程环境下一种设计模式,也许其中有些异曲同工之处吧。

        static void testFillParallel ()

        {

            var list = new BlockingCollection<Person>(9999);

            Enumerable.Range(1, 9999).AsParallel().ForAll(n =>

            {

                var name = "Person " + n % 9;

                if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });

            });

            Console.WriteLine("Person's count is {0}", list.Count);

        }

  • 相关阅读:
    HDU 3951 (博弈) Coin Game
    HDU 3863 (博弈) No Gambling
    HDU 3544 (不平等博弈) Alice's Game
    POJ 3225 (线段树 区间更新) Help with Intervals
    POJ 2528 (线段树 离散化) Mayor's posters
    POJ 3468 (线段树 区间增减) A Simple Problem with Integers
    HDU 1698 (线段树 区间更新) Just a Hook
    POJ (线段树) Who Gets the Most Candies?
    POJ 2828 (线段树 单点更新) Buy Tickets
    HDU 2795 (线段树 单点更新) Billboard
  • 原文地址:https://www.cnblogs.com/XmNotes/p/1822498.html
Copyright © 2011-2022 走看看