前言
内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:
- 一个是原地等待,spinlock
- 一个是挂起当前进程,调度其他进程执行(睡眠)mutex
提示:以下是本篇文章正文内容,下面案例可供参考
一、互斥锁
互斥锁是保护临界资源,在并发机制的情况下,多个线程同时访问同一块资源,为了保护数据操作的准确性就需要通过加锁来进行保护。 保持操作互斥: 在并发机制的情况下,多个线程同时访问同一块资源,但是同一个时间只能有一个操作使用临界资源。
二、自旋锁
自旋锁( Spin lock )是线程间互斥的一种机制。自旋锁本质是一把锁,实现的功能与互斥锁完全一样,都是任一时刻只允许一个线程持有锁,达到互斥访问共享资源的目的; 自旋锁一开始是为防止多核处理器(SMP)并发带来竞态而引入的一种互斥机制。
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。对于自选锁需要注意:
- 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间.
- 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。(在内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起)
使用任何锁需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:
- 建立锁所需要的资源
- 当线程被阻塞时锁所需要的资源
特点:
自旋锁是“原地等待”的方式:线程获取不到锁时就是一直处于忙等待(原地打转?)状态,占用cpu的同时又不能处理任何任务;
自旋锁是一种轻量级的锁,相比互斥锁,资源开销更小,在极短时间的加锁,自旋锁是最理想的选择,可以提高效率。
- 用于线程互斥
- 阻塞一直占用cpu资源
- 不可引起线程睡眠
- 轻量级的锁 资源开销小,包括创建、持有、释放过程
- 单处理器非抢占内核下:自旋锁会在编译时被忽略;
- 单处理器抢占内核下:自旋锁仅仅当作一个设置内核抢占的开关;
- 多处理器下:此时才能完全发挥出自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。
场景:
自旋锁适用于占用锁时间极短的场景,长时间占用自旋锁会降低系统性能。如果访问资源比较耗时,需长时间持有锁的场景,则需考虑其他互斥机制。
- 互斥资源访问时间极短(加锁时间短),小于2次上下文切换的时间
- 特殊场景,不希望挂起线程
中断上下文要用锁,首选 spinlock
使用原则
自旋锁与互斥锁一样,自旋锁使用原则可以参考互斥锁的使用原则,互斥锁的使用原则也是自旋锁的基本使用原则。
- 加锁时间极短,并及时释放锁
- 禁止嵌套(递归)申请持有自旋锁,否则导致死锁
- 避免过多的自旋锁申请,防止cpu资源浪费
- 如果临界区可能包含引起睡眠的代码则不能使用自旋锁,否则可能引起死锁。
注: 申请持有自旋锁时会一直占用cpu,如果嵌套或者递归申请自旋锁,在第二层申请锁时,由于锁被第一层持有,第二层获取不到锁一直处于等待状态并占用cpu,程序也无法跳出到最外层释放锁,导致死锁发生。因此,递归程序中使用自旋锁需谨慎
自旋锁属性
自旋锁是一种轻量级的锁,属性只有一个“作用域”,在调用pthread_spin_init函数初始化自旋锁时指定作用域范围。自旋锁作用域表示自旋锁的互斥作用范围,分为进程内(创建者)作用域``THREAD_PROCESS_PRIVATE和跨进程作用域PTHREAD_PROCESS_SHARED。进程内作用域只能用于进程内线程互斥,跨进程可以用于系统所有线程间互斥。
三、两把锁的区别
1. 调度策略
线程申请不到互斥锁时,会使线程睡眠让出cpu资源,获得互斥锁后线程唤醒继续执行;而自旋锁阻塞后不会引起线程睡眠,一直占用cpu资源直至获得自旋锁。
2.使用场景
自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。
从实现原理上来讲,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()的系统调用并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。
对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
四、常用函数
1.互斥锁
互斥锁常用接口:
pthread_mutex_t lock; /* 互斥锁定义 */ pthread_mutex_init(&lock, NULL); /* 动态初始化, 成功返回0,失败返回非0 */ pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化 */ pthread_mutex_lock(&lock); /* 阻塞的锁定互斥锁 */ pthread_mutex_trylock(&thread_mutex);/* 非阻塞的锁定互斥锁,成功获得互斥锁返回0,如果未能获得互斥锁,立即返回一个错误码 */ pthread_mutex_unlock(&lock); /* 解锁互斥锁 */ pthread_mutex_destroy(&lock) /* 销毁互斥锁 */
2.自旋锁
2.0 自旋锁常用接口
自旋锁接口:
int pthread_spin_init(pthread_spinlock_t *, int); int pthread_spin_lock(pthread_spinlock_t *); int pthread_spin_trylock(pthread_spinlock_t *); int pthread_spin_unlock(pthread_spinlock_t *); int pthread_spin_destroy(pthread_spinlock_t *);
2.1 创建自旋锁
自旋锁静态创建:
posix线程自旋锁以pthread_spinlock_t数据结构表示
pthread_spinlock_t spinlock;
2.2 自旋锁初始化
自旋锁初始化:用来申请使用自旋锁所需要的资源并且将它初始化为非锁定状态。
int pthread_spin_init(pthread_spinlock_t *spinlock, int pshared); spinlock,自旋锁实例地址,不能为NULL pshared,自旋锁作用域 PTHREAD_PROCESS_PRIVATE,进程内(创建者)作用域,只能用于进程内线程互斥 仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。 PTHREAD_PROCESS_SHARED,跨进程作用域,用于系统所有进程包含线程间互斥。 返回,成功返回0,参数无效返回 EINVAL
2.2 自旋锁上锁
自旋锁申请持有分为阻塞方式和非阻塞方式,常用的一般是阻塞方式。
自旋锁上锁阻塞方式。
用来获取(锁定)指定的自旋锁。如果自旋锁还没有被其他线程持有(上锁),则申请持有自旋锁的线程获得锁。如果自旋锁被其他线程持有,则线程一直处于等待状态(占用cpu),直到持自旋锁的线程解锁后,线程获得锁继续执行。不允许递归嵌套申请自旋锁,否则导致死锁。
int pthread_spin_lock(pthread_spinlock_t *spinlock); spinlock,自旋锁实例地址,不能为NULL 返回,成功返回0,参数无效返回 EINVAL
自旋锁上锁阻非塞方式
int pthread_spin_trylock(pthread_spinlock_t *spinlock); spinlock,自旋锁实例地址,不能为NULL 返回值
返回值 | 描述 |
0 | 成功 |
EINVAL | 参数无效 |
EDEADLK | 死锁 |
EBUSY | 锁被其他线程持有 |
调用该函数会立即返回,不会阻塞等待。实际应用可以根据返回状态执行不同的任务操作。
2.3 自旋锁释放
释放指定的自旋锁。
int pthread_spin_unlock(pthread_spinlock_t *spinlock); spinlock,自旋锁实例地址,不能为NULL 返回
返回值 | 描述 |
0 | 成功 |
EINVAL | 参数无效 |
EDEADLK | 死锁 |
EBUSY | 锁被其他线程持有 |
自旋锁持有后必须及时释放,不允许多次释放锁
。
2.4 自旋锁销毁
自旋锁释销毁。
int pthread_spinlock_destroy(pthread_spinlock_t *spinlock); spinlock,自旋锁实例地址,不能为NULL 返回
返回值 | 描述 |
0 | 成功 |
EINVAL | spinlock已被销毁过,或者spinlock为空 |
EBUSY | 锁被其他线程持有 |
pthread_spinlock_destroy
用于销毁一个已经使用动态初始化的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)。销毁后的自旋锁处于未初始化状态,自旋锁的属性和控制块参数处于不可用状态。使用销毁函数需要注意几点:
- 已销毁的自旋锁,可以使用pthread_spinlock_init重新初始化使用
- 不能重复销毁已销毁的自旋锁
- 没有线程持有自旋锁时,才能销毁
总结
(1)Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。
(2)spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。
例子
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #undef USER_SPINLOCK #define USER_SPINLOCK 0 #define LOOP_MAX 5 pthread_spinlock_t spinlock; pthread_mutex_t mutex; int g_count = 0; void *pfuncf_call(void *arg){ int i = 0; pthread_detach(pthread_self()); printf("Producer :"); #if USER_SPINLOCK pthread_spin_lock(&spinlock); #else pthread_mutex_lock(&mutex); #endif for (i=0; i < LOOP_MAX; i++){ printf("%d ",g_count++); } printf("\n"); #if USER_SPINLOCK pthread_spin_unlock(&spinlock); #else pthread_mutex_unlock(&mutex); #endif } void *pfunc_call(void *argv) { printf("Cosumer: count:%d ",g_count); #if USER_SPINLOCK pthread_spin_lock(&spinlock); #else pthread_mutex_lock(&mutex); #endif while(g_count > 0) { printf("%d", g_count--); } printf("\n"); #if USER_SPINLOCK pthread_spin_unlock(&spinlock); #else pthread_mutex_unlock(&mutex); #endif } int main(int argc, char **argv){ // pthread_mutex_init(&mutex, NULL); #if USER_SPINLOCK pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED); #else pthread_mutex_init(&mutex, NULL); #endif pthread_t thid[3] = {0}; pthread_t thc; int i = 0; int ret = 0; for (i; i < 3; i++){ ret += pthread_create(&thid[i], NULL, pfuncf_call, NULL); } sleep(1); printf("-----%d\n",g_count); ret += pthread_create(&thc, NULL, pfunc_call, NULL); // pthread_join(thid[0], NULL); // pthread_join(thid[1], NULL); // pthread_join(thid[2], NULL); pthread_join(thc, NULL); return 0; }