我是如何一步步的在并行编程中将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<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锁次数降到最低实现无锁编程的更多相关文章
- 【Java并发编程】9、非阻塞同步算法与CAS(Compare and Swap)无锁算法
转自:http://www.cnblogs.com/Mainz/p/3546347.html?utm_source=tuicool&utm_medium=referral 锁(lock)的代价 ...
- 我是如何一步步裹挟老板从.net 转到 java 阵营的
我是如何一步步裹挟老板从.net 转到 java 阵营的 仅记录从 .net(C#) 转到 java 的一些心路历程 时间点跨度 2016 — 2017 一.前 xx 公司同事群的一次聊天 前公司同事 ...
- SQL注入—我是如何一步步攻破一家互联网公司的
最近在研究Web安全相关的知识,特别是SQL注入类的相关知识.接触了一些与SQL注入相关的工具.周末在家闲着无聊,想把平时学的东东结合起来攻击一下身边某个小伙伴去的公司,看看能不能得逞.不试不知道,一 ...
- Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) JAVA日志的前世今生 .NET MVC采用SignalR更新在线用户数 C#多线程编程系列(五)- 使用任务并行库 C#多线程编程系列(三)- 线程同步 C#多线程编程系列(二)- 线程基础 C#多线程编程系列(一)- 简介
Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) 一.前言 由于本篇文章较长,所以下面给出内容目录方便跳转阅读,当然也可以用博客页面最右侧的文章目录导航栏进行跳转查阅. 一.前言 ...
- 我是如何一步步编码完成万仓网ERP系统的(一)系统架构
https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...
- 我是如何一步步编码完成万仓网ERP系统的(二)前端框架
https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...
- 我是如何一步步编码完成万仓网ERP系统的(三)登录
https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...
- 我是如何一步步编码完成万仓网ERP系统的(四)登录的具体实现
https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...
- 我是如何一步步编码完成万仓网ERP系统的(五)产品库设计 1.产品类别
https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...
随机推荐
- [gcd,灵感] Codeforces 1200C Round Corridor
题目:https://codeforces.com/contest/1200/problem/C C. Round Corridor time limit per test 1 second memo ...
- HDU-6376 度度熊剪纸条
链接 http://acm.hdu.edu.cn/showproblem.php?pid=6376 分析 这道题好像不是很难,因为是要拼出前缀1,所以确定剪下每一段1需要的刀数,然后因为有次数限制,所 ...
- Python第十一章-常用的核心模块01-collections模块
python 自称 "Batteries included"(自带电池, 自备干粮?), 就是因为他提供了很多内置的模块, 使用这些模块无需安装和配置即可使用. 本章主要介绍 py ...
- iOS 页面流畅技巧(1)
一.屏幕显示图像原理 首先明确两个概念:水平同步信号.垂直同步信号. CRT 的电子枪按照上图中的方式,从上到下一行一行的扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次的扫描. ...
- yum-程序包管理器前端工具
一.要想使用yum先要指定yum源 /etc/yum.com /etc/yum.repos.d/*repo 一.yum的使用 yum [option] command 包名 option -y: c ...
- dp例题02. 滑雪问题 (poj1088)
poj1088滑雪问题 题目链接:http://poj.org/status Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激.可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得 ...
- supervisor 的使用 (fastcgi管理)
本文主要介绍 supervisor 对 fastcgi 进程的管理 fastcgi 进程的管理 在php 中,php-fpm 有主进程来管理和维护子进程的数量.但是并不是所有的服务都有类似的主进程来做 ...
- 在vue+elementUI项目中使用分页功能
HTML: <div class="mRttt"> <!-- 分页 --> <el-pagination ...
- JS数据结构与算法 - 二叉树(一)基本算法
仅供JavaScript刷题参考用. 二叉查找树和平衡二叉树 其它树:满二叉树.完全二叉树.完美二叉树.哈弗曼树.二叉查找树BST.平衡二叉树AVL 了解:红黑树,是一种特殊的二叉树.这种树可以进行高 ...
- const不同位置带来的区别
const不同位置带来的区别 今天同学问我数据结构时,我对以下代码懵了一下: template <class T> class Link{ public: T data; Link< ...