POSIX threads(簡稱Pthreads)是在多核平臺上進行並行編程的一套經常使用的API。線程同步(Thread Synchronization)是並行編程中很是重要的通信手段,其中最典型的應用就是用Pthreads提供的鎖機制(lock)來對多個線程之間共 享的臨界區(Critical Section)進行保護(另外一種經常使用的同步機制是barrier)。php
Pthreads提供了多種鎖機制:
(1) Mutex(互斥量):pthread_mutex_***
(2) Spin lock(自旋鎖):pthread_spin_***
(3) Condition Variable(條件變量):pthread_con_***
(4) Read/Write lock(讀寫鎖):pthread_rwlock_***html
Pthreads提供的Mutex鎖操做相關的API主要有:
pthread_mutex_lock (pthread_mutex_t *mutex);
pthread_mutex_trylock (pthread_mutex_t *mutex);
pthread_mutex_unlock (pthread_mutex_t *mutex);linux
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++)。oracle
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 「。app
測試機器參數:
Ubuntu 9.04 X86_64
Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz
4.0 GB Memoryless
從下面是測試結果:性能
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 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就必定更好了呢?讓咱們再來看一個鎖衝突程度很是劇烈的實例程序:
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.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)。
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:)