无锁队列先大致介绍一下无锁队列。无锁队列的根本是cas函数——compareandswap,即比较并交换,函数功能可以用c++函数来说明:
int compare_and_swap (int* reg, int oldval, int newval)
{
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
return old_reg_val;
}
它将reg的值与oldval的值进行对比,若相同则将reg赋新值。注意以上操作是原子操作。大部分语言都有提供cas支持,不过函数原型可能有些微的不同,许多语言(包括go)中cas的返回值是标识是否赋值成功的bool值。
无锁队列则是以cas来实现同步的一种队列,我的具体实现这里就不贴出来了,有点冗长,文末给出了源码地址。这里仅仅大致给出实现思路,网上关于无锁队列的资料很多,这里就不详细说了。
enqueue(x) //进队列改良版
{
q = new record();
q->value = x;
q->next = null;
p = tail;
oldp = p
do {
while (p->next != null)
p = p->next;
} while( cas(p.next, null, q) != true); //如果没有把结点链在尾上,再试
cas(tail, oldp, q); //置尾结点
}
dequeue() //出队列
{
do{
p = head;
if (p->next == null){
return err_empty_queue;
}
while( cas(head, p, p->next) != true );
return p->next->value;
}
自旋锁自旋锁是加锁失败时接着循环请求加锁,直到成功。它的特点是不会释放cpu,故也没有互斥锁那种内核态切换操作,但缺点也很明显,就是会一直占用cpu,理论上适用于临界区小、不需要长时间加锁的场景。 这里只贴锁的相关代码,队列的实现就不贴了:
// 自旋锁
type spinmutex struct {
mutex int32
}
const locked = 1
const unlocked = 0
func (spin *spinmutex) lock() {
for !atomic.compareandswapint32(&spin.mutex, unlocked, locked) {}
}
func (spin *spinmutex) unlock() {
atomic.swapint32(&spin.mutex, unlocked)
}
互斥锁这个没什么好说的,用的golang自带的互斥锁sync.mutex。
测试
下面将分2种场景进行测试:分别是高并发和低并发。高并发我用4个协程往队列中push数据,4个协程从队列中pop数据(虽然不是很高,但足以区分性能,就没测太高并发了,毕竟测一次等的太久也累);低并发不好模拟,于是我干脆极端点改为无并发——先顺序写,再顺序读。
无并发大致测试代码结构如下(删减了不关键的语句):
t1 := time.now()
for i := 1; i <= datanum; i++ {
suc := queue.pushback(i)
}
queue.disable()
for {
val, enable := queue.popfront()
if !enable {
break
}
}
fmt.println(用时:, time.since(t1))
为了方便对比,我特地还增加了不加锁的队列的测试结果。测试结果如下:(左侧为datanum数据量)
添加图片注释,不超过 140 字(可选)
可以看到数据量小的时候性能差别还不明显,甚至cas还有少许的优势。但数据量一大就很明显的看出自旋锁的效率会高一点,cas次之。不过它们差别都不大。
高并发这里用4个生产者4个消费者共用一个队列来模拟高并发。测试代码结构如下:
func test() {
wgr := sync.waitgroup{}
wgw := sync.waitgroup{}
t1 := time.now()
for i := 0; i < 4; i++ {
wgr.add(1)
go reader(i*1000000, &wgr)
}
for i := 0; i < 4; i++ {
wgw.add(1)
go writter(&wgw)
}
wgr.wait()
queue.disable()
wgw.wait()
fmt.println(用时:, time.since(t1))
}
func reader(startnum int, wg *sync.waitgroup) {
for i := 0; i < datanum; i++ {
suc := queue.pushback(startnum + i)
for !suc {
suc = queue.pushback(startnum + i)
}
}
wg.done()
}
func writter(wg *sync.waitgroup) {
for {
r, enable := queue.popfront()
if enable == false {
break
}
if r == defaultval {
continue
}
}
wg.done()
}
这种情况下就没法测试无锁队列了,数据都不完整(已验证)。测试结果如下,左侧为读/写协程数*datanum数据量(下面读/写协程数为4指总共开了8个协程):
添加图片注释,不超过 140 字(可选)
可以看到cas有巨大的性能优势,甚至达到了3到5倍的性能差距,说明这个思路还是可行的!(先开始被chan打击到了)反倒是自旋锁的性能最差,这个倒有些出乎我的意料,按照我的理解在这种频繁加锁解锁的情况下自旋锁的性能应该更好才对,若有知情人士望告知。
分析为了对这几种锁的性能特点有更深入的分析,这里还补充了几组测试,分别用了不同的协程数和数据量进行补充测试:
添加图片注释,不超过 140 字(可选)
可以很明显的看到一个趋势——随着并发度增加,自旋锁的性能急剧下降,由无并发时的与cas性能几乎一样到最后与cas将近7倍的效率差。而mutex和cas情况下,随着并发度增加,性能影响并不大,下面将前面的测试数据重新组织一下方便对比:
添加图片注释,不超过 140 字(可选)
可以看到总数据量不变的情况下,并发协程数对mutex和cas的影响非常小,基本在波动范围以内。相较之下自旋锁就比较惨了。
总结**根据上面的结果来说的话,当实际竞争特别小的时候,可以考虑用自旋锁;而并发大的时候,用无锁队列这种结构有很大潜在优势。**之所以说潜在的是因为我也仅仅是简单的实现某种结构,肯定有考虑不全的地方,我写这个无锁例子主要用于测试,也没打算用于实际场景中。但是我尽量保证了同样的代码结构下,最大化各个锁结构对性能的影响。总的来说,本文测试结果仅作参考,希望能有抛砖引玉的效果。
最后,再附上源码地址:https://github.com/handsomerosin/lockfree
更新:针对自旋锁效率低下的问题我仔细想了想,应该是原子操作cas耗时的问题(毕竟在无并发情况下,cas和真正不加锁还是有很大的性能差距)。于是对自旋锁的代码进行了微调,减少了cas的调用次数:(被注释掉的是原本的代码逻辑)
func (spin *spinmutex) lock() {
// for !atomic.compareandswapint32(&spin.mutex, unlocked, locked) {}
begining:
for spin.mutex != unlocked {}
if !atomic.compareandswapint32(&spin.mutex, unlocked, locked) {
goto begining
}
}
事实证明,这样做效率确实提高了约1/4,不过还是改变不了它的大趋势(与cas和mutex的性能差距依旧巨大),所以就没更新前面的测试数据了。
不过这也佐证了cas也是比较耗时的一个操作,平时还是不能肆意使用。
变压器变比测试仪的作用意义
Python函数形参的分类
三星申请Dual LED商标,电视新品或将于CES 2020上正式亮相
脑洞大开!看看哪些物质能点亮LED灯
世强先进荣获Melexis2022优秀代理商“最佳设计奖”
无锁队列的潜在优势
笔记本电脑VGA信号切换参考设计
卫星通信v2 第七章 仿真与实战(7)基础知识
什么是4~20mA.DC信号制?4~20mA电流环工作原理
串联开关电路的原理图和等效电路图
剖析STM32-定时器1
雙極電晶體模型及電路
中国物联网行业结构及发展趋势报告
石油化工行业的自动化控制系统有哪些
上海规划2025年新能源汽车比重将在35%以上
莱迪思推出支持速率高达10 Gbps嵌入式MIPI D-PHY接口的FPGA
首款AMD EPYC处理器版主板公布:最多支持28块硬盘!
英特尔8核移动处理器旗舰产品i7-10875H曝光,将在3月底推出
特斯拉正在开发一款可续航1000公里的电池
亚马逊出自己的服务器处理器 正减少对英特尔的依赖