Pthreads並行編程之spin lock與mutex性能對比分析(轉)

 

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:)

 

轉自:www.parallellabs.com

相關文章
相關標籤/搜索