【Linux 应用】多线程安全

本文为笔者学习阶段的一个记录,多有错漏与描述不清之处,欢迎大家批评指正。


前言

为什么在操作系统中多线程需要使用互斥锁、信号量等技术?
为什么说多线程环境中全局变量不安全?
本文将对上述问题进行阐述。


一、全局变量为什么不安全?

首先从 cpu 的角度思考对一个变量的 -1 操作需要做什么。
这里需要简单代入一些 cpu 概念,cpu 是有寄存器的,为了不带入太多无关知识,此处仅介绍 r0 - r12 寄存器。

r0 - r12为通用寄存器,用于存储任意数据。

cpu 在将变量 money 进行 -1 操作时需要执行以下三个步骤:
① 首先需要将 money 的值从内存中读取存来存放到某一个寄存器中,例如 r1。
② 随后使用汇编指令计算 r1 - 1,计算结果存放至 r2。
③ 最后将 r2 的值写入 money 所在的地址。

假设一个场景, money = 10,2 个线程同时去执行 money -=1,那么运算过程可能是这样的:
① 线程 1 读取了 money ,值为 10,存放至 r1 = 10。
② 发生线程切换。
③ 线程 2 读取了 money ,此时 money 还是等于10,线程 2 将它存放至 r3。
④ 线程 2 执行汇编指令计算 r3 - 1,将计算结果 9 存放至 r4 ,随后写入到 money 的地址中。
⑤ 线程切换回线程 1
⑥ 线程 1 执行汇编指令计算 r1 - 1,将计算结果 9 存放至 r4 ,随后写入到 money 的地址中。

what,两个线程都执行了 money -= 1,结果现在 money = 9 !!!

简单总结,即使是最简单的变量计算,cpu 也需要使用多个指令完成,而线程之间是会随时发生切换的,因此就有可能出现计算过程被打断的问题,而这个问题可能会导致数据异常。

注意,这里的例子并不完全符合汇编知识,只是为了大致描述问题。

二、如何让全局变量变得安全

2.1 多线程同步技术

各操作系统基本都会为开发者提供线程同步技术以保障多线程之间全局数据的交互安全。
简单的说,线程同步技术主要提供以下两种功能:
① 原子操作
② 睡眠唤醒机制

原子操作:
即该操作已经小的不能再小了,就像原子一样不可再被分割了。
操作系统通过汇编语法实现原子操作,使变量操作变为最小单元,不再会被打断,解决了前面提到的全局变量不安全问题。

睡眠与唤醒:
假如线程 1 长时间占用一个变量,那么线程 2 会进入长时间等待,在等待期间需要不断地访问该变量是否被使用完成,这就会导致 cpu 占用高。而睡眠唤醒机制可以让线程 2 进入睡眠,让出 cpu 资源给其他线程使用,当线程 1 使用完变量后再主动唤醒线程 2。

Linux 系统为我们提供了以下多线程同步技术:
互斥锁、自旋锁、读写锁、条件变量、信号量。

2.2 互斥锁

2.2.1 互斥锁简介

简单概括:互斥锁用于保护多线程下的资源访问(指全局变量,也可以理解为代码块),使资源同一时间只能被一个线程访问,当多个线程访问被保护的资源时,后访问的线程会被阻塞睡眠,直到前一个线程使用完毕。

当一个线程需要使用被互斥锁保护的资源时,首先需要对互斥锁进行上锁,若该锁处于未上锁状态则可以成功上锁,随后该线程占用资源,使用完成后解锁互斥锁。

当一个线程对互斥锁上锁时,发现互斥锁已经处于上锁状态,则说明该资源正在被其他线程访问,该线程会进入睡眠,直到互斥锁被解锁。

2.2.2 互斥锁相关函数

互斥锁操作可以使用以下函数:

– 初始化与释放互斥锁
互斥锁在使用前需要初始化,使用完后需要销毁,函数如下:

int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

mutex:指向互斥锁的指针。
attr:指定互斥锁的属性,可传入NULL表示使用默认属性。
返回值:成功返回0,失败返回非0值。

– 解锁与上锁
pthread_mutex_lock 和 pthread_mutex_trylock 用于互斥锁的上锁。
对已经上锁的互斥锁 lock 会导致线程睡眠,直到锁被释放,而 trylock 则会返回错误。
pthread_mutex_unlock函数用于互斥锁的解锁,函数原型如下

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

mutex:指向互斥锁的指针。
返回值:尝试上锁成功返回0。若互斥锁已被上锁则lock函数会阻塞,而trylock返回错误码EBUSY。若出错返回错误值。

– 获取与设置互斥锁属性
在使用 pthread_mutxt_init 函数初始化互斥锁时,可传入一个 attr 结构体来指定互斥锁的属性

(1)销毁attr结构体
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
(2)初始化attr结构体
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
(3)获取互斥锁属性存入attr结构体中
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
(4)设置attr中所指示的互斥锁属性
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

attr:线程互斥锁属性结构体指针,该结构体会被传入pthread_mutxt_init中。
type:互斥锁的类型,可选值有以下三种:
 -- PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁。若互斥锁处于未锁定状态,或者已由其他线程锁定,对其解锁会导致不确定结果。
 -- PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查,但是由于错误检测会导致效率变慢。它在以下三种情况都会返回错误:
    -- 同一线程对同一互斥锁加锁两次
    -- 线程对由其他线程锁定的互斥锁进行解锁
    -- 线程对处于未锁定状态的互斥锁进行解锁
 -- PTHREAD_MUTEX_RECURSIVE:递归互斥锁,允许对一个已经上锁的互斥锁重复上锁,并且会记录维护这个重复上锁次数。当解锁次数不等于加锁次数时,该互斥锁不会释放。
 -- PTHREAD_MUTEX_DEFAULT : 默认互斥锁,当使用pthread_mutxt_init函数初始化互斥锁传入NULL属性,或者使用宏在定义的同时初始化互斥锁时都会使用该配置,它类似于PTHREAD_MUTEX_NORMAL。
返回值:成功返回0,失败返回非0值。

2.2.3 互斥锁使用示例

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

/* 创建的线程数量 */
#define PTHERAD_COUNT 2

static pthread_mutex_t mutex;
static long totalcount = 100000000;

static void * new_pthread(void * arg)
{
    long count = 0;
    /* 循环争夺资源total */
    for( ; ; ){
        /* 互斥锁上锁 */
        pthread_mutex_lock(&mutex);
        /* 让总资源-1,同时记录抢到的资源+1 */
        totalcount --;
        /* 互斥锁解锁 */
        count ++;
        pthread_mutex_unlock(&mutex);
        /* 当总资源为0时线程退出 */
        if(totalcount <= 0){
            pthread_exit((void *)count);
        }
    } 
}

/* 该函数执行以后会创建两个新的线程
   这两个线程会共同争夺资源total,total总数为1000 0000
   可以发现如果不使用互斥锁会导致两个线程抢到的资源加起来远远超过total总数
   这是因为对于共享资源的同时访问造成了不可预料的问题出现
   当使用互斥锁之后这个问题就不会再出现
    */
int main(int argc, char **argv)
{
    int ret = 0,i = 0;
    void * pret = NULL;
    pthread_t pthread_id[PTHERAD_COUNT];

    /* 初始化一个线程互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 循环创建多个线程 */
    for(i = 0;i < PTHERAD_COUNT;i ++){
        ret = pthread_create(&pthread_id[i], NULL, new_pthread, NULL);
        if(ret){
            fprintf(stderr,"pthread[%d] create is error:%s\r\n", i , strerror(ret));
            exit(-1);
        }
    }

    /* 等待所有的线程退出 */
    for(i = 0;i < PTHERAD_COUNT;i ++){
        /* 阻塞等待线程退出 */
        ret = pthread_join(pthread_id[i], &pret);
        if(ret){
            fprintf(stderr,"ptherad_join is error:%s\r\n",strerror(ret));
            exit(-1);
        }
        /* 打印线程退出时的退出参数 */
        fprintf(stdout,"pthread[%d] exit,return param is %ld\r\n", i, (long)pret);
    }

    /* 销毁互斥锁 */
	pthread_mutex_destroy(&mutex);
    exit(0);
}

2.3 自旋锁

2.3.1 自旋锁简介

简单概括:自旋锁与互斥锁的作用基本一致,区别在于自旋锁不会导致线程睡眠,而是让线程原地等待自旋锁被解锁。

互斥锁会让线程进入睡眠状态,而线程在睡眠和唤醒之间切换是需要一定的系统开销的。
对于一些简单不费时的操作来说,频繁的睡眠和唤醒带来的开销太大。
因此自旋锁适用于资源占用时间非常短的场合,而互斥锁则适用于资源占用时间较长的场合。

2.3.2 自旋锁相关函数

自旋锁操作可以使用以下函数:

– 初始化与释放自旋锁
互斥锁在使用前需要初始化,使用完后需要销毁,函数如下:

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

lock:指向自旋锁的指针。
pshared:自旋锁的共享属性,可选值如下:
 -- PTHREAD_PROCESS_SHARED:共享自旋锁,该自旋锁可以在多个进程中的线程之间共享。
 -- PTHREAD_PROCESS_PRIVATE:私有自旋锁,只有本进程内的线程才能够使用该自旋锁。
返回值:成功返回0,失败返回非0值。

– 解锁与上锁
pthread_spin_lock 和 pthread_spin_trylock 都用于自旋锁上锁
对已经上锁的自旋锁再次上锁时 lock 函数会原地等待解锁(并不会睡眠), 而 trylock 则会返回错误码 EBUSY
pthread_spin_unlock 函数则用于自旋锁解锁,函数原型如下:

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

lock:指向自旋锁的指针。
返回值:成功返回0,若自旋锁已被上锁则lock函数会原地等待解锁,而trylock会返回错误码EBUSY。若出错返回错误值。

2.3.3 自旋锁使用示例

自旋锁的使用与互斥锁类似,只需要把互斥锁的上锁解锁函数替换为自旋锁的上锁解锁函数即可。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

/* 创建的线程数量 */
#define PTHERAD_COUNT 2
static pthread_spinlock_t spinlock;
static long totalcount = 100000000;

static void * new_pthread(void * arg)
{
    long count = 0;
    /* 循环争夺资源total */
    for( ; ; ){
        /* 自旋锁上锁 */
        pthread_spin_lock(&spinlock);
        /* 让总资源-1,同时记录抢到的资源+1 */
        totalcount --;
        count ++;
        /* 自旋锁解锁 */
        pthread_spin_unlock(&spinlock);
        /* 当总资源为0时线程退出 */
        if(totalcount <= 0){
            pthread_exit((void *)count);
        }
    } 
}

/* 该函数执行以后会创建两个新的线程
   这两个线程会共同争夺资源total,total总数为1000 0000
   可以发现如果不使用自旋锁会导致两个线程抢到的资源加起来远远超过total总数
   这是因为对于共享资源的同时访问造成了不可预料的问题出现
   当使用自旋锁之后这个问题就不会再出现
    */
int main(int argc, char **argv)
{
    int ret = 0,i = 0;
    void * pret = NULL;
    pthread_t pthread_id[PTHERAD_COUNT];

    /* 初始化一个线程自旋锁 */
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);

    /* 循环创建多个线程 */
    for(i = 0;i < PTHERAD_COUNT;i ++){
        ret = pthread_create(&pthread_id[i], NULL, new_pthread, NULL);
        if(ret){
            fprintf(stderr,"pthread[%d] create is error:%s\r\n", i , strerror(ret));
            exit(-1);
        }
    }

    /* 等待所有的线程退出 */
    for(i = 0;i < PTHERAD_COUNT;i ++){
        /* 阻塞等待线程退出 */
        ret = pthread_join(pthread_id[i], &pret);
        if(ret){
            fprintf(stderr,"ptherad_join is error:%s\r\n",strerror(ret));
            exit(-1);
        }
        /* 打印线程退出时的退出参数 */
        fprintf(stdout,"pthread[%d] exit,return param is %ld\r\n", i, (long)pret);
    }

    /* 销毁自旋锁 */
    pthread_spin_destroy(&spinlock);
    exit(0);
}

2.4 读写锁

2.4.1 读写锁简介

简单概括:读写锁也类似于互斥锁,区别在于读写锁允许多个线程同时读取资源,但是不允许同时写资源以及同时读写资源

结合前文提到的 cpu 操作变量流程可以发现,若多个线程同时读取数据,并不会导致数据异常,只有在操作数据的途中被打断且数据被修改才会导致数据异常。

读写锁的优势在于可以在多个线程都是读数据时不阻塞线程,提高了多线程读数据的效率。
但是只要有一个线程尝试写数据,则会让其他线程都进入睡眠,这导致在频繁写数据的场景中,读线程可能会被一直阻塞。

因此,读写锁适用于读数据频率远大于写数据频率的场合

2.4.2 读写锁相关函数

读写锁操作可以使用以下函数:

– 初始化与释放读写锁
读写锁在使用前需要初始化,在弃用时需要摧毁它。

使用宏初始化读写锁:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
使用函数初始化与摧毁锁:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

rwlock:指向读写锁的指针。
attr:读写锁的属性,可传入NULL表示使用默认属性。
返回值:成功返回0,失败返回错误码。

– 解锁与上锁

在读写锁的上锁中分为读锁上锁与写锁上锁两种情况,但是对于这两种锁的解锁统一使用同一个函数解锁。它们的函数原型如下:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

rwlock:指向读写锁的指针。
返回值:成功返回0,失败返回错误码。

– 获取与设置读写锁属性
在使用 pthread_wrlock_init 时,可传入参数 attr 来指定读写锁的属性,系统为我们提供了以下函数用于操作 attr 结构体:

初始化、摧毁一个attr
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
设置、获取读写锁的共享属性:
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

attr:定义的pthread_rwlockattr_t类型的结构体指针。
pshared:指定了读写锁的共享属性,可选值如下:
 -- PTHREAD_PROCESS_SHARED:共享读写锁。该读写锁可以在多个进程中的线程之间共享;
 -- PTHREAD_PROCESS_PRIVATE:私有读写锁。只有本进程内的线程才能够使用该读写锁,这是读写锁共享属性的默认值。
返回值:成功返回0,失败返回错误码。

2.4.3 读写锁使用示例

各种锁操作几乎相同,此处不再多做展示,将其他锁示例代码中的上锁和解锁函数换成读写锁相关函数即可。

2.5 信号量

2.5.1 信号量简介

简单概括:信号量类似于一个全局计数器,释放信号量让计数器 +1,等待到信号量让计数器 -1,当信号量为 0 时,等待信号量的线程会被阻塞,直到信号量被其他线程释放。

2.5.2 信号量相关函数

信号量操作可以使用以下函数:

– 初始化与释放信号量
信号量在使用前需要初始化,在弃用时需要摧毁它。

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);

sem:指向信号量的指针。
pshared:为0表示信号量只能在同一进程的线程中共享。不为0则信号灯可以被多个进程共享。通常设置为0。
value:信号量初始数量。
返回值:成功返回0,失败返回-1并设置errno。

– 释放与等待信号量
sem_wait、sem_trywait、sem_timedwait 都用于等待信号量,也就是让信号量 -1,区别在于当等待的信号量为 0 时:
sem_wait 会让线程进入睡眠,直到信号量被其他线程释放;
sem_trywait 会立即返回错误;
sem_timedwait 会让线程睡眠等待一段时间,超时后返回错误。

sem_post 则用于释放信号量,也就是让信号量 +1

等待信号量
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
释放信号量
int sem_post (sem_t *__sem)

2.5.3 信号量使用示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
#include <signal.h>

/* 该程序会创建两个线程公平竞争资源money
   当不使用信号灯做共享资源money的保护时
   会出现两线程抢到的资源数量和大于总资源数量
   使用信号灯后两线程抢到的资源数量和刚好等于总资源数 */

/* 定义信号量 */
static sem_t sem;
static unsigned long long money = 10000000;

/* 线程函数,用于争夺总资源money */
void * new_thread_routine(void * arg)
{
    int my_money = 0;
    while(1) {
        /* 阻塞等待信号量有效 */
        sem_wait(&sem);
        if(money == 0) {
            printf("pthread get money %d \r\n", my_money);
            /* 释放一个信号量 */
            sem_post(&sem);
            pthread_exit(NULL);
        }
        money --;
        my_money ++;
        sem_post(&sem);
    }
}

/* 捕获到 Ctrl + C 信号时摧毁信号灯并退出进程 */
void sighandler(int sig_num)
{
    sem_destroy(&sem);
    exit(1);
}

int main(int argc, char ** argv)
{
    pthread_t tid;
    /* 绑定Ctrl+C信号处理函数 */
    if ( SIG_ERR == signal(SIGINT, sighandler) ) {
        perror("signal error!");
        exit(-1);
    }

    /* 初始化信号灯 */
    if ( 0 != sem_init(&sem, 0, 0) ) {
        perror("sem init error!");
        exit(-1);
    }

    /* 创建两个线程竞争money */
    if ( 0 != pthread_create(&tid, NULL, new_thread_routine, NULL) ) {
        perror("pthread_create error!");
        exit(-1);
    }

    if ( 0 != pthread_create(&tid, NULL, new_thread_routine, NULL) ) {
        perror("pthread_create error!");
        exit(-1);
    }
    /* 释放一个信号灯,让线程开始竞争money */
    sem_post(&sem);
    while(1) {
        sleep(1);
    }
    exit(0);
}

2.6 条件变量

2.6.1 条件变量简介

简单概括:条件变量用于替换代码中死循环等待变量成立的语句。

在程序中难免会遇到以下情况:

while( usart.recvflag == 1 )
{
	do_something();
}

该操作会让 cpu 原地死等 usart.recvflag 标志成立,非常非常浪费 cpu 资源,那么我们可以这样做:

while (1)
{
	if ( usart.recvflag == 1 )
	{
		do_something();
	}
	delay_ms(100);
}

这样又会引入一个新的问题,延迟时间太长了,导致响应不够及时;延迟时间太短了,导致 cpu 频繁访问该标志位。

总之延迟时间到底应该设多少很难抉择,响应时间和 cpu 占用少是对立的,没法两全其美的解决这个问题。
这时候条件变量就可以完美解决这个问题。

条件变量让线程在等待某一条件成立时进入睡眠让出 cpu 资源,当条件成立后再主动唤醒该线程。

例如:
① 线程 1 在等待 usart.recvflag == 1时,通过条件变量等待函数进入睡眠。
② 当串口接收中断接收完一包数据将 usart.recvflag = 1 后,通过条件变量通知函数唤醒线程 1 处理串口数据。

需要注意的是,标志位 usart.recvflag 本身是一个全局变量,对他的读写也需要使用同步技术来保护。
因此,条件变量还需要搭配互斥锁来使用

2.6.2 条件变量相关函数

条件变量操作可以使用以下函数:

– 初始化与释放条件变量
条件变量在使用前需要初始化,在弃用时需要摧毁它。

使用宏初始化条件变量:
static pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;
使用函数初始化与摧毁条件变量:
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
cond:条件变量内存首地址。
attr:条件变量的属性配置,可配置为NULL使用默认属性。
返回值:成功返回0,失败返回非0值。

– 等待条件变量
pthread_cond_wait、pthread_cond_timedwait 都用于等待条件变量成立,区别在于:
pthread_cond_wait 会让线程进入睡眠,直到其他变量通知条件变量成立;
pthread_cond_timedwait 会让线程睡眠等待一段时间,超时后返回错误。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
cond:指向条件变量的指针。
mutex:指向互斥锁的指针。
restrict abstime:超时时间设置。
返回值:成功返回0,失败返回非0值。

– 通知条件变量
pthread_cond_signal 和 pthread_cond_broadcast 函数都用于通知条件变量,区别在于:
pthread_cond_broadcast会通知到所有使用pthread_cond_wait等待条件变量的线程;
pthread_cond_signal则只保证唤醒一条以上的线程,但是它的效率远高于pthread_cond_broadcast;
结合这些特性,pthread_cond_signal函数通常用于只有一个线程等待条件变量时。

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
cond:指向条件变量的指针。
返回值:成功返回0,失败返回非0值。

2.6.3 条件变量使用示例

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

/* 创建的线程数量 */
#define PTHERAD_COUNT 2

static pthread_mutex_t mutex;
/* 定义并初始化一个条件变量 */
static pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;
static int money = 0;

static void * new_pthread(void * arg)
{
    for( ; ; ){
        /* 互斥锁上锁 */
        pthread_mutex_lock(&mutex);

        while(money <= 0){
            pthread_cond_wait(&cond, &mutex);
        }
        money --;
        printf("pthread[%ld] get money \r\n", pthread_self());

        /* 互斥锁解锁 */
        pthread_mutex_unlock(&mutex);
    }
    pthread_exit((void *)0);
}

/* 该函数执行以后会创建两个新的线程
   这两个线程会共同争夺资源total,total总数为1000 0000
   可以发现如果不使用互斥锁会导致两个线程抢到的资源加起来远远超过total总数
   这是因为对于共享资源的同时访问造成了不可预料的问题出现
   当使用互斥锁之后这个问题就不会再出现
    */
int main(int argc, char **argv)
{
    int ret = 0,i = 0;
    void * pret = NULL;
    pthread_t pthread_id[PTHERAD_COUNT];

    /* 初始化一个线程互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 初始化一个条件变量 */
    // pthread_cond_init(&cond, NULL);

    /* 循环创建多个线程 */
    for(i = 0;i < PTHERAD_COUNT;i ++){
        ret = pthread_create(&pthread_id[i], NULL, new_pthread, NULL);
        if(ret){
            fprintf(stderr,"pthread[%d] create is error:%s\r\n", i , strerror(ret));
            exit(-1);
        }
    }

    while(1){
        money ++;
        pthread_cond_signal(&cond);
        sleep(1);
    }

    /* 销毁互斥锁与条件变量 */
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    exit(0);
}

三、 不可重入函数

3.1 不可重入函数为什么不安全

这里简单描述一个多线程概念:同一进程下的不同线程,共用同一块数据空间

在 Linux 中使用 man 3 strtok 可以看到 strtok、strtok_r 两个函数,往下翻可以看到以下描述:在这里插入图片描述
man 手册对 strtok 的描述为:MT-Unsafe(多线程不安全)。
这是因为在 strtok 中维护了一个静态字符串,他会保存上一次调用 strtok 时的字符串,而多线程的调用则可能导致其静态数据区混乱,导致调用结果异常。

假设一个场景,两个线程同时调用 strtok 裁剪字符串
线程 1 裁剪的字符串为 str1 = “A:B:C:D”,线程 2 裁剪的字符串为 str2 = “1:2:3:4”
过程如下:

① 线程 1 通过 strtok(str1, ‘:’) 得到字符 “A”,随后 strtok 内部的静态数据区会存放 “B:C:D” 。
② 线程 1 继续调用 strtok(NULL, ‘:’) 得到字符 “B”,随后 strtok 内部的静态数据区会存放 “C:D” 。
③ 发生线程切换。
④ 线程 2 通过 strtok(str2, ‘:’) 得到字符 “1”,随后 strtok 内部的静态数据区会存放 “2:3:4” 。
⑤ 线程 2 去做其他事情,随后线程切换回线程 1。
⑥ 线程 1 继续调用 strtok(NULL, ‘:’) ,但是用于 strtok 内部静态数据已经在线程 2 调用时被覆盖成了“2:3:4”,此时线程 1 会得到字符 “2”。

很显然,线程 1 的 strtok 得到了意料之外的结果。解决办法也很简单,使用 strtok 的多线程安全版本 strtok_r 即可。

3.2 如何区分函数是否为不可重入函数

若一个函数内部维护了静态数据区且没有做防护,那么它就是不可重入函数。

在 Linux 中,通过 man 手册可以查询各种库函数是否为不可重入函数。
常见的不可重入函数有:strtok、gethostbyname、gmtime、localtime…

对于这些不可重入函数,man 手册中都会将其描述为 MT-Unsafe(多线程不安全)。例如:
在这里插入图片描述
在这里插入图片描述
在多线程环境下需要谨慎使用这些函数。

四、如何正确使用可重入函数

4.1 库函数中的不可重入函数

简单概括:使用库中提供的可重入函数版本。

在 3.2 小节的不可重入函数图片中可以看到,对于一些不可重入函数,系统提供了另一个以该函数名 + _r 为后缀的函数,并且这些函数都被描述为 MT-Safe(多线程安全),这些函数的特点在于多了一个额外参数:
在这里插入图片描述
这是一个双重指针,指向一段由使用者自己申请的内存空间。

前面提到过,不可重入函数很多都是因为它内部维护了静态数据区,而多个线程共用这一片静态数据就会导致数据混乱。
解决方法也很简单,由调用的线程申请一片内存并传入,原本存放于静态数据区的数据转而存放至这片内存中。
这样一来,多个线程调用该函数时,各自都申请了一片内存用于存放静态数据,不会再出现多个线程共用一片数据区的问题。

4.2 自己编写的不可重入函数

自己编写的不可重入函数可以参考库函数中的处理方式来解决问题。


总结

在多线程环境下编程时
每当使用到全局变量,就应该提醒自己,是否应该做线程同步处理。
每当使用内部会保存上一次数据的函数时,就应该提醒自己,是否应该做可重入处理。

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值