嵌入式Linux-线程同步-自旋锁和读写锁


一、自旋锁

1.1 自旋锁概述

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

如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。

  1. 由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者是否已经释放了锁,“自旋”一词因此得名。
  2. 自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。
  3. 试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为PTHREAD_MUTEX_ERRORCHECK 类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。
  4. 因此我们要谨慎使用自旋锁,自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!

总结下自旋锁与互斥锁之间的区别:

  1. 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
  2. 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
  3. 使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!

1.2 自旋锁的初始化

自旋锁使用 pthread_spinlock_t 数据类型表示,当定义自旋锁后,需要使用 pthread_spin_init()函数对其进行初始化,当不再使用自旋锁时,调用 pthread_spin_destroy()函数将其销毁,其函数原型如下所示:

#include <pthread.h>

int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

参数 lock 指向了需要进行初始化或销毁的自旋锁对象,参数 pshared 表示自旋锁的进程共享属性,可以取值如下:

  1. PTHREAD_PROCESS_SHARED:共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
  2. PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁。
    返回值正常都是0;

1.3 自旋锁加锁和解锁

可以使用 pthread_spin_lock()函数或 pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为 EBUSY。不管以何种方式加锁,自旋锁都可以使用 pthread_spin_unlock()函数对自旋锁进行解锁。其函数原型如下所示:

#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

如果自旋锁处于未锁定状态,调用 pthread_spin_lock()会将其锁定(上锁),如果其它线程已经将自旋锁锁住了,那本次调用将会“自旋”等待;如果试图对同一自旋锁加锁两次必然会导致死锁。

我们通过以下案例,看看如何使用自旋锁替换互斥锁来实现线程同步,对共享资源的访问进行保护。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_spinlock_t spin;//定义自旋锁
static int g_count = 0;

static void *new_thread_start(void *arg)
{
	 int loops = *((int *)arg);
	 int l_count, j;
	 for (j = 0; j < loops; j++) 
	 {
		 pthread_spin_lock(&spin); //自旋锁上锁
		 
		 l_count = g_count;
		 l_count++;
		 g_count = l_count;
		 
		 pthread_spin_unlock(&spin);//自旋锁解锁
	 }
	 return (void *)0;
}

static int loops;

int main(int argc, char *argv[])
{
	 pthread_t tid1, tid2;
	 int ret;
	 /* 获取用户传递的参数 */
	 if (2 > argc)
	 loops = 10000000; //没有传递参数默认为 1000 万次
	 else
	 loops = atoi(argv[1]);
	 /* 初始化自旋锁(私有) */
	 pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
	 /* 创建 2 个新线程 */
	 ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 
	 ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 
	 /* 等待线程结束 */
	 ret = pthread_join(tid1, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 
	 ret = pthread_join(tid2, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 打印结果 */
	 printf("g_count = %d\n", g_count);
	 /* 销毁自旋锁 */
	 pthread_spin_destroy(&spin);
	 exit(0);
}

在这里插入图片描述
将互斥锁替换为自旋锁之后,测试结果打印也是没有问题的,并且通过对比可以发现,替换为自旋锁之后,程序运行所耗费的时间明显变短了,说明自旋锁确实比互斥锁效率要高,但是一定要注意自旋锁所适用的场景。

二、读写锁

2.1 何为读写锁

互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有3 种状态:

  1. 读模式下的加锁状态
  2. 写模式下的加锁状态
  3. 不加锁状态
    tips: 一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!

在这里插入图片描述

读写锁有如下两个规则:

  1. 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
  2. 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。

所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;当读写锁处于读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。所以在应用程序当中,使用读写锁实现线程同步,当线程需要对共享数据进行读操作时,需要先获取读模式锁(对读模式锁进行加锁),当读取操作完成之后再释放读模式锁(对读模式锁进行解锁);当线程需要对共享数据进行写操作时,需要先获取到写模式锁,当写操作完成之后再释放写模式锁。

读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。

2.2 读写函数初始化

与互斥锁、自旋锁类似,在使用读写锁之前也必须对读写锁进行初始化操作,读写锁使用pthread_rwlock_t 数据类型表示,读写锁的初始化可以使用宏 PTHREAD_RWLOCK_INITIALIZER 或者函数pthread_rwlock_init(),其初始化方式与互斥锁相同,譬如使用宏 PTHREAD_RWLOCK_INITIALIZER 进行初始化必须在定义读写锁时就对其进行初始化:

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

对于其它方式可以使用 pthread_rwlock_init()函数对其进行初始化,当读写锁不再使用时,需要调用pthread_rwlock_destroy()函数将其销毁,其函数原型如下所示:

#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

参数 rwlock 指向需要进行初始化或销毁的读写锁对象。对于 pthread_rwlock_init()函数,参数 attr 是一个 pthread_rwlockattr_t *类型指针,指向 pthread_rwlockattr_t 对象。pthread_rwlockattr_t 数据类型定义了读写锁的属性,若将参数 attr 设置为 NULL,则表示将读写锁的属性设置为默认值,在这种情况下其实就等价于 PTHREAD_RWLOCK_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查。

读写锁初始化使用示例:

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
......
pthread_rwlock_destroy(&rwlock);

2.3 读写锁上锁和解锁

以读模式对读写锁进行上锁,需要调用 pthread_rwlock_rdlock()函数;
以写模式对读写锁进行上锁,需要调用 pthread_rwlock_wrlock()函数。
不管是以何种方式锁住读写锁,均可以调用 pthread_rwlock_unlock()函数解锁,其函数原型如下所示:

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

当读写锁处于写模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()或 pthread_rwlock_wrlock()函数均会获取锁失败,从而陷入阻塞等待状态;当读写锁处于读模式加锁状态时,其它线程调用pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用pthread_rwlock_wrlock()函数则不能获取到锁,从而陷入阻塞等待状态。

如果线程不希望被阻塞,可以调用 pthread_rwlock_tryrdlock()和 pthread_rwlock_trywrlock()来尝试加锁,如果不可以获取锁时。这两个函数都会立马返回错误,错误码为 EBUSY。其函数原型如下所示:

#include <pthread.h>

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

举例:全局变量 g_count 作为线程间的共享变量,主线程中创建了 5 个读取 g_count 变量的线程,它们使用同一个函数 read_thread,这 5 个线程仅仅对 g_count 变量进行读取,并将其打印出来,连带打印线程的编号1-5; 主线程中还创建了 5 个写 g_count 变量的线程,它们使用同一个函数 write_thread,write_thread 函数中会将 g_count 变量的值进行累加,循环 10 次,每次将 g_count 变量的值在原来的基础上增加 20,并将其打印出来,连带打印线程的编号(1~5)。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_rwlock_t rwlock;//定义读写锁
static int g_count = 0;

static void *read_thread(void *arg)
{
	 int number = *((int *)arg);
	 int j;
	 for (j = 0; j < 10; j++) 
	 {
		 pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
		 printf("读线程<%d>, g_count=%d\n", number+1, g_count);
		 pthread_rwlock_unlock(&rwlock);//解锁
		 sleep(1);
	 }
	 return (void *)0;
}

static void *write_thread(void *arg)
{
	 int number = *((int *)arg);
	 int j;
	 for (j = 0; j < 10; j++) 
	 {
		 pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
		 printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);
		 pthread_rwlock_unlock(&rwlock);//解锁
		 sleep(1);
	 }
	 return (void *)0;
}

static int nums[5] = {0, 1, 2, 3, 4};

int main(int argc, char *argv[])
{
	 pthread_t tid[10];
	 int j;
	 
	 /* 对读写锁进行初始化 */
	 pthread_rwlock_init(&rwlock, NULL);
	 
	 /* 创建 5 个读 g_count 变量的线程 */
	 for (j = 0; j < 5; j++)
	 pthread_create(&tid[j], NULL, read_thread, &nums[j]);
	 
	 /* 创建 5 个写 g_count 变量的线程 */
	 for (j = 0; j < 5; j++)
	 pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
	 
	 /* 等待线程结束 */
	 for (j = 0; j < 10; j++)
	 pthread_join(tid[j], NULL);//回收线程
	 /* 销毁自旋锁 */
	 pthread_rwlock_destroy(&rwlock);
	 exit(0);
}

在这里插入图片描述
在这个例子中,我们演示了读写锁的使用,但仅作为演示使用,在实际的应用编程中,需要根据应用场景来选择是否使用读写锁。

2.4 读写锁的属性

读写锁与互斥锁类似,也是有属性的,读写锁的属性使用 pthread_rwlockattr_t 数据类型来表示,当定义pthread_rwlockattr_t 对象时,需要使用 pthread_rwlockattr_init()函数对其进行初始化操作,初始化会将pthread_rwlockattr_t 对象定义的各个读写锁属性初始化为默认值;当不再使用 pthread_rwlockattr_t 对象时,需要调用 pthread_rwlockattr_destroy()函数将其销毁,其函数原型如下所示:

#include <pthread.h>

int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

attr :指向需要进行初始化或销毁的 pthread_rwlockattr_t 对象;

读写锁只有一个属性,那便是进程共享属性,它与互斥锁以及自旋锁的进程共享属性相同。Linux 下提供了相应的函数用于设置或获取读写锁的共享属性。函 数 pthread_rwlockattr_getpshared() 用于从pthread_rwlockattr_t 对象中获取共享属性,函数 pthread_rwlockattr_setpshared()用于设置 pthread_rwlockattr_t对象中的共享属性,其函数原型如下所示:

#include <pthread.h>

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

函数 pthread_rwlockattr_getpshared()参数和返回值:

  1. attr:指向 pthread_rwlockattr_t 对象;
  2. pshared:调用 pthread_rwlockattr_getpshared()获取共享属性,将其保存在参数 pshared 所指向的内存中;
  3. 返回值:成功返回 0,失败将返回一个非 0 值的错误码。

函数 pthread_rwlockattr_setpshared()参数和返回值:

  1. attr:指向 pthread_rwlockattr_t 对象;
  2. pshared:调用 pthread_rwlockattr_setpshared()设置读写锁的共享属性,将其设置为参数 pshared 指定的值。参数 pshared 可取值如下:
    ⚫ PTHREAD_PROCESS_SHARED:共享读写锁。该读写锁可以在多个进程中的线程之间共享;
    ⚫ PTHREAD_PROCESS_PRIVATE:私有读写锁。只有本进程内的线程才能够使用该读写锁,这是读写锁共享属性的默认值。

使用方式如下:

pthread_rwlock_t rwlock; //定义读写锁
pthread_rwlockattr_t attr; //定义读写锁属性

/* 初始化读写锁属性对象 */
pthread_rwlockattr_init(&attr);

/* 将进程共享属性设置为 PTHREAD_PROCESS_PRIVATE */
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);

/* 初始化读写锁 */
pthread_rwlock_init(&rwlock, &attr);

......

/* 使用完之后 */
pthread_rwlock_destroy(&rwlock); //销毁读写锁
pthread_rwlockattr_destroy(&attr); //销毁读写锁属性对象

本文参考正点原子的嵌入式LinuxC应用编程。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

The endeavor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值