线程锁和自旋锁的比较
最近从事多线程相关的编程,对于多线程的性能比较关心,所以去网上找了一些资料。看到了并行实验室的冠诚前辈的博文 学习到了很多,下面是我的学习笔记。光荣属于前辈。
    线程锁调用API如下:
-       pthread_mutex_lock(&mutex);
 - pthread_mutex_unlock(&mutex);
 
   自旋锁调用的API 如下:
-      pthread_spin_lock(&spinlock);
 - pthread_spin_unlock(&spinlock);
 
    所谓线程锁,是指如果线程没有抢到线程锁,那么线程就会被阻塞,线程所在的CPU会发生进程调度,选择其他进程执行。所以可以想见,如果线程锁竞争的非常激烈(如100个线程争一把线程锁),那么,上下文切换会非常的多。下面我们的实验会证明这一点。
 
   
而自旋锁则不同,它属于busy-waiting类型的锁,线程竞争自旋锁,如果竞争不到,线程会不停的忙等待,不停的重试锁请求。如果长时间请求不到自
旋锁,自旋锁看起来就像死循环一样。从自旋锁的特点来看,自旋锁只适合与竞争不太激烈(即并发争锁的线程个数不多,),并且临界区不大的情况。下面我们的
实验也会证明这一点。
    下面是我的测试代码,little和big是临界区要执行的代码。故名思议,little是临界区粒度很小,big是临界区很大。通过宏来控制选择自旋锁还是线程锁。
- #include<stdio.h>
 - 
#include<stdlib.h>
 - 
#include<pthread.h>
 - #define USE_SPINLOCK
 - #ifdef USE_SPINLOCK
 - 
pthread_spinlock_t spinlock;
 - 
#else
 - 
pthread_mutex_t    mutex;
 - #endif
 - #define NR_THREAD 2
 - #define MILLION 1000000L
 - #define TIMES 100000
 - #define EXEC_TIMES 1000000
 - 
unsigned long long counter = 0;
 - 
inline int little()
 - 
{
 - 
    counter++;
 - 
}
 - 
inline int big()
 - 
{
 - 
    int j;
 - 
    for(j = 0;j<TIMES;j++)
 - 
    {
 -          counter++;
 - 
    }
 - 
}
 - 
void * worker(void* arg)
 - 
{
 - 
   int i;
 - 
     for(i = 0;i<EXEC_TIMES;i++)
 - 
     {
 - #ifdef USE_SPINLOCK
 - 
             pthread_spin_lock(&spinlock);
 - 
    #else
 - 
             pthread_mutex_lock(&mutex);
 - #endif
 - 
             little();
 - 
             //big();
 - #ifdef USE_SPINLOCK
 - 
             pthread_spin_unlock(&spinlock);
 - 
    #else
 - 
             pthread_mutex_unlock(&mutex);
 - #endif
 - 
     }
 - 
     return NULL;
 - 
}
 - 
int main()
 - 
{
 - 
  int i;
 - 
    struct timeval tv_start,tv_end;
 - 
    unsigned long long interval = 0;
 - #ifdef USE_SPINLOCK
 - 
    pthread_spin_init(&spinlock,0);
 - 
#else 
 - 
    pthread_mutex_init(&mutex,NULL);
 - #endif
 - 
  pthread_t Tid[NR_THREAD];
 - 
    gettimeofday(&tv_start,NULL);
 - 
    for(i = 0;i<NR_THREAD;i++)
 - 
    {
 -          if(pthread_create(&Tid[i],NULL,worker,NULL) != 0)
 - 
         {
 - fprintf(stderr,"pthread create failed
 -                                when i = %d\n",i);
 -                return -1;
 - 
         }
 - 
    }
 - 
    for(i = 0;i<NR_THREAD;i++)
 - 
    {
 -             if(pthread_join(Tid[i],NULL))
 - 
            {
 - fprintf(stderr,"pthread join failed
 -                                   when i = %d\n",i);
 -                   return -2;
 - 
            }
 - 
    }
 - 
    gettimeofday(&tv_end,NULL);
 -     interval = MILLION*(tv_end.tv_sec  - tv_start.tv_sec )
 - 
                       +       (tv_end.tv_usec - tv_start.tv_usec);
 - #ifdef USE_SPINLOCK
 - fprintf(stderr,"thread num %d spinlock version
 -                     cost time %llu\n",NR_THREAD,interval);
 - 
#else
 - fprintf(stderr,"thread num %d mutex version
 -                     cost time %llu\n",NR_THREAD,interval);
 - #endif
 - 
    return 0;
 - }
 
 1 临界区小,线程个数为2 
- root@libin:~/program/C/thread/thread_lock_cmp# time ./mutex_2_comp
 - 
thread num 2 mutex     version cost time 193155
 - 
real    0m0.195s
 - 
user    0m0.208s
 - 
sys    0m0.172s
 - 
root@libin:~/program/C/thread/thread_lock_cmp# time ./spinlock_2_comp
 - 
thread num 2 spinlock  version cost time 179761
 - 
real    0m0.181s
 - 
user    0m0.360s
 - sys 0m0.000s
 
性能上看差不多,这是由于线程数比较小,竞争不激烈。关注下sys 时间,mutex锁版本的时间大,因为它会存在争不到锁而调用system wait情况。
2 临界区小,线程个数为10
- root@libin:~/program/C/thread/thread_lock_cmp# time ./mutex_10_comp
 - thread num 10 mutex version cost time 1456112
 - real 0m1.458s
 - user 0m1.840s
 - sys 0m3.808s
 - root@libin:~/program/C/thread/thread_lock_cmp# time ./spinlock_10_comp
 - thread num 10 spinlock version cost time 2425690
 - real 0m2.427s
 - user 0m9.577s
 - sys 0m0.016s
 - root@libin:~/program/C/thread/thread_lock_cmp#
 
看下10个线程的情况,自旋锁性能已经明显不如线程锁了。因为竞争变得激烈了。我使用systemtap观察了进程调度的频繁程度,每秒统计一次上下文切换的次数
- root@libin:~/program/systemtap# cat sched.stp
 - global cnt;
 - probe scheduler.cpu_on {cnt<<<1;}
 - probe timer.s(1){printf("%d\n", @count(cnt)); delete cnt;}
 - probe timer.s(40){exit();}
 - root@libin:~/program/systemtap#
 
线程锁上下文切换的情况:
- 2393
 - 2275
 - 2156
 - 122098
 - 72827
 - 2741
 - 4760
 - 3159
 
看到中间有两个比较大的值,就是因为我执行了mutex版本的程序,而程序执行时间只有1.5秒左右,所以只有两个比较大的值。这就证明了mutex锁存在激烈竞争的情况下,会出现大量的上下文切换。
自旋锁版本执行期间,上下文切换没有明显变化,表明自旋锁不会引发上下文切换。它原地死循环。
3 临界区小,竞争特别激烈 100个线程。
先说mutex锁的情况:
- root@libin:~/program/C/thread/thread_lock_cmp# time ./mutex_100_comp 
 - 
thread num 100 mutex     version cost time 15101059
 - 
real    0m15.103s
 - 
user    0m18.337s
 - sys 0m40.827s
 
执行systemtap脚本的输出:
- 3567
 - 2245
 - 2291
 - 82863
 - 122166
 - 110381
 - 126612
 - 126960
 - 124175
 - 126085
 - 126417
 - 120905
 - 119271
 - 120717
 - 125181
 - 124713
 - 126694
 - 125177
 - 51845
 - 4633
 - 2633
 
就像10个线程的情况一样,在执行mutex版本期间,发生了大量的上下文切换。
top的输出如下:
- top - 12:46:38 up  2:27,  4 users,  load average: 16.72, 7.52, 3.88
 - 
Tasks: 223 total,   3 running, 220 sleeping,   0 stopped,   0 zombie
 - 
Cpu0  : 35.0%us, 64.7%sy,  0.0%ni,  0.3%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
 - 
Cpu1  : 32.4%us, 67.0%sy,  0.3%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
 - 
Cpu2  : 35.0%us, 65.0%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
 - 
Cpu3  : 34.8%us, 64.5%sy,  0.0%ni,  0.3%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
 - 
Mem:   1985648k total,  1441328k used,   544320k free,   110548k buffers
 - 
Swap:  1951736k total,        0k used,  1951736k free,   521980k cached
 - 
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                  
 - 5774 root 20 0 802m 860 368 S 392 0.0 0:39.27 mutex_100_comp
 
可以看出,系统时间占60%以上,这是因为有进程调度。
下面看自旋锁,自旋锁就比较悲惨了,我起来自旋锁版本后,先去泡了壶茶,太慢了。
- root@libin:~/program/C/thread/thread_lock_cmp# time ./spinlock_100_comp 
 - 
thread num 100 spinlock  version cost time 233026239
 - 
real    3m53.028s
 - 
user    15m18.985s
 - sys 0m1.712s
 
   上下文调度的情况我就不贴了,没有超3000次/s的。
   贴下top的情况
- top - 12:49:45 up  2:30,  4 users,  load average: 45.98, 15.50, 7.02
 - 
Tasks: 230 total,   1 running, 229 sleeping,   0 stopped,   0 zombie
 - 
Cpu0  :100.0%us,  0.0%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
 - 
Cpu1  : 99.4%us,  0.0%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.6%si,  0.0%st
 - 
Cpu2  :100.0%us,  0.0%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
 - 
Cpu3  : 98.4%us,  1.3%sy,  0.0%ni,  0.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
 - 
Mem:   1985648k total,  1471804k used,   513844k free,   111164k buffers
 - 
Swap:  1951736k total,        0k used,  1951736k free,   523212k cached
 - 
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                  
 - 5875 root 20 0 802m 860 368 S 395 0.0 2:24.78 spinlock_100_co
 
程序执行期间,CPU被浪费,我执行其他的任务,电脑特别的卡,卡的不能忍受。
临界区大的情况我就不继续写了。有兴趣的同学可以自己测试一下:
结论:
1 自旋锁适用于竞争不激烈,线程数较少,并且临界区小的情况。
2 线程锁竞争激烈的情况下,引发大量的上下文切换。所以由于竞争的存在,并不是线程愈多,效率越高。
3 保险情况下使用线程锁,因为,极端情况下,自旋锁不停的自旋,浪费CPU,影响效率。
参考文献:
1 Pthreads并行编程之spin lock与mutex性能对比分析
2 latencytop深度了解你的Linux系统的延迟
3 UNIX系统编程
                    
                
                
            
        
浙公网安备 33010602011771号