C#并发实战Parallel.ForEach使用

C#并发实战Parallel.ForEach使用前言:最近给客户开发一个伙食费计算系统,大概需要计算2000个人的伙食。需求是按照员工的预定报餐计划对消费记录进行检查,如有未报餐有刷卡或者有报餐没刷卡的要进行一定的金额扣减等一系列规则。一开始我的想

大家好,又见面了,我是你们的朋友全栈君。

     前言:最近给客户开发一个伙食费计算系统,大概需要计算2000个人的伙食。需求是按照员工的预定报餐计划对消费记录进行检查,如有未报餐有刷卡或者有报餐没刷卡的要进行一定的金额扣减等一系列规则。一开始我的想法比较简单,直接用一个for循环搞定,统计结果倒是没问题,但是计算出来太慢了需要7,8分钟。这样系统服务是报超时错误的,让人觉得有点不太爽。由于时间也不多就就先提交给用户使用了,后面逻辑又增加了,计算时间变长,整个计算一遍居然要将近10分钟了。这个对用户来说是能接收的(原来自己手算需要好几天呢),但是我自己接受不了,于是就开始优化了,怎么优化呢,用多线程呗。

     一提到多线程,最先想到的是Task了,毕竟.net4.0以上Task封装了很多好用的方法。但是Task毕竟是多开一些线程去执行任务,最后整合结果,这样可以快一些,但我想更加快速一些,于是想到了另外一个对象:Parallel。之前在维护代码是确实有遇到过别人写的Parallel.Invoke,只是指定这个函数的作用是并发执行多项任务,如果遇到多个耗时的操作,他们之间又不贡献变量这个方法不错。我的情况是要并发执行一个集合,于是就用了List.ForAll 这个方法其实是拓展方法,完整的调用为:List.AsParallel().ForAll,需要先转换成支持并发的集合,等同于Parallel.ForEach,目的是对集合里面的元素并发执行一系列操作。

     于是乎,把原来的foreach换成了List.AsParallel().ForAll,运行起来,果然速度惊人,不到两分钟就插入结果了,但最后却是报主键重复的错误,这个错误的原因是,由于使用了并发,这个时候变量自增,其实是在强着自增,当多个线程同时获取到了id值,都去自增然后就重复了,举个例子如下:

            int num = 1;
            List<int> list = new List<int>();
            for (int i = 1; i <= 2000; i++)
            {
                list.Add(i);
            }
            Console.WriteLine($"num初始值为:" + num.ToString());
            list.AsParallel().ForAll(n =>
            {
                num++;
            });
            Console.WriteLine($"不加锁,并发{list.Count}次后为:" + num.ToString());
            Console.ReadKey();

这段代码是让一个变量执行2000次自增,正常结果应该是2001,但实际结果如下:

<span role="heading" aria-level="2">C#并发实战Parallel.ForEach使用

有经验的同学,立马能想到需要加锁了,C#内置了很多锁对象,如lock 互斥锁,Interlocked 内部锁,Monitor 这几个比较常见,lock内部实现其实就是使用了Monitor对象。对变量自增,Interlocked对象提供了,变量自增,自减、或者相加等方法,我们使用自增方法Interlocked.Increment,函数定义为:int Increment(ref int num),该对象提供原子性的变量自增操作,传入目标数值,返回或者ref num都是自增后的结果。 在之前的基础上我们增加一些代码:

           num = 1;
            Console.WriteLine($"num初始值为:" + num.ToString());
            list.AsParallel().ForAll(n =>
            {
                Interlocked.Increment(ref num);
            });
            Console.WriteLine($"使用内部锁,并发{list.Count}次后为:" + num.ToString());
            Console.ReadKey();

我们来看运行结果:

<span role="heading" aria-level="2">C#并发实战Parallel.ForEach使用

加了锁之后ID重复算是解决了,其实别高兴太早,由于正常的环境有了ID我们还有用这些ID来构建对象呢,于是又写了写代码,用集合来添加这些ID,为了更真实的模拟生产环境,我在forAll里面又加了一层循环代码如下:

            num = 1;
            Random random = new Random();
            var total = 0;
            var m = new ConcurrentBag<int>();
            list.AsParallel().ForAll(n =>
            {
                var c = random.Next(1, 50);
                Interlocked.Add(ref total, c);
                for (int i = 0; i < c; i++)
                {
                    Interlocked.Increment(ref num);
                    m.Add(num);
                }
            });
            Console.WriteLine($"使用内部锁,并发+内部循环{list.Count}次后为:" + num.ToString());
            Console.WriteLine($"实际值为:{total + 1}");
            var l = m.GroupBy(n => n).Where(o => o.Count() > 1);
            Console.WriteLine($"并发里面使用安全集合ConcurrentBag添加num,集合重复值:{l.Count()}个");
            Console.ReadKey();

<span role="heading" aria-level="2">C#并发实战Parallel.ForEach使用

上面的代码里面我用到了线程安全集合ConcurrentBag<T>它的命名空间是:using System.Collections.Concurrent,尽管使用了线程安全集合,但是在并发面前仍然是不安全的,到了这里其实比较郁闷了,自增加锁,安全集合内部应该也使用了锁,但还是重复了。有点说不过去了,想想多线程执行时有个上下文对象,即当多个线程同时执行任务,共享了变量他们一开始传进去的对象数值应该是相同的,由于变量自增时加了锁,所以ID是不会重复了。我猜测问题应该出在Add方法了,就是说当num值自增后还没有来得及传出去就已经执行了Add方法,故添加了重复变量。于是乎,我重新写了段代码,让ID自增和集合添加都放到锁里面:

            num = 1;
            total = 0;
            using (var q = new BlockingCollection<int>())
            {
                list.AsParallel().ForAll(n =>
                {
                    var c = random.Next(1, 50);
                    Interlocked.Add(ref total, c);
                    for (int i = 0; i < c; i++)
                    {
                        
                       // Task.Delay(100);
                        q.Add(Interlocked.Increment(ref num));
                        
                        //可控
                        //lock (objLock)
                        //{
                        //    num++;
                        //    q.Add(num);
                        //}
                    }

                });
                q.CompleteAdding();
                Console.WriteLine($"num累计值为:{total},并发之后值为:{num}");
                var x = q.GroupBy(n => n).Where(o => o.Count() > 1);
                Console.WriteLine($"并发使用安全集合BlockingCollection+Interlocked添加num,集合重复值:{x.Count()}个");
                Console.ReadKey();
            }

这里我测试了另外一个线程安全的集合BlockingCollection,关于这个集合的使用请自行查找MSDN文档,上面的关键代码直接添加安全集合的返回值,可以保证集合不会重复,但其实下面的lock更适用与正式环境,因为我们添加的一般都是对象不会是基础类型数值,运行结果如下:

<span role="heading" aria-level="2">C#并发实战Parallel.ForEach使用

至此,我们的问题解决了,计算时间由原来的9分多降至110秒左右,可见Parallel的处理还是很给力的,唯一不足的是,很占CPU,执行计算后CPU达到了88%。附上计算结果:

<span role="heading" aria-level="2">C#并发实战Parallel.ForEach使用

优化前后对比

<span role="heading" aria-level="2">C#并发实战Parallel.ForEach使用

 

      总结:C#安全集合在并发的情况下其实不一定是安全的,还是需要结合实际应用场景和验证结果为准。Parallel.ForEach在对循环数量可观的情况下是可以去使用的,如果有共享变量,一定要配合锁做同步处理。还是得慎用这个方法,如果方法内部有操作数据库的记得增加事务处理,否则就呵呵了。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/154878.html原文链接:https://javaforall.net

(0)
上一篇 2022年7月4日 上午10:00
下一篇 2022年7月4日 上午10:00


相关推荐

  • 天天网摘——文摘_书签_网址_收藏夹

    天天网摘——文摘_书签_网址_收藏夹http www 365key com Private

    2026年3月20日
    2
  • 什么是文本挖掘 ?「建议收藏」

    什么是文本挖掘 ?「建议收藏」什么是文本挖掘  文本挖掘是抽取有效、新颖、有用、可理解的、散布在文本文件中的有价值知识,并且利用这些知识更好地组织信息的过程。1998年底,国家重点研究发展规划首批实施项目中明确指出,文本挖掘是“图像、语言、自然语言理解与知识挖掘”中的重要内容。  文本挖掘是信息挖掘的一个研究分支,用于基于文本信息的知识发现。文本挖掘利用智能算法,如神经网络、基于案例的推理、可能性推理等,并结合文字处

    2022年6月17日
    29
  • acwing-171. 送礼物(双向dfs+打标+二分)

    acwing-171. 送礼物(双向dfs+打标+二分)达达帮翰翰给女生送礼物,翰翰一共准备了 N 个礼物,其中第 i 个礼物的重量是 G[i]。达达的力气很大,他一次可以搬动重量之和不超过 W 的任意多个物品。达达希望一次搬掉尽量重的一些物品,请你告诉达达在他的力气范围内一次性能搬动的最大重量是多少。输入格式第一行两个整数,分别代表 W 和 N。以后 N 行,每行一个正整数表示 G[i]。输出格式仅一个整数,表示达达在他的力气范围内一次性能搬动的最大重量。数据范围1≤N≤46,1≤W,G[i]≤231−1输入样例:20 5754

    2022年8月8日
    10
  • ESLint简介

    ESLint简介一ESLint简介ESLint是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。不管是多人合作还是个人项目,代码规范是很重要的。这样做不仅可以很大程度地避免基本语法错误,也保证了代码的可读性。这所谓工欲善其事,必先利其器,推荐ESLint+vscode来写vue,有种飞一般的感觉。每次保存,vscode就能标红不符合ESLint规则的地方,同时还会做一些简单的自我修正。二启用ESLint1ESLint插件安装vscode的ESLint插件,

    2022年6月18日
    34
  • hue 安装笔记

    hue 安装笔记本文主要记录 hue 的 yum 源下的安装与配置 Hue 集成 Hdfs Hive Impala Yarn Kerberos LDAP Sentry Solr 等 nbsp nbsp 集群情况 192 168 211 178 HA active nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp 192 168 211 179 datanode nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp 192 168 211 180

    2026年2月9日
    3
  • Windows 下使用 Mingw32-make 来执行 Makefile示例[通俗易懂]

    文章目录先下载Mingw设置好环境变量确认安装环境状态gccmingw32-makeC工程测试main.cmath.ccall_math.c准备好Makefile文件执行Makefile执行前执行后运行main.exe执行Makefileclean清理文件执行前执行后整体运行演示GIFReferernces先下载Mingw这里使用的是mingw32不是64的,需要64位的自行搜索下载32位的可以参考我之前一篇的:C-BookNote-Win开发环境设置

    2022年4月8日
    173

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号