您的位置:首页 > 其它

Processing , Mutex and SpinLock

2011-12-30 17:16 316 查看
最近从事多线程相关的编程,对于多线程的性能比较关心,所以去网上找了一些资料。看到了并行实验室的冠诚前辈的博文 学习到了很多,下面是我的学习笔记。光荣属于前辈。

线程锁调用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系统编程
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: