【Linux应用编程】POSIX线程互斥与同步机制—自旋锁



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("\r\n");
#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("\r\n");
#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_countprintf输出,实际结果没有符合预期。

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节的“自旋锁使用原则”。至此,互斥锁、读写锁、自旋锁描述完成,三者的特点差异,罗列出下表比较。

互斥锁、读写锁、自旋锁对比

主要特点 引起线程睡眠 适用范围 资源开销
互斥锁 互斥 一般互斥访问 普通
读写锁 读读共享 多读少写 普通
自旋锁 自旋等待 加锁时间极短 低开销
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章