在并行编程中,经常会遇到多线程间操作共享集合的问题,很多时候大家都很难逃避这个问题做到一种无锁编程状态,你也知道一旦给共享集合套上lock之后,并发和伸缩能力往往会造成很大影响,这篇就来谈谈如何尽可能的减少lock锁次数甚至没有。

一:缘由

1. 业务背景

昨天在review代码的时候,看到以前自己写的这么一段代码,精简后如下:

  1. private static List<long> ExecuteFilterList(int shopID, List<MemoryCacheTrade> trades, List<FilterConditon> filterItemList, MatrixSearchContext searchContext)
  2. {
  3. var customerIDList = new List<long>();
  4. var index = 0;
  5. Parallel.ForEach(filterItemList, new ParallelOptions() { MaxDegreeOfParallelism = 4 },
  6. (filterItem) =>
  7. {
  8. var context = new FilterItemContext()
  9. {
  10. StartTime = searchContext.StartTime,
  11. EndTime = searchContext.EndTime,
  12. ShopID = shopID,
  13. Field = filterItem.Field,
  14. FilterType = filterItem.FilterType,
  15. ItemList = filterItem.FilterValue,
  16. SearchList = trades.ToList()
  17. };
  18. var smallCustomerIDList = context.Execute();
  19. lock (filterItemList)
  20. {
  21. if (index == 0)
  22. {
  23. customerIDList.AddRange(smallCustomerIDList);
  24. index++;
  25. }
  26. else
  27. {
  28. customerIDList = customerIDList.Intersect(smallCustomerIDList).ToList();
  29. }
  30. }
  31. });
  32. return customerIDList;
  33. }

这段代码实现的功能是这样的,filterItemList承载着所有原子化的筛选条件,然后用多线程的形式并发执行里面的item,最后将每个item获取的客户人数集合在高层进行整体求交,画个简图就是下面这样。

2. 问题分析

其实这代码存在着一个很大的问题,在Parallel中直接使用lock锁的话,filterItemList有多少个,我的lock就会锁多少次,这对并发和伸缩性是有一定影响的,现在就来想想怎么优化吧!

3. 测试案例

为了方便演示,我模拟了一个小案例,方便大家看到实时结果,修改后的代码如下:

  1. public static void Main(string[] args)
  2. {
  3. var filterItemList = new List<string>() { "conditon1", "conditon2", "conditon3", "conditon4", "conditon5", "conditon6" };
  4. ParallelTest1(filterItemList);
  5. }
  6. public static void ParallelTest1(List<string> filterItemList)
  7. {
  8. var totalCustomerIDList = new List<int>();
  9. bool isfirst = true;
  10. Parallel.ForEach(filterItemList, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, (query) =>
  11. {
  12. var smallCustomerIDList = GetCustomerIDList(query);
  13. lock (filterItemList)
  14. {
  15. if (isfirst)
  16. {
  17. totalCustomerIDList.AddRange(smallCustomerIDList);
  18. isfirst = false;
  19. }
  20. else
  21. {
  22. totalCustomerIDList = totalCustomerIDList.Intersect(smallCustomerIDList).ToList();
  23. }
  24. Console.WriteLine($"{DateTime.Now} 被锁了");
  25. }
  26. });
  27. Console.WriteLine($"最后交集客户ID:{string.Join(",", totalCustomerIDList)}");
  28. }
  29. public static List<int> GetCustomerIDList(string query)
  30. {
  31. var dict = new Dictionary<string, List<int>>()
  32. {
  33. ["conditon1"] = new List<int>() { 1, 2, 4, 7 },
  34. ["conditon2"] = new List<int>() { 1, 4, 6, 7 },
  35. ["conditon3"] = new List<int>() { 1, 4, 5, 7 },
  36. ["conditon4"] = new List<int>() { 1, 2, 3, 7 },
  37. ["conditon5"] = new List<int>() { 1, 2, 4, 5, 7 },
  38. ["conditon6"] = new List<int>() { 1, 3, 4, 7, 9 },
  39. };
  40. return dict[query];
  41. }
  42. ------ output ------
  43. 2020/04/21 15:53:34 被锁了
  44. 2020/04/21 15:53:34 被锁了
  45. 2020/04/21 15:53:34 被锁了
  46. 2020/04/21 15:53:34 被锁了
  47. 2020/04/21 15:53:34 被锁了
  48. 2020/04/21 15:53:34 被锁了
  49. 最后交集客户ID:1,7

二:第一次优化

从结果中可以看到,filterItemList有6个,锁次数也是6次,那如何降低呢? 其实实现Parallel代码的FCL大神也考虑到了这个问题,从底层给了一个很好的重载,如下所示:


  1. public static ParallelLoopResult ForEach<TSource, TLocal>(OrderablePartitioner<TSource> source, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<TSource, ParallelLoopState, long, TLocal, TLocal> body, Action<TLocal> localFinally);

这个重载很特别,多了两个参数localInit和localFinally,过会说一下什么意思,先看修改后的代码体会一下


  1. public static void ParallelTest2(List<string> filterItemList)
  2. {
  3. var totalCustomerIDList = new List<int>();
  4. var isfirst = true;
  5. Parallel.ForEach<string, List<int>>(filterItemList,
  6. new ParallelOptions() { MaxDegreeOfParallelism = 2 },
  7. () => { return null; },
  8. (query, loop, index, smalllist) =>
  9. {
  10. var smallCustomerIDList = GetCustomerIDList(query);
  11. if (smalllist == null) return smallCustomerIDList;
  12. return smalllist.Intersect(smallCustomerIDList).ToList();
  13. },
  14. (finalllist) =>
  15. {
  16. lock (filterItemList)
  17. {
  18. if (isfirst)
  19. {
  20. totalCustomerIDList.AddRange(finalllist);
  21. isfirst = false;
  22. }
  23. else
  24. {
  25. totalCustomerIDList = totalCustomerIDList.Intersect(finalllist).ToList();
  26. }
  27. Console.WriteLine($"{DateTime.Now} 被锁了");
  28. }
  29. });
  30. Console.WriteLine($"最后交集客户ID:{string.Join(",", totalCustomerIDList)}");
  31. }
  32. ------- output ------
  33. 2020/04/21 16:11:46 被锁了
  34. 2020/04/21 16:11:46 被锁了
  35. 最后交集客户ID:1,7
  36. Press any key to continue . . .

很好,这次优化将lock次数从6次降到了2次,这里我用了 new ParallelOptions() { MaxDegreeOfParallelism = 2 } 设置了并发度为最多2个CPU核,程序跑起来后会开两个线程,将一个大集合划分为2个小集合,相当于1个集合3个条件,第一个线程在执行3个条件的起始处会执行你的localInit函数,在3个条件迭代完之后再执行你的localFinally,第二个线程也是按照同样方式执行自己的3个条件,说的有点晦涩,画一张图说明吧。

三: 第二次优化

如果你了解Task<T>这种带有返回值的Task,这就好办了,多少个filterItemList就可以开多少个Task,反正Task底层是使用线程池承载的,所以不用怕,这样就完美的实现无锁编程。


  1. public static void ParallelTest3(List<string> filterItemList)
  2. {
  3. var totalCustomerIDList = new List<int>();
  4. var tasks = new Task<List<int>>[filterItemList.Count];
  5. for (int i = 0; i < filterItemList.Count; i++)
  6. {
  7. tasks[i] = Task.Factory.StartNew((query) =>
  8. {
  9. return GetCustomerIDList(query.ToString());
  10. }, filterItemList[i]);
  11. }
  12. Task.WaitAll(tasks);
  13. for (int i = 0; i < tasks.Length; i++)
  14. {
  15. var smallCustomerIDList = tasks[i].Result;
  16. if (i == 0)
  17. {
  18. totalCustomerIDList.AddRange(smallCustomerIDList);
  19. }
  20. else
  21. {
  22. totalCustomerIDList = totalCustomerIDList.Intersect(smallCustomerIDList).ToList();
  23. }
  24. }
  25. Console.WriteLine($"最后交集客户ID:{string.Join(",", totalCustomerIDList)}");
  26. }
  27. ------ output -------
  28. 最后交集客户ID:1,7
  29. Press any key to continue . . .

四:总结

我们将原来的6个lock优化到了无锁编程,但并不说明无锁编程就一定比带有lock的效率高,大家要结合自己的使用场景合理的使用和混合搭配。

好了,本篇就说到这里,希望对您有帮助。


如您有更多问题与我互动,扫描下方进来吧~


我是如何一步步的在并行编程中将lock锁次数降到最低实现无锁编程的更多相关文章

  1. 【Java并发编程】9、非阻塞同步算法与CAS(Compare and Swap)无锁算法

    转自:http://www.cnblogs.com/Mainz/p/3546347.html?utm_source=tuicool&utm_medium=referral 锁(lock)的代价 ...

  2. 我是如何一步步裹挟老板从.net 转到 java 阵营的

    我是如何一步步裹挟老板从.net 转到 java 阵营的 仅记录从 .net(C#) 转到 java 的一些心路历程 时间点跨度 2016 — 2017 一.前 xx 公司同事群的一次聊天 前公司同事 ...

  3. SQL注入—我是如何一步步攻破一家互联网公司的

    最近在研究Web安全相关的知识,特别是SQL注入类的相关知识.接触了一些与SQL注入相关的工具.周末在家闲着无聊,想把平时学的东东结合起来攻击一下身边某个小伙伴去的公司,看看能不能得逞.不试不知道,一 ...

  4. Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) JAVA日志的前世今生 .NET MVC采用SignalR更新在线用户数 C#多线程编程系列(五)- 使用任务并行库 C#多线程编程系列(三)- 线程同步 C#多线程编程系列(二)- 线程基础 C#多线程编程系列(一)- 简介

    Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) 一.前言 由于本篇文章较长,所以下面给出内容目录方便跳转阅读,当然也可以用博客页面最右侧的文章目录导航栏进行跳转查阅. 一.前言 ...

  5. 我是如何一步步编码完成万仓网ERP系统的(一)系统架构

    https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...

  6. 我是如何一步步编码完成万仓网ERP系统的(二)前端框架

    https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...

  7. 我是如何一步步编码完成万仓网ERP系统的(三)登录

    https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...

  8. 我是如何一步步编码完成万仓网ERP系统的(四)登录的具体实现

    https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...

  9. 我是如何一步步编码完成万仓网ERP系统的(五)产品库设计 1.产品类别

    https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...

随机推荐

  1. 洛谷 P1047 校门外的树 题解

    Case 1. 本题其实不难,直接模拟就可以了.时间复杂度: \(O(L \times M)\) Case 2. 考虑一个简单的增强:把原来的: \[L \leq 10^4,M \leq 10^2 \ ...

  2. 我是如何用IDEA调试BUG的?

    最近小明的bug有点多,忙的连王者荣耀都顾不上玩了,导致现在不得不抽点时间研究一下作为当前大多Java程序员开发工具的IDEA DEBUG功能,以提高效率. 一.条件断点 场景:我们在遍历某个集合,期 ...

  3. 【bug】table重新加载数据,页面滚动条下沉到底部,记录scrollTop后将其恢复scrollTop出现闪烁

    1.table数据请求前记录scrollTop $scope.scrollPos = document.documentElement.scrollTop; 2.html中添加指令repeat-fin ...

  4. laravel中间件的创建思路分析

    网上有很多解析laravel中间件的实现原理,但是不知道有没有读者在读的时候不明白,作者是怎么想到要用array_reduce函数的? 本文从自己的角度出发,模拟了如果我是作者,我是怎么实现这个中间件 ...

  5. 关于Anaconda安装以后使用Jupyter Notebook无法直接打开浏览器的解决方法

    关于Anaconda安装以后使用Jupyter Notebook无法直接打开浏览器的解决方法 1.首先打开Anoconda Prompt,输入命令 jupyter notebook --generat ...

  6. shell脚本介绍以及常用命令

    Shell脚本 Shell Script,Shell脚本与Windows/Dos下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的 ...

  7. 在TensorFlow中实现文本分类的卷积神经网络

    在TensorFlow中实现文本分类的卷积神经网络 Github提供了完整的代码: https://github.com/dennybritz/cnn-text-classification-tf 在 ...

  8. TensorFlow系列专题(十三): CNN最全原理剖析(续)

    目录: 前言 卷积层(余下部分) 卷积的基本结构 卷积层 什么是卷积 滑动步长和零填充 池化层 卷积神经网络的基本结构 总结 参考文献   一.前言 上一篇我们一直说到了CNN[1]卷积层的特性,今天 ...

  9. 使用FME裁剪矢量shapefile文件

  10. Git入门操作(一)

    最近真正用到了Git,感觉还是需要好好整理一下最最基础用法,与萌新共享.^_^ 关于Git的基础介绍,这里不再赘述,下面撸代码了(主要是命令行的操作,属于linux操作系统的,可能没听过,但记住就好了 ...