Skip to content

LV015-自旋锁

本文主要是线程同步——自旋锁的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、自旋锁的概念

1. 基本概念与特性

自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。

如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。这与互斥锁很相似,都是为了保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者是否已经释放了锁,“自旋”一词因此得名。

自旋锁的不足之处在于:自旋锁一直占用的CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着CPU,如果不能在很短的时间内获取锁,这会使CPU效率降低。

2. 应用场景

自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!

3. 与互斥锁的比较

  • 实现方式上的区别

互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;

  • 开销上的区别

获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得CPU使用效率降低,故自旋锁不适用于等待时间比较长的情况。

  • 使用场景的区别

自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁。

【注意事项】试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为PTHREAD_MUTEX_ERRORCHECK类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。

二、基本操作

1. 自旋锁初始化

1.1 pthread_spin_init()

1.1.1 函数说明

linux下可以使用man pthread_spin_init命令查看该函数的帮助手册。

c
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

函数说明】该函数初始化一个自旋锁。

函数参数

  • lockpthread_spinlock_t *类型,指向需要进行初始化操作的自旋锁对象
  • psharedint类型,表示自旋锁的进程共享属性。pshared 可以取的值如下:
PTHREAD_PROCESS_SHARED 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享
PTHREAD_PROCESS_PRIVATE私有自旋锁。只有本进程内的线程才能够使用该自旋锁
【**返回值**】`int`类型,成功返回`0`;失败将返回一个非`0`的错误码。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_spinlock_t spin;/*定义自旋锁 */
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);/* 初始化自旋锁(私有) */

注意事项none

1.1.2 使用实例

暂无。

2. 加锁和解锁

2.1 pthread_spin_lock()

2.1.1 函数说明

linux下可以使用man pthread_spin_lock命令查看该函数的帮助手册。

c
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);

函数说明】该函数将对自旋锁加锁,获取自旋锁。

函数参数

  • lockpthread_spinlock_t *类型,指向已经初始化的自旋锁对象

返回值int类型,成功返回0;失败将返回一个非0的错误码。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_spinlock_t spin;/* 定义自旋锁 */
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);/* 初始化自旋锁(私有) */

pthread_spin_lock(&spin); /*自旋锁上锁 */

注意事项none

2.1.2 使用实例

暂无。

2.2 pthread_spin_trylock()

2.2.1 函数说明

linux下可以使用man pthread_spin_trylock命令查看该函数的帮助手册。

c
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_spin_trylock(pthread_spinlock_t *lock);

函数说明】该函数将对自旋锁加锁,获取自旋锁。类似于pthread_spin_lock(),不同之处在于,如果lock引用的自旋锁当前处于锁定状态,那么调用将立即返回错误EBUSY,而不是自旋。

函数参数

  • lockpthread_spinlock_t *类型,指向已经初始化过的自旋锁对象

返回值int类型,成功返回0;失败将返回一个非0的错误码。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_spinlock_t spin;/*定义自旋锁 */
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);/* 初始化自旋锁(私有) */

pthread_spin_trylock(&spin); /*自旋锁上锁 */

注意事项none

2.2.2 使用实例

暂无。

2.3 pthread_spin_unlock()

2.3.1 函数说明

linux下可以使用man pthread_spin_unlock命令查看该函数的帮助手册。

c
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_spin_unlock(pthread_spinlock_t *lock);

函数说明】该函数用于对自旋锁解锁、释放自旋锁。

函数参数

  • lockpthread_spinlock_t *类型,指向已经初始化过的自旋锁对象

返回值int类型,成功返回0;失败将返回一个非0的错误码。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_spinlock_t spin;/*定义自旋锁 */
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);/* 初始化自旋锁(私有) */

pthread_spin_lock(&spin); /*自旋锁上锁 */
pthread_spin_unlock(&spin); /* 自旋锁解锁 */

注意事项none

2.3.2 使用实例

暂无。

3. 自旋锁销毁

3.1 pthread_spin_destroy()

3.1.1 函数说明

linux下可以使用man pthread_spin_destroy命令查看该函数的帮助手册。

c
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);

函数说明】该函数销毁一个不再使用的自旋锁。

函数参数

  • lockpthread_spinlock_t *类型,指向需要进行销毁的自旋锁对象

返回值int类型,成功返回0;失败将返回一个非0的错误码。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_spinlock_t spin;/*定义自旋锁 */
pthread_spin_destroy(&spin);/* 销毁自旋锁 */

注意事项none

3.1.2 使用实例

暂无。

4. 自旋锁使用实例

4.1 测试源码

c
#include <stdio.h>  /* fopen fputc*/
#include <pthread.h>/* pthread_create pthread_exit */
#include <unistd.h> /* sleep */
#include <string.h> /* strlen */

FILE *fp;/* 定义一个文件指针变量 */

#define macro_spin 0 /* 0,不使用自旋锁;1,使用自旋锁 */
#if macro_spin == 1
pthread_spinlock_t spin;/* 定义自旋锁 */
#endif

void *threadMutex1(void *arg);
void *threadMutex2(void *arg);

int main(int argc, char *argv[])
{
	int ret;
	pthread_t tid1, tid2;
	/* 创建自旋锁(私有) */
#if macro_spin == 1
	pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
#endif
	/* 1. 以追加读写的方式使用标准IO打开一个文件 */
	fp = fopen("test.txt", "a+");
	/* 2. 判断是否打开成功 */
	if(fp == NULL)
	{
		perror("open error");
		return -1;
	}

	/* 3. 创建两个线程 */
	ret = pthread_create(&tid1, NULL, threadMutex1, NULL);
	printf("This is threadMutex1 thread create,ret=%d,tid1=%lu\n", ret, tid1);
	ret = pthread_create(&tid2, NULL, threadMutex2, NULL);
	printf("This is threadMutex2 thread create,ret=%d,tid2=%lu\n", ret, tid2);
	/* 4. 等待回收线程(设置线程分离) */

	while(1)
	{
		sleep(1);
	}
	return 0;
}

void *threadMutex1(void *arg)
{
	int i = 0;
	char str[]="I write threadMutex1 line\n";
	/* 设置线程分离,结束后自动回收 */
	pthread_detach(pthread_self());
	printf("This is a threadMutex1 thread!\n");
	/* 按字符将字符串写入文件 */
	while(1)
	{
#if macro_spin == 1
		pthread_spin_lock(&spin); /* 自旋锁上锁 */
#endif
		for(i = 0; i<strlen(str); i++)
		{
			fputc(str[i], fp);
			usleep(100);
		}
#if macro_spin == 1
		pthread_spin_unlock(&spin); /* 自旋锁解锁 */
#endif
		usleep(1000);
	}
	pthread_exit("threadMutex1 exit!");
}

void *threadMutex2(void *arg)
{
	int i = 0;
	char str[]="I write threadMutex2 line\n";
	/* 设置线程分离,结束后自动回收 */
	pthread_detach(pthread_self());
	printf("This is a threadMutex2 test!\n");
	/* 按字符将字符串写入文件 */
	while(1)
	{
#if macro_spin == 1
		pthread_spin_lock(&spin); /* 自旋锁上锁 */
#endif
		for(i = 0; i<strlen(str); i++)
		{
			fputc(str[i], fp);
			usleep(100);
		}
#if macro_spin == 1
		pthread_spin_unlock(&spin); /* 自旋锁解锁 */
#endif
		usleep(1000);
	}

	pthread_exit("threadMutex2 exit!");

}

程序中的宏macro_spin用于决定是否使用自旋锁。

  • macro_spin0,则不使用自旋锁。
  • macro_spin1,则使用自旋锁。

4.2 macro_spin == 0

在终端执行以下命令编译程序:

shell
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

shell
This is threadMutex1 thread create,ret=0,tid1=140129428600384
This is a threadMutex1 thread!
This is threadMutex2 thread create,ret=0,tid2=140129420207680
This is a threadMutex2 test!

然后我们打开test.txt文件,查看文件内容如下:

shell
II  wrwriittee  tthhrreeaaddMutMuext2ex l1 inlei
ne
I wIr iwtre itteh rtheraedaMduMtuetxe2x 1l ilnien
e
I Iwr iwrtiete  tthhrreaeaddMuMutteexx21 l liinnee
# 后边的省略 ......

该文件被两个线程共享,两个线程都是写入,未保护共享文件,导致文件写入出错。

4.3 macro_spin == 1

在终端执行以下命令编译程序:

shell
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

shell
This is threadMutex1 thread create,ret=0,tid1=140667718608448
This is a threadMutex1 thread!
This is threadMutex2 thread create,ret=0,tid2=140667710215744
This is a threadMutex2 test!

然后我们打开test.txt文件,查看文件内容如下:

shell
I write threadMutex1 line
I write threadMutex2 line
I write threadMutex1 line
I write threadMutex2 line
# 后边的省略 ......

该文件被两个线程共享,两个线程都是写入,使用自旋锁保护共享文件,文件写入正常。