良许Linux教程网 干货合集 — 线程安全和互斥量

— 线程安全和互斥量

线程与共享资源

线程的主要优势在于能够通过全局变量来共享信息,但这种便捷的共享方式也伴随着一定的风险,需要确保多个线程不会同时修改同一变量。可以用一个公厕的例子来说明,比如甲占用公厕时,乙就必须等甲离开才能占用,这个例子很形象地说明了问题。

访问共享资源的代码片段被称为临界区。

1. 线程安全的定义

如果一个函数可以安全地同时供多个线程调用,则称之为线程安全函数。相反,如果一个函数不是线程安全的,则不能在并发情况下调用。

举个例子,下面这个函数就不是线程安全的:

void incrementCounter() {
    static int counter = 0;
    counter++; // 非原子操作,不具备线程安全性
}

在多线程环境下,如果有两个线程同时调用incrementCounter,就可能导致counter值不正确,因为counter++不是原子操作,无法保证线程安全。

为了确保线程安全,需要使用同步机制,比如互斥锁或信号量,在临界区内对共享资源进行保护,避免多个线程同时访问和修改共享资源导致的问题。

static int a = 0;
static void incr(int loops)
{
 int b, j;
 for(j = 0; j 

如果多个线程同时调用该函数,则 a 的最终值不得而知,导致线程不安全的典型原因就是使用了在所有线程之间共享的全局或静态变量

2. 如何实现线程安全

实现线程安全只需实现以下三点即可:

  • 代码必须要有互斥的行为,当一个线程正在临界区中执行时,不允许其他线程进去该临界区中
  • 如果多个线程同时要求执行临界区的代码,并且当前临界区并没有线程在执行,那么只能允许一个线程进入该临界区
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

实现以上三点需要用到互斥量。

2.1 互斥量(mutex 是 mutual exclusion 的缩写)

使用互斥量可以确保同时仅有一个线程访问共享资源,互斥量有两种状态:已锁定(locked)和未锁定(unlocked),任何时候,只能有一个线程锁定该互斥量,一旦线程锁定互斥量,也必须由这一线程给互斥量解锁。

如果多个线程试图执行临界区代码,每个线程进入临界区前,必须先锁定互斥量才能进入临界区,没有锁定互斥量的线程就会阻塞,即只能允许一个线程进入这段代码区域,如下图所示:

image-20240407222623549
image-20240407222623549

所以实现多线程对共享资源的互斥访问,只需要5步:

  • 第一步 — 定义一个互斥量

    互斥量是一个 pthread_mutex_t 类型的变量,这个变量一般需要定义在全局区,让所有的线程都能访问的到

  • 第二步 — 初始化互斥量

     

    在说初始化互斥量之前先来说一下互斥量的属性。

    1). 互斥量类型

    互斥量类型有以下几种,如果一个线程试图去锁定自己之前已经锁定的互斥量,以下几种互斥量会有不同的反应:

    互斥量类型 当线程试图去锁定自己之前已经锁定的互斥量时
    PTHREAD_MUTEX_FAST_NP(默认类型) 发生死锁,这种类型的互斥量不具有死锁检测(自检)功能
    PTHREAD_MUTEX_RECURSIVE_NP 这种类型有一个锁计数器,没锁定一次,计数器就会递增1,每解锁一次,计数器就会递减1,只有当计数器为0时,才会释放该互斥量,即其他线程才可以锁定
    PTHREAD_MUTEX_ERRORCHECK_NP 立即返回错误,此类互斥量会执行错误检查,可以将其作为调试工具,以发现程序在哪里违反了互斥量的使用原则

    2). 是否进程间共享

    PTHREAD_PROCESS_PRIVATE(默认属性),表示只能在同一进程的线程之间共享

    PTHREAD_PROCESS_SHARED,表示可以在多个进程的线程之间共享 ,当多个进程需要协同工作,同时使用共享资源时需要使用该选项

    互斥量在使用之前必须先初始化,初始化后,互斥量处于未锁定状态,初始化互斥量有两种方式:

    1)静态初始化,以下表示使用静态方法初始化三种互斥量类型

    pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;  //PTHREAD_MUTEX_FAST_NP
    pthread_mutex_t mutex=PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;  //PTHREAD_MUTEX_RECURSIVE_NP
    pthread_mutex_t mutex=PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP; //PTHREAD_MUTEX_ERRORCHECK_NP
    

    2)动态初始化,动态初始化需要使用到 pthread_mutex_init 函数

    #include 
    /*************************************************
    函数名称: pthread_mutex_init     
    函数功能: 初始化互斥量
    输入参数: 
      mutex -- 要初始化的互斥量
      attr -- attr 是一个指向 pthread_mutexattr_t 类型对象的指针,用于定义互斥量的属性,比如互斥量的类型,是否进程共享等
    函数返回类型值:成功返回0
    注意:如果第二个参数attr设置为NULL,就使用默认互斥锁属性
         如果第二个参数attr不为NULL,则需要一系列函数来设置互斥量的属性。下面详细说。
    *************************************************/
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr)
    

    设置互斥量属性的相关函数如下:

    pthread_mutexattr_init

    #include 
    /*************************************************
    函数名称: pthread_mutexattr_init
    函数功能: 初始化互斥量属性对象
    输入参数: attr 指向互斥量属性对象的指针
    函数返回类型值:成功返回0
    注意:调用此函数后,互斥量属性为默认属性
    *************************************************/
    int pthread_mutexattr_init(pthread_mutexattr_t *attr);
    

    pthread_mutexattr_destroy

    #include 
    /*************************************************
    函数名称: pthread_mutexattr_destroy
    函数功能: 销毁互斥量属性对象
    输入参数: attr 指向互斥量属性对象的指针
    函数返回类型值:成功返回0
    *************************************************/
    int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
    

    pthread_mutexattr_settype

    #include 
    /*************************************************
    函数名称: pthread_mutexattr_settype
    函数功能: 设置互斥量类型
    输入参数: 
      attr 指向互斥量属性对象的指针
            kind 互斥量类型
    函数返回类型值:成功返回0
    *************************************************/
    int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int kind);
    

    pthread_mutexattr_gettype

    #include 
    /*************************************************
    函数名称: pthread_mutexattr_gettype
    函数功能: 获取互斥量类型
    输入参数: 
      attr 指向互斥量属性对象的指针
            kind 存储获取到的互斥量类型
    函数返回类型值:成功返回0
    *************************************************/
    int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *kind);
    

    pthread_mutexattr_setpshared

    #include 
    /*************************************************
    函数名称: pthread_mutexattr_setpshared
    函数功能: 设置互斥量的 process-shared 属性
    输入参数: 
      attr 指向互斥量属性对象的指针
            pshared 要设置的属性值,PTHREAD_PROCESS_PRIVATE 或者 PTHREAD_PROCESS_SHARED
    函数返回类型值:成功返回0
    *************************************************/
    int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared)
    

    pthread_mutexattr_getpshared

    #include 
    /*************************************************
    函数名称: pthread_mutexattr_getpshared
    函数功能: 获取互斥量的 process-shared 属性
    输入参数: 
      attr 指向互斥量属性对象的指针
            pshared 存储获得的互斥量的 process-shared 属性
    函数返回类型值:成功返回0
    *************************************************/
    int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);
    

    下面这个例子演示了如何设置互斥量类型,这个例子创建了一个带有错误检查属性(error-checking)的互斥量

    pthread_mutex_t mtx;
    pthread_mutexattr_t mtxAttr;
    pthread_mutexattr_init(&mtxAttr);
    pthread_mutexattr_settype(&mtxAttr, PTHREAD_MUTEX_ERRORCHECK)
    pthread_mutex_init(mtx, &mtxAttr);
    //使用互斥锁
    ......
    pthread_mutexattr_destroy(&mtxAttr);
    
  • 第三步 — 在临界区代码前锁定互斥量

    使用 pthread_mutex_lock() 函数锁定互斥量

    #include 
    /*************************************************
    函数名称: pthread_mutex_lock     
    函数功能: 锁定互斥量
    输入参数: pthread_mutex_t *mutex 需要被锁定的互斥量
    函数返回类型值:成功返回0
    注意:调用 pthread_mutex_lock 锁定互斥量时,如果此互斥量已经被其他线程锁定,那么 pthread_mutex_lock  就会一直阻塞,直到该互斥量被解锁,再锁定此互斥量后返回,如果有不止一个线程在等待由 pthread_mutex_unlock 解锁的互斥量,没办法判断哪个线程会如愿以偿
    *************************************************/
    int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
    
  • 第四步 — 在临界区代码后解锁互斥量

    使用 pthread_mutex_unlock() 解锁互斥量

    #include 
    /*************************************************
    函数名称: pthread_mutex_unlock     
    函数功能: 解锁互斥量
    输入参数: pthread_mutex_t *mutex 需要被解锁的互斥量
    函数返回类型值:成功返回0
    注意:对处于未锁定状态的互斥量进行解锁,或者解锁由其他线程锁定的互斥量都是错误的
    *************************************************/
    int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
    
  • 第五步 — 销毁互斥量

    使用 pthread_mutex_destroy() 函数销毁互斥量,对于使用 PTHREAD_MUTEX_INITIALIZER 静态初始化的互斥量,无需调用 pthread_mutex_destroy 销毁互斥量。

    #include 
    /*************************************************
    函数名称: pthread_mutex_destroy     
    函数功能: 销毁互斥量
    输入参数: pthread_mutex_t *mutex 需要被销毁的互斥量
    函数返回类型值:成功返回0
    注意:只有当互斥量处于未锁定状态,并且后续也没有其他线程来锁定它,才可以将其销毁,由pthread_mutex_destroy 销毁的互斥量,可以调用 pthread_mutex_init 重新初始化
    *************************************************/
    int pthread_mutex_destroy(pthread_mutex_t *mutex)
    

2.1 死锁

下图展示了一个死锁的例子,线程A和线程B都成功的锁住一个互斥量,接着试图对另一线程锁定的互斥量加锁,两个线程将永远的等待下去。

image-20240407222629242
image-20240407222629242

如何避免死锁,最简单的方法是定义互斥量的层级关系,当多个线程对一组互斥量操作时,总是应该以相同顺序对该组互斥量进行锁定,例如,在上图中,如果两个线程总是先锁定 mutex1 再锁定 mutex2 ,死锁就不会出现。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部