Pthreads并行编程之spin lock与mutex性能对比分析

RT-Thread的临界段的保护(开关中断) 临界区(Critical Section)是指在多线程编程中,一个访问共享资源的代码段,这些共享资源不能被多个线程同时访问,否则可能会导致数据不一致或其他并发问题。在操作系统中,临界区是指那些必须以原子方式执行的操作,以确保系统的一致性和正确性。临界区的概念是操作系统中同步和互斥的基础。在多线程环境中,由于线程之间的执行顺序是不可预测的,因此需要一些机制来确保当一个线程正在执行临界区代码时,其他线程不会同时进入相同的临界区。 阅读详情

POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套常用的API。线程同步(Thread Synchronization)是并行编程中非常重要的通讯手段,其中最典型的应用就是用Pthreads提供的锁机制(lock)来对多个线程之间共 享的临界区(Critical Section)进行保护(另一种常用的同步机制是barrier)。

Pthreads提供了多种锁机制:
(1) Mutex(互斥量):pthread_mutex_***
(2) Spin lock(自旋锁):pthread_spin_***
(3) Condition Variable(条件变量):pthread_con_***
(4) Read/Write lock(读写锁):pthread_rwlock_***

Pthreads提供的Mutex锁操作相关的API主要有:
pthread_mutex_lock (pthread_mutex_t *mutex);
pthread_mutex_trylock (pthread_mutex_t *mutex);
pthread_mutex_unlock (pthread_mutex_t *mutex);

Pthreads提供的与Spin Lock锁操作相关的API主要有:
pthread_spin_lock (pthread_spinlock_t *lock);
pthread_spin_trylock (pthread_spinlock_t *lock);
pthread_spin_unlock (pthread_spinlock_t *lock);

从实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

如果大家去查阅Linux glibc中对pthreads API的实现NPTL(Native POSIX Thread Library) 的源码的话(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我们系统中NPTL的版本号),就会发现pthread_mutex_lock()操作如果没有锁成功的话就会调用system_wait()的系统调用(现在NPTL的实现采用了用户空间的futex,不需要频繁进行系统调用,性能已经大有改善),并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.cspinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。

那么在实际编程中mutex和spin lcok哪个的性能更好呢?我们知道spin lock在Linux内核中有非常广泛的利用,那么这是不是说明spin lock的性能更好呢?下面让我们来用实际的代码测试一下(请确保你的系统中已经安装了最近的g++)。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// Name: spinlockvsmutex1.cc
// Compiler(spin lock version): g++ -o spin_version -DUSE_SPINLOCK spinlockvsmutex1.cc -lpthread
// Compiler(mutex version): g++ -o mutex_version spinlockvsmutex1.cc -lpthread
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
#include <sys/time.h>
#include <list>
#include <pthread.h>
 
#define LOOPS 50000000
 
using namespace std;
 
list< int > the_list;
 
#ifdef USE_SPINLOCK
pthread_spinlock_t spinlock;
#else
pthread_mutex_t mutex;
#endif
 
//Get the thread id
pid_t gettid() { return syscall( __NR_gettid ); }
 
void *consumer( void *ptr)
{
     int i;
 
     printf ( "Consumer TID %lun" , (unsigned long )gettid());
 
     while (1)
     {
#ifdef USE_SPINLOCK
         pthread_spin_lock(&spinlock);
#else
         pthread_mutex_lock(&mutex);
#endif
 
         if (the_list.empty())
         {
#ifdef USE_SPINLOCK
             pthread_spin_unlock(&spinlock);
#else
             pthread_mutex_unlock(&mutex);
#endif
             break ;
         }
 
         i = the_list.front();
         the_list.pop_front();
 
#ifdef USE_SPINLOCK
         pthread_spin_unlock(&spinlock);
#else
         pthread_mutex_unlock(&mutex);
#endif
     }
 
     return NULL;
}
 
int main()
{
     int i;
     pthread_t thr1, thr2;
     struct timeval tv1, tv2;
 
#ifdef USE_SPINLOCK
     pthread_spin_init(&spinlock, 0);
#else
     pthread_mutex_init(&mutex, NULL);
#endif
 
     // Creating the list content...
     for (i = 0; i < LOOPS; i++)
         the_list.push_back(i);
 
     // Measuring time before starting the threads...
     gettimeofday(&tv1, NULL);
 
     pthread_create(&thr1, NULL, consumer, NULL);
     pthread_create(&thr2, NULL, consumer, NULL);
 
     pthread_join(thr1, NULL);
     pthread_join(thr2, NULL);
 
     // Measuring time after threads finished...
     gettimeofday(&tv2, NULL);
 
     if (tv1.tv_usec > tv2.tv_usec)
     {
         tv2.tv_sec--;
         tv2.tv_usec += 1000000;
     }
 
     printf ( "Result - %ld.%ldn" , tv2.tv_sec - tv1.tv_sec,
         tv2.tv_usec - tv1.tv_usec);
 
#ifdef USE_SPINLOCK
     pthread_spin_destroy(&spinlock);
#else
     pthread_mutex_destroy(&mutex);
#endif
 
     return 0;
}

该程序运行过程如下:主线程先初始化一个list结构,并根据LOOPS的值将对应数量的entry插入该list,之后创建两个新线程,它们都执行consumer()这个任务。两个被创建的新线程同时对这个list进行pop操作。主线程会计算从创建两个新线程到两个新线程结束之间所用的时间,输出为下文中的”Result “。

测试机器参数:
Ubuntu 9.04 X86_64
Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz
4.0 GB Memory

从下面是测试结果:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
gchen@gchen-desktop:~ /Workspace/mutex $ g++ -o spin_version -DUSE_SPINLOCK spinvsmutex1.cc -lpthread
gchen@gchen-desktop:~ /Workspace/mutex $ g++ -o mutex_version spinvsmutex1.cc -lpthread
gchen@gchen-desktop:~ /Workspace/mutex $ time . /spin_version
Consumer TID 5520
Consumer TID 5521
Result - 5.888750
 
real    0m10.918s
user    0m15.601s
sys    0m0.804s
 
gchen@gchen-desktop:~ /Workspace/mutex $ time . /mutex_version
Consumer TID 5691
Consumer TID 5692
Result - 9.116376
 
real    0m14.031s
user    0m12.245s
sys    0m4.368s

可以看见spin lock的版本在该程序中表现出来的性能更好。另外值得注意的是sys时间,mutex版本花费了更多的系统调用时间,这就是因为mutex会在锁冲突时调用system wait造成的。

但是,是不是说spin lock就一定更好了呢?让我们再来看一个锁冲突程度非常剧烈的实例程序:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//Name: svm2.c
//Compile(spin lock version): gcc -o spin -DUSE_SPINLOCK svm2.c -lpthread
//Compile(mutex version): gcc -o mutex svm2.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/syscall.h>
 
#define        THREAD_NUM     2
 
pthread_t g_thread[THREAD_NUM];
#ifdef USE_SPINLOCK
pthread_spinlock_t g_spin;
#else
pthread_mutex_t g_mutex;
#endif
__uint64_t g_count;
 
pid_t gettid()
{
     return syscall(SYS_gettid);
}
 
void *run_amuck( void *arg)
{
        int i, j;
 
        printf ( "Thread %lu started.n" , (unsigned long )gettid());
 
        for (i = 0; i < 10000; i++) {
#ifdef USE_SPINLOCK
            pthread_spin_lock(&g_spin);
#else
                pthread_mutex_lock(&g_mutex);
#endif
                for (j = 0; j < 100000; j++) {
                        if (g_count++ == 123456789)
                                printf ( "Thread %lu wins!n" , (unsigned long )gettid());
                }
#ifdef USE_SPINLOCK
            pthread_spin_unlock(&g_spin);
#else
                pthread_mutex_unlock(&g_mutex);
#endif
        }
        
        printf ( "Thread %lu finished!n" , (unsigned long )gettid());
 
        return (NULL);
}
 
int main( int argc, char *argv[])
{
        int i, threads = THREAD_NUM;
 
        printf ( "Creating %d threads...n" , threads);
#ifdef USE_SPINLOCK
        pthread_spin_init(&g_spin, 0);
#else
        pthread_mutex_init(&g_mutex, NULL);
#endif
        for (i = 0; i < threads; i++)
                pthread_create(&g_thread[i], NULL, run_amuck, ( void *) i);
 
        for (i = 0; i < threads; i++)
                pthread_join(g_thread[i], NULL);
 
        printf ( "Done.n" );
 
        return (0);
}

这个程序的特征就是临界区非常大,这样两个线程的锁竞争会非常的剧烈。当然这个是一个极端情况,实际应用程序中临界区不会如此大,锁竞争也不会如此激烈。测试结果显示mutex版本性能更好:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
gchen@gchen-desktop:~ /Workspace/mutex $ time . /spin
Creating 2 threads...
Thread 31796 started.
Thread 31797 started.
Thread 31797 wins!
Thread 31797 finished!
Thread 31796 finished!
Done.
 
real    0m5.748s
user    0m10.257s
sys    0m0.004s
 
gchen@gchen-desktop:~ /Workspace/mutex $ time . /mutex
Creating 2 threads...
Thread 31801 started.
Thread 31802 started.
Thread 31802 wins!
Thread 31802 finished!
Thread 31801 finished!
Done.
 
real    0m4.823s
user    0m4.772s
sys    0m0.032s

另外一个值得注意的细节是spin lock耗费了更多的user time。这就是因为两个线程分别运行在两个核上,大部分时间只有一个线程能拿到锁,所以另一个线程就一直在它运行的core上进行忙等待,CPU占用率一直是100%;而mutex则不同,当对锁的请求失败后上下文切换就会发生,这样就能空出一个核来进行别的运算任务了。(其实这种上下文切换对已经拿着锁的那个线程性能也是有影响的,因为当该线程释放该锁时它需要通知操作系统去唤醒那些被阻塞的线程,这也是额外的开销)

总结
(1)Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。

(2)spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。

(3)更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lock和rw lock)。

rt-thread学习之路第十一章--对称多处理SMP 当 CPU 需要切换任务执行时,任务调度器查找系统中优先级最高的就绪任务执行,即全局就绪任务表和当前 CPU 的局部就绪任务表中优先级最高的任务。最终,CPU0 在执行 main 线程之前,唤醒其它的次级 CPU,引导它们执行次级 CPU 的初始化代码,这段代码会让各个次级 CPU 去完成自身相关的硬件初始化,并开启任务调度。每个次级 CPU 启动之后,从全局任务表和当前 CPU 的局部任务表中选取优先级最高的任务执行,在优先级相同的情况下,优先选择当前 CPU 的局部任务表中的任务执行。 阅读详情

相关推荐

rd630服务器系统,联想thinkserverRD630安装windowsserver2012

服务器型号:thinkserver RD630系统:windowsserver2012安装故障:引导从镜像启动后报错:Your computer ran into a problem and needs to restart we are just collcting some error info and then we will restart for you,安装失败蓝屏,后来联系我们云修网...

weixin_36336710的博客 2142

RT-thread移植指南-RISC-V

目录 RT-thread移植指南-RISC-V.. 3 1. 概述... 3 1.1 移植资料参考... 3 1.2 移植开发环境准备... 4 2. 移植步骤... 4 2.1 全局中断开关函数... 5 2.2 线程上下文切换函数... 5 2.3. 线程栈的初始化... 6 2.4 时钟节拍的配置... 6 2.5 中断函数(中断时现场保护、中断注册和使能)... 8 2.5.1 interrupt_gcc.S:... 8 2.5.2 中断注册、使能...

7042

Dell PowerEdge T630加装RTX显卡保姆级教程:供电卡安装、主板拆卸要点全解析

本文提供Dell PowerEdge T630服务器加装RTX显卡的完整教程,重点解析供电卡安装主板拆卸要点。详细指导硬件兼容性检查、配件准备、安全拆解流程及供电系统改造,确保RTX 3060等显卡的稳定运行,适合需要提升服务器图形计算能力的技术人员参考。

weixin_30633405的博客 367

关于RT-Thread调度器锁

1.前言   RT-Thread系统开发,业务应用使用到了RT-Thread的调度器锁,因为使用不慎导致走了一部分弯路。致命的错误是,未有考虑到逻辑的执行非执行后果,线程上锁后,逻辑条件未满足调度锁未能释放,从而导致其他线程未能获得CPU资源,出现是系统“假死”的现象。由于逻辑条件比较难重现,任务线程也及中断条件也不少,查找问题花费一部分时间。当然,最后还是发现致命的基础问题,就是调度器锁未能...

只要思想不滑坡,想法总比问题多。 3823

捡漏Dell T630服务器后,我是如何一步步搞定RTX 3060显卡安装的(含配件清单)

本文详细记录了在Dell PowerEdge T630服务器上安装RTX 3060显卡的全过程,包括硬件选型、供电系统改造和实战安装步骤。通过分享配件清单和关键调试技巧,帮助预算有限的开发者成功将消费级显卡应用于企业级服务器,显著提升深度学习训练效率。

weixin_30879833的博客 344

Dell PowerEdge T630 升级RTX 3060:从选型到供电的避坑实战

本文详细介绍了在Dell PowerEdge T630服务器上升级RTX 3060显卡的全过程,包括选型考量、供电配件准备、安装步骤及常见问题解决方案。通过实战经验分享,帮助用户规避升级过程中的各种陷阱,实现高性价比的GPU计算能力提升,特别适合预算有限的个人研究者或小型团队。

weixin_33226548的博客 165

咸鱼淘的Dell T630服务器,如何一步步搞定RTX 3060显卡安装(附完整配件清单)

本文详细介绍了在二手Dell PowerEdge T630服务器上安装RTX 3060显卡的全流程指南,包括硬件选型、配件采购、安装步骤和BIOS设置。通过完整的配件清单和避坑指南,帮助预算有限的深度学习研究者高效完成GPU升级,显著提升计算性能

weixin_30785593的博客 437

联想服务器rd630硬盘报错,联想thinkserverRD630安装windowsserver2012

服务器型号:thinkserver RD630系统:windowsserver2012安装故障:引导从镜像启动后报错:Your computer ran into a problem and needs to restart we are just collcting some error info and then we will restart for you,安装失败蓝屏,后来联系我们云修网...

weixin_29082143的博客 1671

捡漏Dell T630服务器后,我如何用RTX 3060 12G给它装上“游戏显卡”跑深度学习?

本文详细记录了如何在Dell PowerEdge T630服务器上安装RTX 3060 12G显卡以运行深度学习的全过程。从硬件选型、必备配件清单到拆解安装步骤,再到系统配置性能测试,为低成本搭建AI开发环境提供了实用指南。特别强调了GPU供电改造和散热优化的关键细节,帮助读者避免常见问题。

weixin_30509393的博客 411

捡漏Dell T630服务器后,我是如何给它装上RTX 3060显卡跑深度学习的(附完整配件清单)

本文详细介绍了如何在二手Dell PowerEdge T630服务器上安装RTX 3060显卡以搭建高性能深度学习工作站。从硬件兼容性验证、关键配件选购到详细安装步骤和性能优化,提供了完整的实战指南,帮助预算有限的开发者实现低成本高性能的深度学习环境。

weixin_30888413的博客 522

天正T20 V10.0安装失败原因CAD环境依赖详解

天正T20是深度集成于AutoCAD的专业建筑插件,其安装本质不是软件部署,而是构建满足三重硬性约束的CAD运行生态:第一,必须匹配特定小版本号的AutoCAD(如2022.1.100.0),因ObjectARX接口存在版本敏感性;第二,依赖Windows系统级组件严格顺序安装(VC++ 2015–2022 → .NET Framework 4.8 → DirectX),顺序错误将导致安装器静默退出;第三,显卡需支持OpenGL 4.1+并生成动态.hdi硬件描述文件,核显或驱动不兼容时易报‘文件损坏’。技

weixin_30437847的博客 510

天正T20 V10.0安装失败原因系统级排障指南

天正建筑软件作为AutoCAD深度集成的BIM辅助设计插件,其安装本质是构建一套跨组件依赖体系:需严格匹配64位AutoCAD版本、预置兼容MySQL 5.7/8.0本地数据库、满足OpenGL 3.3+显卡驱动要求,并保障.NET Framework 4.8、VC++运行库及WMI服务等Windows底层组件完整。该过程远超常规软件部署,技术价值在于确保图库索引、材质管理、三维渲染等核心功能稳定运行。典型应用场景包括设计院批量部署、高校机房标准化配置及建筑师个人工作站搭建。本文聚焦天正T20 V10.0安

aikb6223的博客 707

一加6/6T刷Win11 ARM版保姆级避坑指南:从TWRP到驱动安装全流程

本文提供了一加6/6T刷入Win11 ARM版的详细教程,涵盖从分区重构到驱动安装的全流程避坑指南。通过精确匹配硬件软件、分区表重构、UEFI刷入及驱动部署等关键步骤,帮助用户安全完成系统安装,并优化性能以提升使用体验。

weixin_30709061的博客 194

ibm服务器修复安装win7系统,联想Tthinkpad T15装win7系统及bios设置方法(新方法)

最近有网友问刚购买的联想Tthinkpad T15笔记本怎么装win7系统?按照平常的方法安装一定会失败,最新联想Tthinkpad T15笔记本默认只支持uefi模式,并且可能出现卡四叶草logo的问题,并且安装后usb键盘不能使用,那么联想Tthinkpad T15要怎么安装win7系统呢?下面小编就给大家介绍详细的操作步骤,一定要按以下操作步骤及工具才能正常安装。一、联想Tthinkpad ...

weixin_39815310的博客 2312

vivado2023.2下载安装教程:全面讲解硬件配置驱动设置

深入讲解vivado2023.2下载安装教程,涵盖系统要求、硬件适配及关键驱动设置步骤,帮助用户顺利完成开发环境搭建,提升FPGA开发效率。

weixin_31800911的博客 850

T2-Ubuntu项目:解决MacBook Pro 16.1安装Ubuntu 24.04后黑屏问题

T2-Ubuntu项目:解决MacBook Pro 16.1安装Ubuntu 24.04后黑屏问题 在MacBook Pro 16.1(Intel Core i9处理器,配备Intel UHD Graphics 630和AMD Radeon Pro 5500M双显卡)上安装Ubuntu 24.04时,用户可能会遇到两个主要问题: 安装阶段问题:选择"安装Ubuntu"时出现&...

gitblog_07561的博客 568

联想M920x黑苹果安装保姆级教程:从硬件兼容到完美驱动的折腾指南

想要在联想M920x迷你主机上体验macOS系统?本教程将带你完成从硬件兼容性检测到EFI配置优化的全过程,助你打造稳定流畅的黑苹果环境。我们会详细讲解安装步骤、性能优化技巧及常见问题解决方案,让你少走弯路,顺利实现M920x的黑苹果完美驱动。 ## 需求分析:为什么选择M920x搭建黑苹果 联想M920x作为一款高性能迷你主机,凭借其紧凑的设计和强大的硬件配置,成为黑苹果爱好者的理想选择。它

gitblog_00239的博客 1310
上一篇: 自旋锁spinlock
下一篇: 二叉树前序中序后序
edward0004
博客等级 码龄15年 65粉丝 57原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值