概述
文章目录
- 1 前言
- 2 自旋锁
- 2.1 自旋锁特点
- 2.2 自旋锁适用场景
- 2.3 自旋锁使用原则
- 3 自旋锁使用
- 3.1 创建自旋锁
- 3.2 初始化自旋锁
- 3.3 自旋锁上锁(申请锁)
- 3.3.1 阻塞方式
- 3.3.2 非阻塞方式
- 3.4 自旋锁释放
- 3.5 自旋锁销毁
- 3.6 写个例子
- 4 自旋锁属性
- 5 总结
1 前言
前面文章分别描述了互斥锁和读写锁的含义、属性、使用原则、使用场景以及使用方法。本文描述除了互斥锁、读写锁外的第三种锁——自旋锁。
2 自旋锁
自旋锁( Spin lock )是线程间互斥的一种机制。自旋锁本质是一把锁,实现的功能与互斥锁完全一样,都是任一时刻只允许一个线程持有锁,达到互斥访问共享资源的目的。唯一的不同之处在于两者的调度策略不一样,线程申请不到互斥锁时,会使线程睡眠让出cpu资源,获得互斥锁后线程唤醒继续执行;而自旋锁阻塞后不会引起线程睡眠,一直占用cpu资源直至获得自旋锁。自旋锁是一种轻量级的锁,相比互斥锁,资源开销更小,在极短时间的加锁,自旋锁是最理想的选择,可以提高效率。
2.1 自旋锁特点
自旋锁的特点与其命名匹配,线程获取不到锁时就是一直处于忙等待(原地打转?)状态,占用cpu的同时又不能处理任何任务。根据自旋锁的特点,自旋锁适用于占用锁时间极短的场景,长时间占用自旋锁会降低系统性能。如果访问资源比较耗时,需长时间持有锁的场景,则需考虑其他互斥机制。
- 用于线程互斥
- 阻塞一直占用cpu资源
- 不可引起线程睡眠
- 轻量级的锁
- 资源开销小,包括创建、持有、释放过程
2.2 自旋锁适用场景
自旋锁一开始是为防止多核处理器(SMP)并发带来竞态而引入的一种互斥机制。 自旋锁在用户态使用得比较少,在内核态下,常见的驱动开发会经常用到自旋锁。内核态下的自旋锁使用可以参考文章并发与竞态(如何选择合适的保护机制)。自旋锁适用于短期内进行轻量级的锁定。
- 互斥资源访问时间极短(加锁时间短),小于2次上下文切换的时间
- 特殊场景,不希望挂起线程
2.3 自旋锁使用原则
自旋锁与互斥锁一样,自旋锁使用原则可以参考互斥锁的使用原则,互斥锁的使用原则也是自旋锁的基本使用原则。
- 加锁时间极短,并及时释放锁
- 禁止嵌套(递归)申请持有自旋锁,否则导致死锁
- 避免过多的自旋锁申请,防止cpu资源浪费
注:
申请持有自旋锁时会一直占用cpu,如果嵌套或者递归申请自旋锁,在第二层申请锁时,由于锁被第一层持有,第二层获取不到锁一直处于等待状态并占用cpu,程序也无法跳出到最外层释放锁,导致死锁发生。因此,递归程序中使用自旋锁需谨慎
3 自旋锁使用
自旋锁使用的基本步骤为:
【1】创建自旋锁实例
【2】初始化自旋锁
【3】持有自旋锁
【4】释放自旋锁
【5】销毁自旋锁实例
3.1 创建自旋锁
posix线程自旋锁以pthread_spinlock_t
数据结构表示。自旋锁实例可以用静态和动态创建。
pthread_spinlock_t spinlock;
3.2 初始化自旋锁
自旋锁初始化只支持使用pthread_rwlock_init
函数进行动态初始化 。
int pthread_spin_init(pthread_spinlock_t *spinlock, int pshared);
-
spinlock
,自旋锁实例地址,不能为NULL -
pshared
,自旋锁作用域PTHREAD_PROCESS_PRIVATE
,进程内(创建者)作用域,只能用于进程内线程互斥PTHREAD_PROCESS_SHARED
,跨进程作用域,用于系统所有线程间互斥 -
返回,成功返回0,参数无效返回 EINVAL
3.3 自旋锁上锁(申请锁)
自旋锁申请持有分为阻塞方式和非阻塞方式,常用的一般是阻塞方式。
3.3.1 阻塞方式
int pthread_spin_lock(pthread_spinlock_t *spinlock);
-
spinlock
,自旋锁实例地址,不能为NULL -
返回,成功返回0,参数无效返回 EINVAL
如果自旋锁还没有被其他线程持有(上锁),则申请持有自旋锁的线程获得锁。如果自旋锁被其他线程持有,则线程一直处于等待状态(占用cpu),直到持自旋锁的线程解锁后,线程获得锁继续执行。不允许递归嵌套申请自旋锁,否则导致死锁。
3.3.2 非阻塞方式
int pthread_spin_trylock(pthread_spinlock_t spinlock*);
spinlock
,自旋锁实例地址,不能为NULL- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 参数无效 |
EDEADLK | 死锁 |
EBUSY | 锁被其他线程持有 |
调用该函数会立即返回,不会阻塞等待。实际应用可以根据返回状态执行不同的任务操作。
3.4 自旋锁释放
int pthread_spin_unlock(pthread_spinlock_t *spinlock);
spinlock
,自旋锁实例地址,不能为NULL- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 参数无效 |
EDEADLK | 死锁 |
EBUSY | 锁被其他线程持有 |
自旋锁持有后必须及时释放,不允许多次释放锁。
3.5 自旋锁销毁
int pthread_spinlock_destroy(pthread_spinlock_t *spinlock);
spinlock
,自旋锁实例地址,不能为NULL- 返回
返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | spinlock已被销毁过,或者spinlock为空 |
EBUSY | 自旋锁被其他线程使用 |
pthread_spinlock_destroy
用于销毁一个已经使用动态初始化的自旋锁。销毁后的自旋锁处于未初始化状态,自旋锁的属性和控制块参数处于不可用状态。使用销毁函数需要注意几点:
- 已销毁的自旋锁,可以使用
pthread_spinlock_init
重新初始化使用 - 不能重复销毁已销毁的自旋锁
- 没有线程持有自旋锁时,才能销毁
3.6 写个例子
代码实现功能:
- 创建两个线程
- 两个线程分别对全局变量访问,并输出到终端
- 期望结果,线程1输出结果“ 1 2 3 4 5”,线程2输出结果“5 4 3 2 1”
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include "pthread.h"
#define USE_SPINLOCK 1 /* 是否使用自旋锁,使用,0不使用 */
#if USE_SPINLOCK
pthread_spinlock_t spinlock;
#endif
static int8_t g_count = 0;
void *thread0_entry(void *data)
{
uint8_t i =0;
#if USE_SPINLOCK
pthread_spin_lock(&spinlock);
#endif
for (i = 0;i < 5;i++)
{
g_count ++;
printf("%d ", g_count);
usleep(100);
}
printf("rn");
#if USE_SPINLOCK
pthread_spin_unlock(&spinlock);
#endif
}
void *thread1_entry(void *data)
{
uint8_t i =0;
usleep(10); /* 让线程0先执行 */
#if USE_SPINLOCK
pthread_spin_lock(&spinlock);
#endif
for (i = 0;i < 5;i++)
{
printf("%d ", g_count);
g_count--;
usleep(100);
}
printf("rn");
#if USE_SPINLOCK
pthread_spin_unlock(&spinlock);
#endif
}
int main(int argc, char **argv)
{
pthread_t thread0,thread1;
void *retval;
#if USE_SPINLOCK
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);/* 进程内作用域 */
#endif
pthread_create(&thread0, NULL, thread0_entry, NULL);
pthread_create(&thread1, NULL, thread1_entry, NULL);
pthread_join(thread0, &retval);
pthread_join(thread1, &retval);
return 0;
}
不加自旋锁的结果
由于不使用锁,线程间并发执行,"同时"访问全局变量g_count
及printf
输出,实际结果没有符合预期。
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ gcc spinlock.c -o spinlock -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ ./spinlock
1 2 2 2 2 2 2 1 1 1
使用自旋锁的结果
线程0持有锁之后,访问执行完后才释放锁,线程2申请到锁,输出结果正确。
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ gcc spinlock.c -o spinlock -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/spinlock$ ./spinlock
1 2 3 4 5
5 4 3 2 1
代码中,对printf
函数加锁,实际使用是不允许的,违反了加锁的原则,这里只是模拟场景测试。
4 自旋锁属性
自旋锁是一种轻量级的锁,属性只有一个“作用域”,在调用pthread_spin_init
函数初始化自旋锁时指定作用域范围。自旋锁作用域表示自旋锁的互斥作用范围,分为进程内(创建者)作用域PTHREAD_PROCESS_PRIVATE
和跨进程作用域PTHREAD_PROCESS_SHARED
。进程内作用域只能用于进程内线程互斥,跨进程可以用于系统所有线程间互斥。
5 总结
自旋锁实现的功能与互斥锁一样,都是用于线程间互斥访问。自旋锁是一种不会引起线程睡眠的轻量级锁,适用于加锁时间极短的场景,由于其资源开销比互斥锁低,在极短的加锁场景使用自旋锁效率会更高。自旋锁的使用注意事项,结合互斥锁文章2.3节的"互斥锁使用原则",参考2.3节的“自旋锁使用原则”。至此,互斥锁、读写锁、自旋锁描述完成,三者的特点差异,罗列出下表比较。
互斥锁、读写锁、自旋锁对比
主要特点 | 引起线程睡眠 | 适用范围 | 资源开销 | |
---|---|---|---|---|
互斥锁 | 互斥 | 是 | 一般互斥访问 | 普通 |
读写锁 | 读读共享 | 是 | 多读少写 | 普通 |
自旋锁 | 自旋等待 | 否 | 加锁时间极短 | 低开销 |
最后
以上就是精明奇迹为你收集整理的【Linux应用编程】POSIX线程互斥与同步机制—自旋锁1 前言2 自旋锁3 自旋锁使用4 自旋锁属性5 总结的全部内容,希望文章能够帮你解决【Linux应用编程】POSIX线程互斥与同步机制—自旋锁1 前言2 自旋锁3 自旋锁使用4 自旋锁属性5 总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复