在并行编程中,经常会遇到多线程间操作共享集合的问题,很多时候大家都很难逃避这个问题做到一种无锁编程状态,你也知道一旦给共享集合套上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<T>这种带有返回值的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的效率高,大家要结合自己的使用场景合理的使用和混合搭配。

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


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


我是如何一步步的在并行编程中将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. 聊聊用Selenium做自动化碰到了哪些坑?都是怎么解决的?

    本周我们的讨论话题是关于Selenium自动化: 话题:聊聊用Selenium做自动化碰到了哪些坑?都是怎么解决的? 话题描述:Selenium是大家做UI自动化用到的主流框架,在平时写脚本的过程中, ...

  2. JMeter入门介绍

    目录 概述 下载&安装 实战JMetetr 测试计划简述 准备测试计划 编写测试计划 录制测试脚本 执行性能测试 单机测试 分布式测试 分析测试报告 APDEX 响应时间和吞吐量统计 测试结果 ...

  3. OpenCV-Python 特征匹配 + 单应性查找对象 | 四十五

    目标 在本章节中,我们将把calib3d模块中的特征匹配和findHomography混合在一起,以在复杂图像中找到已知对象. 基础 那么我们在上一环节上做了什么?我们使用了queryImage,找到 ...

  4. 使用SpringCloud将单体迁移至微服务

    使用SpringBoot构建单体项目有一段时间了,准备对一个老项目重构时引入SpringCloud微服务,以此奠定后台服务能够应对未知的业务需求. 现在SOA架构下的服务管理面临很多挑战,比如面临一个 ...

  5. html前端之css基础

    CSS 属性导航: CSS 属性组 动画 背景 边框和轮廓 框 颜色 内容页的媒体属性 尺寸 盒子模型(新) 盒子模型(旧) 字体 内容生成 网格 超链接 线框 列表 外边距 字幕 多列 内边距 页面 ...

  6. jQuery学习笔记01

    1.jQuery介绍 1.1什么是jQuery ? jQuery,顾名思义,也就是JavaScript和查询(Query),它就是辅助JavaScript开发的js类库. 1.2 jQuery核心思想 ...

  7. Leetcode力扣45题 跳跃游戏 II

    原题目: 跳跃游戏 II 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 你的目标是使用最少的跳跃次数到达数组的最后一个位置. 示例: 输入: ...

  8. G1垃圾回收器

    垃圾回收器的发展历程 背景 01.G1解决的问题 G1垃圾回收器是04年正式提出,12开始正式支持,在17年作为JDK9默认的垃圾处理器. 在04年的时候,java程序堆的内存越来越大,从而导致程序中 ...

  9. Html 慕课园编程练习10-1

    23:10:25 2019-08-14 自己写的这个好丑.... 题目:利用之前我们学过的JavaScript知识,实现选项卡切换的效果. 效果图: (另外 这个动图是怎么插入的 用url就行 复制就 ...

  10. Java HashMap源码详解

    Java数据结构-HashMap 目录 Java数据结构-HashMap 1. HashMap 1.1 HashMap介绍 1.1.1 HashMap介绍 1.1.2 HashMap继承图 1.2 H ...