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 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
[email protected]-desktop:~/Workspace/mutex$ g++ -o spin_version -DUSE_SPINLOCK spinvsmutex1.cc -lpthread
[email protected]-desktop:~/Workspace/mutex$ g++ -o mutex_version spinvsmutex1.cc -lpthread
[email protected]-desktop:~/Workspace/mutex$ time ./spin_version
Consumer TID 5520
Consumer TID 5521
Result - 5.888750
real 0m10.918s
user 0m15.601s
sys 0m0.804s
[email protected]-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就一定更好了呢?讓我們再來看一個鎖沖突程度非常劇烈的執行個體程式:
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版本性能更好:
[email protected]:~/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
[email protected]-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