大并发下的高性能编程 – 改进的(用户态)自旋锁

简介: 前言多线程程序中,锁的使用往往成为系统性能的关键。在做地址可视化项目的时候,由于内存管理部分需要频繁的更新内存的引用计数,所以产生了使用自旋锁的想法,这篇文章我们从自旋锁的性能开始说起,由浅入深的给出了一种改进的自旋锁的实现。

前言

多线程程序中,锁的使用往往成为系统性能的关键。在做地址可视化项目的时候,由于内存管理部分需要频繁的更新内存的引用计数,所以产生了使用自旋锁的想法,这篇文章我们从自旋锁的性能开始说起,由浅入深的给出了一种改进的自旋锁的实现。

这里我们 1) 讨论自旋锁对并发程序性能的影响; 2) glibc中自旋锁的缺陷; 3) 随后提出了一种改进的(用户空间)自旋锁的实现,供大家在今后的程序设计中参考、使用。欢迎给出改进的自旋锁中的不足和意见。

关于锁

总体上来看,锁分为两种:休眠式锁和自旋锁。休眠式锁的原理是当当前线程不能获取到指定的锁时,它就让出CPU,加入到一个等待队列中,直到被唤醒,它才会被重新调度执行。自旋锁的原理是若当前线程不能获取到指定的锁,它不会主动让出CPU,而是会在一个紧凑循环中重复的检测锁是否已经可用,即忙等待(busy wait)。

休眠锁与自旋锁的对比

如果对临界资源的访问时间很短(如更新引用计数、修改状态等),等待锁的线程只要稍等片刻即可取得资源的访问权,这个开销要远远小于进入休眠再被唤醒。为了体现自旋锁对性能的影响,我们在相同的场景下分别使用休眠锁(pthread_mutex_t) 和glibc 自旋锁(pthread_spinlock_t)来测试程序的执行时间。

测试场景如下:

  • 硬件: 4 core CPU
  • 程序功能: 多线程, 各个线程都去访问同一个数据结构(临界资源),更新此结构的引用计数,再附件一些其他操作,来模拟实际操作以耗费cpu circle, 每个线程循环执行上面的操作1千万次。
  • 线程个数 4 (= cpu核数), 8 (> cpu核数)

在不同的并发度下,mutex和glibc spinlock的执行时间如下图所示:

在并发度为4的时候,各个CPU核的使用情况如下所示

  1. mutex

  2. glibc spinlock

有上述测试可见,当并发度小于(等于)cpu core的数量时,glibc spinlock在执行效率上有明显的优势,但是当系统的并发度提高时,glibc spinlock的执行效率急剧的降低,这个问题我们将在后面章节详细描述。

由CPU的使用情况可见,使用mutex时,CPU很大程度上运行在内核态,这是因为它频繁的休眠、被唤醒导致的。而glibc spinlock则一直运行在用户态,因为它一直在CPU上循环检测锁的状态。

抛开glibc spinlock在并发度大时性能降低问题,根据上面的测试和分析,我们讨论一下适合自旋锁的场景:

  • 线程持有锁的时间很短暂
  • 系统对(锁)资源的竞争很激烈

glibc中自旋锁的缺陷

glic中自旋锁pthread_spinlock_t的实现原理正如我们上面讨论的,等待锁的线程会在一个紧凑循环中不停的检测锁的状态从而确定它何时可以获取到锁。虽然glic的自旋锁使程序的性能有了很大的提升,但是这个锁在实现中忽略了一个重要的事实:用户空间的进程受时间片轮转的控制,持有锁的进程可能被调度出CPU! 从而造成等待锁(正在自旋)的线程在自己的时间片里空转进而浪费整个时间片。内核空间的自旋锁也是使用上述原理实现的,为什么它没有问题呢?因为内核态的自旋锁spin_lock() 在获取锁的时候首先会关闭CPU调度(preempt_disable),所以直到主动释放锁,它不会被调度出CPU.

改进的自旋锁

从上面测试中CPU的利用率也可以看出,自旋锁pthread_spinlock_t一直运行在用户态,CPU利用率为100%,并且当线程并发度很高的时候系统的性能有很大的降低。在高并发的系统中,pthread_spinlock_t其实在阻碍进程并发执行的效率,因为等待锁的进程在CPU上空浪费属于它的时间片,而其他可以执行的进程必须等待。基于以上讨论,我在这里给出了一个新的自旋锁,它的实现如下,我暂且把它称作nongreedy_spinlock:

int spinlock_internal(pthread_spinlock_t *lock)
{

int ret = 0;
__asm__ (“\n”
“1:\tlock; decl %0\n\t”
“jne 2f\n\t”
“movl $0, %1\n\t”
“jmp 4f\n\t”
“\n”
“.subsection 2\n\t”
“.align 16\n\t”
“2:\tmovl $5, %%ecx\n\t”
“3:\trep; nop\n\t”
“cmpl $0, %0\n\t”
“jg 1b\n\t”
“decl %%ecx\n\t”
“jnz 3b\n\t”
“jmp 5f\n\t”
“.previous\n\t”
“5:\tmovl $1, %1\n\t”
“4:\tnop”
: “=m” (*lock), “=r”(ret) : “m” (*lock) : “%ecx”);

return ret;

}

int nongreedy_spinlock(pthread_spinlock_t *lock)
{

int rc = 0;

rc = spinlock_internal(lock);
while (rc) {
sched_yield();
rc = spinlock_internal(lock);
}
return 0;

}

上面程序的逻辑很简单易懂,基本原理就是等待锁的线程如果在给定的时间内没有获取到锁,那么它就会主动的放弃CPU ,给其他等待CPU的线程一个运行的机会。 下面我们评测一下改造后的自旋锁的性能,测试场景同上:

程序执行效率如下图:

系统中各CPU的使用情况如下:

可见,程序大部分时间运行在用户态,少量时间运行在内核态,这是由于等待锁的线程会主动让出CPU给其他等待执行的线程使用,所以在执行过程中会有较多执行任务的调度,与测试中得到的结论相同。

由上面的测试可见,无论线程的并发度是多大,经过优化的自旋锁(我们称之为nongreedy spinlock)在性能上胜过了mutex和glibc spinlock。

由于我们的系统中数据量越来越大,系统的并发度也在提高,所以应该谨慎选择锁的类型,如果使用自旋锁的话,欢迎大家评测、使用本文中给出的这个改进的实现。

总结

在程序设计的时候,要分析问题处理的场景,千万不能生搬硬套,我个人认为glibc spinlock的问题就是把linux kernel中的自旋锁生搬到用户空间造成的,它忽略了linux内核态和用户态环境的不同。我们要从中吸取经验教训。

转自:http://blogread.cn/it/article.php?id=5011

目录
相关文章
|
开发框架 .NET 芯片
电子技术实训——多功能数字钟的设计
电子技术实训——多功能数字钟的设计
电子技术实训——多功能数字钟的设计
|
敏捷开发 Devops 测试技术
构建软件质量保障体系
构建软件质量保障体系
1197 0
|
存储 Android开发 iOS开发
app 上架需要准备什么以及上架流程
上架前需要准备的材料:众所周知,应用市场主要分为两类,一类是 App Store,另一类是安卓市场。而安卓市场又分为:第三方市场(如:应用宝、360 手机助手、豌豆荚),和手机厂商市场(如:华为、OPPO、VIVO、小米等)。
|
人工智能 自然语言处理 Java
一文轻松拿下DeepSeek满血版的Api接口接入
本文详细介绍了如何基于阿里云百炼平台快速接入DeepSeek满血版API接口,实现文本问答的智能操作。通过简单的步骤,包括开通平台、创建API-KEY、配置环境变量、安装SDK和调用模型,即使是编程新手也能在5分钟内完成配置。DeepSeek-R1模型拥有671B参数,在数学、代码和自然语言推理等任务上表现出色。文章还提供了完整的Java代码示例,帮助读者轻松上手。链接:[解决方案](https://www.aliyun.com/solution/tech-solution/deepseek-r1-for-platforms?utm_content=g_1000401616)
2805 1
一文轻松拿下DeepSeek满血版的Api接口接入
|
存储 数据安全/隐私保护 数据中心
Incus 6.4 容器和虚拟机管理器发布
【10月更文挑战第26天】
882 2
Incus 6.4 容器和虚拟机管理器发布
|
存储 边缘计算 运维
移动LTE背后:核心网络详解
移动LTE背后:核心网络详解
1845 4
|
消息中间件 Java Maven
Java整合RabbitMQ实现生产消费(7种通讯方式)
Java整合RabbitMQ实现生产消费(7种通讯方式)
690 0
|
编译器 Linux C语言
ARM64上开启MTE
ARM64上开启MTE
|
自然语言处理 供应链 数据可视化
大数据在市场营销中的应用案例:精准洞察,驱动增长
【8月更文挑战第25天】大数据在市场营销中的应用案例不胜枚举,它们共同展示了大数据技术在精准营销、市场预测、用户行为分析等方面的巨大潜力。通过深度挖掘和分析数据,企业能够更加精准地洞察市场需求,优化营销策略,提升市场竞争力。未来,随着大数据技术的不断发展和普及,其在市场营销领域的应用将更加广泛和深入。
4341 3
|
存储 安全 Linux
Linux命令mkfifo深度解析
`mkfifo`在Linux中创建命名管道,用于进程间通信(IPC)。管道是临时的,非持久存储,作为数据传输的通道。特点是无缓冲、支持阻塞/非阻塞模式和权限控制。命令语法:`mkfifo -m <权限> 文件名`。示例:创建`mypipe`,一个进程写入,另一进程读取。注意选择阻塞模式、管理权限、删除不再使用的管道,并处理可能的错误。用于数据交换和高效能应用。

热门文章

最新文章