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

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

一:缘由

1. 业务背景

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

        private static List<long> ExecuteFilterList(int shopID, List<MemoryCacheTrade> trades, List<FilterConditon> filterItemList, MatrixSearchContext searchContext){var customerIDList = new List<long>();var index = 0;Parallel.ForEach(filterItemList, new ParallelOptions() { MaxDegreeOfParallelism = 4 },(filterItem) =>{var context = new FilterItemContext(){StartTime = searchContext.StartTime,EndTime = searchContext.EndTime,ShopID = shopID,Field = filterItem.Field,FilterType = filterItem.FilterType,ItemList = filterItem.FilterValue,SearchList = trades.ToList()};var smallCustomerIDList = context.Execute();lock (filterItemList){if (index == 0){customerIDList.AddRange(smallCustomerIDList);index++;}else{customerIDList = customerIDList.Intersect(smallCustomerIDList).ToList();}}});return customerIDList;}

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

2. 问题分析

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

3. 测试案例

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

        public static void Main(string[] args){var filterItemList = new List<string>() { "conditon1", "conditon2", "conditon3", "conditon4", "conditon5", "conditon6" };ParallelTest1(filterItemList);}public static void ParallelTest1(List<string> filterItemList){var totalCustomerIDList = new List<int>();bool isfirst = true;Parallel.ForEach(filterItemList, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, (query) =>{var smallCustomerIDList = GetCustomerIDList(query);lock (filterItemList){if (isfirst){totalCustomerIDList.AddRange(smallCustomerIDList);isfirst = false;}else{totalCustomerIDList = totalCustomerIDList.Intersect(smallCustomerIDList).ToList();}Console.WriteLine($"{DateTime.Now} 被锁了");}});Console.WriteLine($"最后交集客户ID:{string.Join(",", totalCustomerIDList)}");}public static List<int> GetCustomerIDList(string query){var dict = new Dictionary<string, List<int>>(){["conditon1"] = new List<int>() { 1, 2, 4, 7 },["conditon2"] = new List<int>() { 1, 4, 6, 7 },["conditon3"] = new List<int>() { 1, 4, 5, 7 },["conditon4"] = new List<int>() { 1, 2, 3, 7 },["conditon5"] = new List<int>() { 1, 2, 4, 5, 7 },["conditon6"] = new List<int>() { 1, 3, 4, 7, 9 },};return dict[query];}------ output ------
2020/04/21 15:53:34 被锁了
2020/04/21 15:53:34 被锁了
2020/04/21 15:53:34 被锁了
2020/04/21 15:53:34 被锁了
2020/04/21 15:53:34 被锁了
2020/04/21 15:53:34 被锁了
最后交集客户ID:1,7

二:第一次优化

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


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,过会说一下什么意思,先看修改后的代码体会一下

public static void ParallelTest2(List<string> filterItemList){var totalCustomerIDList = new List<int>();var isfirst = true;Parallel.ForEach<string, List<int>>(filterItemList,new ParallelOptions() { MaxDegreeOfParallelism = 2 },() => { return null; },(query, loop, index, smalllist) =>{var smallCustomerIDList = GetCustomerIDList(query);if (smalllist == null) return smallCustomerIDList;return smalllist.Intersect(smallCustomerIDList).ToList();},(finalllist) =>{lock (filterItemList){if (isfirst){totalCustomerIDList.AddRange(finalllist);isfirst = false;}else{totalCustomerIDList = totalCustomerIDList.Intersect(finalllist).ToList();}Console.WriteLine($"{DateTime.Now} 被锁了");}});Console.WriteLine($"最后交集客户ID:{string.Join(",", totalCustomerIDList)}");}------- output ------
2020/04/21 16:11:46 被锁了
2020/04/21 16:11:46 被锁了
最后交集客户ID:1,7
Press any key to continue . . .

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

三:第二次优化

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

public static void ParallelTest3(List<string> filterItemList){var totalCustomerIDList = new List<int>();var tasks = new Task<List<int>>[filterItemList.Count];for (int i = 0; i < filterItemList.Count; i++){tasks[i] = Task.Factory.StartNew((query) =>{return GetCustomerIDList(query.ToString());}, filterItemList[i]);}Task.WaitAll(tasks);for (int i = 0; i < tasks.Length; i++){var smallCustomerIDList = tasks[i].Result;if (i == 0){totalCustomerIDList.AddRange(smallCustomerIDList);}else{totalCustomerIDList = totalCustomerIDList.Intersect(smallCustomerIDList).ToList();}}Console.WriteLine($"最后交集客户ID:{string.Join(",", totalCustomerIDList)}");}------ output -------最后交集客户ID:1,7
Press any key to continue . . .

四:总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310371.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

常用Arthas命令

jad反编译 检查线上代码是否修改成功&#xff0c;例如修改interface后看Jar包是否引入新的&#xff0c;或者代码是否最新的。 jad com.zhenai.counseling.business.provider.facade.supremecourse.RedeemRecordFacadeImpl //反编译只展示源码 jad --source-only com.zhenai.c…

关于分布式锁的面试题都在这里了

「我今天班儿都没上&#xff0c;就为了赶紧把这篇文章分布式锁早点写完。我真的不能再贴心了。」「边喝茶边构思&#xff0c;你们可不要白嫖了&#xff01;三连来一遍&#xff1f;」引言为什么要学习分布式锁&#xff1f;最简单的理由就是作为一个社招程序员&#xff0c;面试的…

Git 15周年:当年的分道扬镳,成就了今天的开源传奇

4 月 7 日&#xff0c;全球最主流的版本控制系统 —— Git 迎来 15 周年纪念日&#xff0c;项目主要维护者 Junio C Hamano&#xff08;滨野 纯&#xff09; 先生发邮件庆祝了这一日子。我们知道&#xff0c;所有的软件项目在整个生命周期中都要经过不断迭代&#xff0c;在一个…

使用 docker 编译运行 abp 项目

在前面的两篇文章中&#xff0c;介绍了如何在华为鲲鹏架构及其Euler系统上运行dotnet core, 使用docker运行了默认的mvc模板项目&#xff0c;这篇文章继续介绍在docker中运行更复杂的dotnet core项目&#xff0c;这里以业内鼎鼎大名的abp vnext框架&#xff0c;版本 2.6 为例。…

数据结构与算法--数组中的逆序对

题目&#xff1a;在数组中的两个数字如果签名一个数字大于后面的数组&#xff0c;则这两个数字组成一个逆序对。输入一个数组&#xff0c;求出这个数组中的逆序对的总数。 案例&#xff1a;输入数组{7,5&#xff0c;6,4}中一共有5个逆序对分别是{7,6}&#xff0c;{7,5}&#x…

用了这么多年的泛型,你对它到底有多了解?

现代程序员写代码没有人敢说自己没用过泛型&#xff0c;这个泛型模板T可以被任何你想要的类型替代&#xff0c;确实很魔法很神奇&#xff0c;很多人也习以为常了&#xff0c;但就是这么有趣的泛型T底层到底是怎么帮你实现的&#xff0c;不知道有多少人清楚底层玩法&#xff0c;…

数据结构与算法--两个链表中第一个公共节点

链表中第一个公共节点 公节点定义&#xff1a;同一个节点在两个链表中&#xff0c;并不是节点值相同题目&#xff1a;输入两个节点&#xff0c;找出他们的第一个公共节点&#xff0c;节点定义如需 /*** 链表元素节点** author liaojiamin* Date:Created in 12:17 2021/3/5*/ …

ASP.NET Core技术研究-全面认识Web服务器Kestrel

因为IIS不支持跨平台的原因&#xff0c;我们在升级到ASP.NET Core后&#xff0c;会接触到一个新的Web服务器Kestrel。相信大家刚接触这个Kestrel时&#xff0c;会有各种各样的疑问。今天我们全面认识一下ASP.NET Core的默认Web服务器Kestrel。一、初识Kestrel首先&#xff0c;K…

数据结构与算法--二叉堆(最大堆,最小堆)实现及原理

二叉堆&#xff08;最大堆&#xff0c;最小堆&#xff09;实现及原理 二叉堆与二叉查找树一样&#xff0c;堆也有两个性质&#xff0c;即结构性质和堆性质。和AVL树一样&#xff0c;对堆的一次操作必须到堆的所有性质都被满足才能终止&#xff0c;也就是我们每次对堆的操作都必…

Blazor WebAssembly 3.2.0 已在塔架就位 将发射新一代前端SPA框架

最美人间四月天&#xff0c;春光不负赶路人。在充满无限希望的明媚春天里&#xff0c;一路风雨兼程的.NET团队正奋力实现新的突破。根据计划&#xff0c;新一代基于WebAssembly 技术研发的前端SPA框架Blazor 将于5月19日在微软Build大会升空。目前&#xff0c;Blazor 的测试工作…

如何将 Azure 上的 Ubuntu 19.10 服务器升级到 20.04

点击上方蓝字关注“汪宇杰博客”导语Ubuntu 20.04 LTS 已经正式推出了。作为一名软粉&#xff0c;看到新版鲍叔毒瘤&#xff0c;我当然是激动万分&#xff0c;抱着批判的态度&#xff0c;第一时间很不情愿的更新了我的服务器。4月23日发布的 Ubuntu 20.04 是个 LTS 版。其 Linu…

我想快速给WPF程序添加托盘菜单

我想...1 简单要求&#xff1a;使用开源控件库在XAML中声明托盘菜单&#xff0c;就像给控件添加ContextMenu一样封装了常用命令&#xff0c;比如&#xff1a;打开主窗体、退出应用程序等TerminalMACS我在TerminalMACS中添加了托盘菜单&#xff0c;最终实现的托盘菜单效果&#…