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



1 前言

  在上一篇文章中,主要描述了一些概念性的知识,包括互斥与同步、临界区与临界资源以及他们之间的含义、特性和联系,概括了POSIX线程间互斥与同步的机制。本文主要内容是关于最简单的互斥机制的—互斥锁。


2 互斥锁

  互斥锁(Mutex)是最简单的一种互斥机制。互斥锁,顾名思义就是一把锁,互斥的意思就是(线程)不能同时持有锁(上锁)。即任一时刻只有一个线程持有锁,其他线程需要持有锁时,必须等待其他持有线程释放锁(解锁)。

  利用互斥锁实现线程间互斥的原理是,通过加锁方法来实现对共享资源原子操作,保证共享资源的完整性 。由于任一时刻只有一个线程持有锁,也就是只有一个线程在访问共享资源,其他线程申请不到锁。一个线程申请互斥锁失败时,会主动放弃cpu资源,线程被系统挂起,直至申请到互斥锁再被唤醒,访问共享资源。


2.1 互斥锁特点

  • 不能同时持有锁
  • 上锁和解锁必须由同一线程操作
  • 可引起线程睡眠,线程申请不到互斥锁(被其他线程占用),线程进入睡眠状态

2.2 互斥锁适用场景

  • 多个资源共享
  • 临界区任务少(加锁时间短)

2.3 互斥锁使用原则

  • 锁的职责单一,每个锁只锁一个共享资源

    如果一个互斥锁对多个共享资源保护,不能保证每个共享资源的唯一性,因此,必须确保每个共享资源配一把锁。

  • 锁的范围尽可能小,加锁时间短

    锁的范围尽可能小,只对临界区进行加锁,减少加锁时间;长时间加锁导致,会增加线程串行时间,降低系统调度效率。因此,对锁操作的原则是,只锁共享资源访问代码区域,尽量在函数内部靠近资源操作的地方加锁而不是靠近线程和函数外部加锁,访问完共享资源,立即释放锁。

  • 尽量少用嵌套加锁

    原则上,不建议使用嵌套锁,容易造成死锁。必须使用嵌套锁时,必须确保上锁和解锁的嵌套顺序,否则易造成死锁发生。

  • 避免回调函数加锁

    遵循加锁范围尽可能小的原则,减少避免死锁概率。

  • 避免锁中有跳转语句

    注意锁范围的return、break、continue、goto等跳转语句,防止函数返回时未解锁,导致死锁。


3 互斥锁使用

  互斥锁使用的基本步骤为:

【1】创建互斥锁实例

【2】初始化互斥锁

【3】访问共享资源前上锁

【4】访问完资源后立即释放锁

【5】销毁互斥锁


3.1 创建互斥锁

  posix线程互斥锁以pthread_mutex_t数据结构表示。互斥锁实例可以用静态和动态创建。

pthread_mutex_t mutex;

3.2 初始化互斥锁

  互斥锁初始化可以使用pthread_mutex_init动态初始化,也可以使用宏 PTHREAD_MUTEX_INITIALIZER 实现静态初始化,PTHREAD_MUTEX_INITIALIZER是POSIX定义的一个结构体常量 。

动态初始化

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex,互斥锁实例地址,不能为NULL

  • attr,互斥锁属性地址,传入NULL表示使用默认属性;大部分场合使用默认属性即可,关于属性详见第四节。

  • 返回,成功返回0,参数无效返回 EINVAL


静态初始化

  使用宏PTHREAD_MUTEX_INITIALIZER的静态初始化方式等价于使用pthread_mutex_init采用默认属性(attr传入NULL)的动态初始化,不同之处在于PTHREAD_MUTEX_INITIALIZER宏没有相关错误参数的检查。

使用例子:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

3.3 互斥锁上锁(申请锁)

  互斥锁上锁分为阻塞方式和非阻塞方式,常用的一般是阻塞方式。

3.3.1 阻塞方式上锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex,互斥锁实例地址,不能为NULL

  • 返回

    返回值描述
    0成功
    EINVAL参数无效
    EDEADLK非嵌套锁重复申请锁

       如果互斥锁还没有被其他线程持有(上锁),则申请持有锁的线程获得锁。如果互斥锁已经被当前线程持有,且互斥锁属性设置的类型为嵌套锁,则该互斥锁的持有计数加 1,当前线程也不会挂起或者睡眠,线程必须根据嵌顺序依次解锁,否则造成死锁问题。如果互斥锁被其他线程持有,则当前线程将被阻塞,直到持有互斥锁的线程解锁后才唤醒继续执行,所有等待互斥锁的线程按照先进先出的原则获取互斥锁。


3.3.2 非阻塞上锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • mutex,互斥锁实例地址,不能为NULL
  • 返回
返回值描述
0成功
EINVAL参数无效
EDEADLK非嵌套锁重复申请锁
EBUSY锁被其他线程持有

  调用该函数会立即返回,不会引起线程睡眠。实际应用可以根据返回状态执行不同的任务操作。


3.4 互斥锁释放

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • mutex,互斥锁实例地址,不能为NULL
  • 返回
返回值描述
0成功
EINVAL参数无效
EPERM非嵌套锁重复释放锁
EBUSY锁被其他线程持有

3.5 互斥锁销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • mutex,互斥锁实例地址,不能为NULL
  • 返回
返回值描述
0成功
EINVALmutex已被销毁过,或者mutex为空
EBUSY锁被其他线程持有

  pthread_mutex_destroy用于销毁一个已经使用动态初始化的互斥锁。销毁后的互斥锁处于未初始化状态,互斥锁的属性和控制块参数处于不可用状态。使用销毁函数需要注意几点:

  • 已销毁的互斥锁,可以使用pthread_mutex_init重新初始化使用
  • 不能重复销毁已销毁的互斥锁
  • 使用宏PTHREAD_MUTEX_INITIALIZER静态初始化的互斥锁不能销毁
  • 没有线程持有锁时,且该锁没有阻塞任何线程,才能销毁

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_MUTEX 1	/* 是否使用互斥锁,使用,0不使用 */

#if USE_MUTEX
pthread_mutex_t mutex;
#endif

static int8_t g_count = 0;

void *thread0_entry(void *data)  
{
	uint8_t  i =0;

#if USE_MUTEX
	pthread_mutex_lock(&mutex);
#endif
	for (i = 0;i < 5;i++)
	{
		g_count ++;
		printf("%d ", g_count);
		usleep(100);
	}
#if USE_MUTEX
	pthread_mutex_unlock(&mutex);
#endif
	printf("\r\n");
}

void *thread1_entry(void *data)  
{
	uint8_t  i =0;
	
#if USE_MUTEX
	pthread_mutex_lock(&mutex);
#endif
	for (i = 0;i < 5;i++)
	{
		printf("%d ", g_count);
		g_count--;
		usleep(100);
	}
#if USE_MUTEX
	pthread_mutex_unlock(&mutex);
#endif
	printf("\r\n");
}

int main(int argc, char **argv)  
{
	pthread_t thread0,thread1; 
    void *retval; 
    
#if USE_MUTEX
	pthread_mutex_init(&mutex, NULL);
#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/mutex$ gcc mutex.c -o mutex -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/mutex$ ./mutex
1 1 0 0 0 0 1 1 1 1 

使用互斥锁的结果

  线程1持有锁之后,访问执行完后才释放锁,线程2申请到锁,输出结果正确。

acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/mutex$ gcc mutex.c -o mutex -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/mutex$ ./mutex
1 2 3 4 5 
5 4 3 2 1 

  代码中,使用了usleep函数模拟线程并发执行的情景;对printf函数上锁,实际使用是不允许的,违反了加锁的原则,这里只是模拟场景测试。


4 互斥锁属性

  使用默认的互斥锁属性可以满足绝大部分的应用场景,特殊场景也可以调整互斥锁属性。下面描述主要的互斥锁属性及API。互斥锁属性设置,基本步骤为:

【1】创建互斥锁属性实例

【2】初始化属性实例

【3】设置属性

【4】销毁属性实例


4.1 创建互斥锁属

  posix线程互斥锁属性以pthread_mutexattr_t 数据结构表示。互斥锁属性实例可以用静态和动态创建。

pthread_mutexattr_t attr;

4.2 互斥锁属性初始化与销毁

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
  • attr,互斥锁属性实例地址,不能为NULL
  • 成功返回0,参数无效返回 EINVAL

  设置线程属性时,首先创建一个属性pthread_mutexattr_t实例,然后调用pthread_mutexattr_init函数初始实例,接下来就是属性设置。初始化后的属性值就是默认互斥锁属性,等价于使用pthread_mutex_init采用默认属性(attr传入NULL)的初始化。


4.3 互斥锁作用域

  互斥锁作用域表示互斥锁的作用范围,分为进程内(创建者)作用域PTHREAD_PROCESS_PRIVATE和跨进程作用域PTHREAD_PROCESS_SHARED。进程内作用域只能用于进程内线程互斥,跨进程可以用于系统所有线程间互斥。

作用域设置与获取函数:

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int  pshared);
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);
  • attr,互斥锁属性实例地址,不能为NULL
  • pshared,作用域类型,PTHREAD_PROCESS_PRIVATEPTHREAD_PROCESS_SHARED
  • 成功返回0,参数无效返回 EINVAL

4.4 互斥锁类型

  互斥锁的类型决定了一个线程在申请锁时的呈现出的动作,常用的互斥锁类型有:

  • PTHREAD_MUTEX_NORMAL,普通锁,默认类型。一个线程持有锁后,其他线程申请锁会被阻塞挂起,形成一个等待队列;线程解锁后,等待线程按“先进先出”原则获取锁,保证资源分配的公平性。该类型的锁不检测死锁,一个线程未解锁情况下重复申请持有该锁,会形成死锁。
  • PTHREAD_MUTEX_RECURSIVE,嵌套锁。允许线程对同一个锁持多次申请持有,但持有者必须按照嵌套顺序依次解锁,否则造成死锁。一般不建议用嵌套锁,使用时许谨慎。
  • PTHREAD_MUTEX_ERRORCHEC,检错锁。改该类似锁可以检测死锁,一个线程未解锁情况下重复申请持有该锁,会返回一个错误码( EDEADLK ),保证重复申请锁不会出现死锁。建议使用检测锁。
  • PTHREAD_MUTEX_DEFAULT,其他类型锁,与普通类似。

互斥锁类型设置与获取函数:

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
  • attr,互斥锁属性实例地址,不能为NULL
  • type,锁类型
  • 成功返回0,参数无效返回 EINVAL

5 总结

  互斥锁是最简单的互斥机制,也易于使用,但也容易造成重要问题,那就是死锁问题。死锁问题会导致整进程内或者系统内线程因申请不到锁而“饥饿”,一直处于睡眠状态。因此,使用互斥锁时要谨慎,尽量不使用嵌套锁,而是使用检错锁。互斥锁的使用注意事项,可以参考2.3节的互斥锁使用原则。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Acuity.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值