概述
文章目录
- 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 互斥锁属性
- 4.1 创建互斥锁属
- 4.2 互斥锁属性初始化与销毁
- 4.3 互斥锁作用域
- 4.4 互斥锁类型
- 5 总结
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 | 成功 |
EINVAL | mutex已被销毁过,或者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("rn");
}
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("rn");
}
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_count
及printf
输出,实际结果没有符合预期。
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
,互斥锁属性实例地址,不能为NULLpshared
,作用域类型,PTHREAD_PROCESS_PRIVATE
和PTHREAD_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
,互斥锁属性实例地址,不能为NULLtype
,锁类型- 成功返回0,参数无效返回 EINVAL
5 总结
互斥锁是最简单的互斥机制,也易于使用,但也容易造成重要问题,那就是死锁问题。死锁问题会导致整进程内或者系统内线程因申请不到锁而“饥饿”,一直处于睡眠状态。因此,使用互斥锁时要谨慎,尽量不使用嵌套锁,而是使用检错锁。互斥锁的使用注意事项,可以参考2.3节的互斥锁使用原则。
最后
以上就是闪闪大叔为你收集整理的【Linux应用编程】POSIX线程互斥与同步机制—互斥锁1 前言2 互斥锁3 互斥锁使用4 互斥锁属性5 总结的全部内容,希望文章能够帮你解决【Linux应用编程】POSIX线程互斥与同步机制—互斥锁1 前言2 互斥锁3 互斥锁使用4 互斥锁属性5 总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复