hello 你好呀!,我是aoz,今天我们来一起研究Linux互斥锁

废话不多说,我们进入正题 有需要的同学可以自行跳转哟~

文章目录

0.线程基础概念

想要理解互斥锁就得先理解线程的概念与临界区的概念呦~

打个比方,线程就像两个人在聊天,分为悄悄话和公屏聊天
悄悄话只能自己听见,但是公屏聊天大家都能听见,因为共用一个公屏,所以还得抢

悄悄话 = 线程内操作,线程外拿不到的数据
公屏聊天 = 临界区操作,共用区域

为了方便理解线程和互斥锁的概念我画了一个图,下面了两个线程,线程对同一个数据区域进行访问

在这里插入图片描述

那什么时候用到互斥锁呢?

当你说话不想被打断的时候。

你以为的多线程运行逻辑
在这里插入图片描述

实际的多线程运行逻辑
在这里插入图片描述
多线程的操作并不是在系统中真的有两个线程在跑,而是一个线程执行一段时间再切到另一个线程上去,这种线程切换非常快,如果进行一个非常长的写入操作,可能写到了一半线程就切走了,那么就会出现写入不完全,也就是话说了一半没说完,这样的话被别的线程听去,就会出大乱子,因此在任一线程说话的时候,就需要让另一个线程闭嘴,不能打断他。

在Posix Thread中定义有一套专门用于线程同步的mutex函数,专门用于锁定某些线程对于临界区的操作。

具体是这样的

运行…
加锁 lock
临界区长写入操作
很长
很长
长…
解锁 unlock

1. 创建和销毁

有两种方法创建互斥锁, 静态方式 动态方式

先说结果:
动态初始化在堆中创建,不用了需要删除以释放内存;
静态初始化在静态存储区,初始化之后直接用就可以,不用了也不需要删除。

1.1静态初始化方法及要点

静态初始化其实是使用了一个POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER

下面这段转载,戳这里转原文

#include <pthread.h>                                                                      
int main(int argc,char *argv[]) 
{ 

      pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; /*正确的使用方法*/

      pthread_mutex_t mutex2; /* 错误 */
      mutex2 = PTHREAD_MUTEX_INITIALIZER; 

      pthread_mutex_t mutex3; /* 错误 */
      mutex3 = mutex1; /* 这里注意, mutex3的赋值是成功的。*/

      return 0;  
}

上面三个例子,其中mutex1 是正确初始化的,mutex2 直接编译报错,mutex3 编译不报错,但是这种赋值的结果是未定的,所以应该禁止这种用法。

编译错误如下:

mingq@mingq-linux:~/test$ g++ test8.cpp -o test8 -lpthread
test8.cpp: In function ‘int main(int, char**)’:
test8.cpp:12:14: warning: extended initializer lists only available with -std=c++11 or -std=gnu++11
       mutex2 = PTHREAD_MUTEX_INITIALIZER; 
              ^

原因分析

静态初始化过程就是编译器在编译的过程中完成了某些内存空间的初始化,也就是说这个**初始化过程发生在编译时,而不是运行时,**因此称之为静态初始化。

PTHREAD_MUTEX_INITIALIZER 的完整定义为:

# define PTHREAD_MUTEX_INITIALIZER \ 
  { { 0, 0, 0, 0, 0, 0, { 0, 0 } } } 

上述问题并不仅仅针对于Mutex变量,而是所有的结构体变量。结构体变量在使用常量进行整体初始化的时候只能在声明定义的时候进行,不能是声明结束之后。

1.2 动态初始化方法及要点

动态方式是采用pthread_mutex_init()函数进行初始化,

API定义如下: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性

互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性(缺省值),不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。 当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:

属性名称说明解锁者
PTHREAD_MUTEX_TIMED_NP缺省值就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。同进程内任何线程
PTHREAD_MUTEX_RECURSIVE_NP嵌套锁允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。加锁者(暂定)
PTHREAD_MUTEX_ERRORCHECK_NP检错锁如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。加锁者
PTHREAD_MUTEX_ADAPTIVE_NP适应锁动作最简单的锁类型,仅等待解锁后重新竞争同进程内任何线程
#include <pthread.h>
int main(int argc, char *argv[])
{
    pthread_mutex_t *pMutex = NULL;
    pMutex = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t)); /* 需要申请内存 */
    
    pthread_mutex_init(pMutex, NULL);
    
    pthread_mutex_destroy(pMutex); /* 同时程序或者模块结束时互斥锁要记得销毁,不然是资源泄漏。*/
    free(pMutex);                  /*销毁之后要记得释放内存,不然会内存泄漏。 */
}

1.3 销毁锁

pthread_mutex_destroy ()用于注销一个互斥锁
API定义如下:int pthread_mutex_destroy(pthread_mutex_t *mutex)销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态

由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy( )除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

2.锁定与解锁

锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁 pthread_mutex_trylock()三个
无论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁

对于普通锁适应锁类型,解锁者可以是同进程内任何线程;

检错锁则必须由加锁者解锁才有效,否则返回EPERM

对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同还没有得到解释。

在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_unlock(pthread_mutex_t *mutex)

int pthread_mutex_trylock(pthread_mutex_t *mutex)

pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

注意

加锁之后,未解锁之前,其他线程请求加锁时,会进入阻塞状态,直到这边解锁为止

任何地方加锁,在返回的时候的要记得解锁,不然进程就死锁了

原则上,谁加锁,谁就来解锁

3.死锁的产生及解决

1.任何地方线程加锁,在返回的时候的要记得解锁,不然进程中如果其他线程申请加锁的时候就永远阻塞了,这就造成了死锁了

2.两个锁以上的情况。以不同的线程以相反的顺序加锁,很容易造成死锁
在这里插入图片描述

3.同个线程在加锁成功后,未解锁的情况下,重新申请加锁

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐