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.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。

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

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

该程序运行过程如下:主线程先初始化一个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

从下面是测试结果:

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.c spinlock.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
// Source: http://www.alexonlinux.com/pthread-mutex-vs-pthread-spinlock
// 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 50000000using 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_SPINLOCKpthread_spin_lock(&spinlock);
#elsepthread_mutex_lock(&mutex);
#endifif (the_list.empty()){
#ifdef USE_SPINLOCKpthread_spin_unlock(&spinlock);
#elsepthread_mutex_unlock(&mutex);
#endifbreak;}i = the_list.front();the_list.pop_front();#ifdef USE_SPINLOCKpthread_spin_unlock(&spinlock);
#elsepthread_mutex_unlock(&mutex);
#endif}return NULL;
}int main()
{int i;pthread_t thr1, thr2;struct timeval tv1, tv2;#ifdef USE_SPINLOCKpthread_spin_init(&spinlock, 0);
#elsepthread_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_SPINLOCKpthread_spin_destroy(&spinlock);
#elsepthread_mutex_destroy(&mutex);
#endifreturn 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.888750real    0m10.918s
user    0m15.601s
sys    0m0.804sgchen@gchen-desktop:~/Workspace/mutex$ time ./mutex_version
Consumer TID 5691
Consumer TID 5692
Result - 9.116376real    0m14.031s
user    0m12.245s
sys    0m4.368s

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

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

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

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

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.004sgchen@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)。

2010年3月3日补记:这个观点在Oracle的文档中得到了支持:

During configuration, Berkeley DB selects a mutex implementation for the architecture. Berkeley DB normally prefers blocking-mutex implementations over non-blocking ones. For example, Berkeley DB will select POSIX pthread mutex interfaces rather than assembly-code test-and-set spin mutexes because pthread mutexes are usually more efficient and less likely to waste CPU cycles spinning without getting any work accomplished.

p.s.调用syscall(SYS_gettid)和syscall( __NR_gettid )都可以得到当前线程的id:)

转自:www.parallellabs.com

Pthreads并行编程之spin lock与mutex性能对比分析(转)相关推荐

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

    POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套常用的API.线程同步(Thread Synchronization)是并行编程中非常重要的通讯手段,其中最典型的应用 ...

  2. Spin lock 与mutex 的区别--2011.01.06

    POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套常用的API.线程同步(Thread Synchronization)是并行编程中非常重要的通讯手段,其中最典型的应用 ...

  3. C#多线程编程之:lock使用注意事项

    1.避免锁定public类型对象. 如果实例可以被公共访问,将出现lock(this)问题. 如有一个类MyClass,该类有一个Method方法通过lock(this)来实现互斥: 1 public ...

  4. golang sync.Map和map+mutex性能比较

    目录 测试环境 测试代码 运行指令 测试结果 结论 测试环境 goos: linux goarch: amd64 go version go1.14 linux/amd64 Run on (8 X 2 ...

  5. Delphi D10.X 并行库PPL编程之 TParallel.For

    Delphi D10.X 并行库PPL编程系列之 TParallel.For delphi中的RTL(运行库)提供了并行编程库(PPL --Parallel Programming Library) ...

  6. Delphi D10.X 并行库PPL编程之TTask

    Delphi D10.X 并行库PPL编程系列之 TTask delphi中的RTL(运行库)提供了并行编程库(PPL --Parallel Programming Library) ,让您的应用程序 ...

  7. Python并发编程之threading模块

    Python并发编程之threading模块 threading 模块 1. Timer对象 2. Lock对象 3. RLock 4. 信号量和有边界的信号量 5. 事件 6. 条件变量 7. 使用 ...

  8. linux c编程之fcntl

    fcntl可实现对指定文件描述符的各种操作,其函数原型如下: int fcntl(int fd, int cmd, ... /* arg */ ); 其中,操作类型由cmd决定.cmd可取如下值: F ...

  9. python 多线程编程之_thread模块

    python 多线程编程之_thread模块 参考书籍:python核心编程 _thread模块除了可以派生线程外,还提供了基本的同步数据结构,又称为锁对象(lock object,也叫原语锁.简单锁 ...

最新文章

  1. jquery API
  2. linux下mv命令移动目录的二种情况
  3. 剑指offer4:重建二叉树
  4. layui datetimepicker 只日期范围到当前时间的前一天_浪琴手表如何正确调整日期?手表调日期的方法...
  5. java集合转换_java各种集合的转换
  6. 在Linux终端下调用可执行文件时总要加上符号./的原因
  7. mybatis中使用小于号
  8. Oracle建表添加数据
  9. delete 和 delete []
  10. 苹果Mac数据库管理开发工具:JetBrains DataGrip
  11. 蚂蚁课堂视频笔记思维导图-4期 一、微服务技术
  12. 硬件信息修改工具 支持修改机器码 硬盘 MAC 等信息
  13. Smart V3触摸屏与S7-200Smart PLC实现时间同步的具体方法
  14. 信息技术智库丨月度大考试
  15. osg显示CEGUI界面
  16. 枣庄市建筑物矢量数据(Shp格式+带高度)
  17. 南宁富士康c语言笔试题,富士康笔试题目
  18. python 按列读取数据并写入txt_Python数据分析之Pandas读写外部数据文件!
  19. Windows 11 Insider Preview Build 22621.730/22623.730(KB5017385)发布!
  20. 富文本编辑:wangEditor使用教程

热门文章

  1. 计算机公式大全日期与时间函数,09年计算机等级辅导:日期时间函数
  2. 【交换篇】04. 划分 VLAN ❀ C3750-E ❀ CISCO 交换机
  3. php和h5哪个需求量大,【linux】thinkphp3.23开发的网站关于并发量大的优化有哪些?...
  4. HashMap看这一篇就够了
  5. 基于SpringBoot的仿饿了吗外卖APP设计与实现
  6. ECIR 2016 Paper Beyond Factoid QA: Effective Methods for Non-factoid Answer Sentence Retrieval
  7. 互联网金融网贷平台技术架构介绍
  8. 注册机偷懒写法2、之直接调用源程序的函数
  9. 有关cache命中率的问题
  10. mos管寄生二极管的作用