Linux 线程同步

文章目录

  • 一、线程同步介绍
    • 同步与互斥概述
    • 线程同步问题
  • 二、互斥锁
    • 为什么需要互斥锁
    • 互斥锁 Mutex 介绍
    • 互斥锁相关 API
    • 死锁 DeadLock
  • 三、读写锁
      • 读写锁概述
      • 读写锁相关 API
  • 四、生产者与消费者模型
  • 五、条件变量
    • 条件变量概述
    • 条件变量相关 API
    • 生产者消费者条件变量模型
  • 六、信号量
    • 信号量概述
    • 信号量相关 API
    • 生产者消费者信号量模型

一、线程同步介绍

同步与互斥概述

现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源。
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。
    这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。

互斥 :对于散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个线程使用,多个线程不能同时使用公共资源。

同步:对于散布在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。

线程同步问题

线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。如果多个线程同时对某一个共享资源做了操作,这样会产生线程安全问题(数据安全问题),或者称之为线程同步问题。

临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作(不考虑其他因素,原子是不可再继续分割的粒子,类似的原子操作是指不会被线程调度机制打断或者分割的的最小操作),也就是同时访问同一共享资源的其他线程不应终端该片段的执行

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。所以线程同步会一定程度上降低并发的效率,但是它是必须的,因为可以保证共享数据的安全性。

linux 中通常可以通过互斥锁、读写锁、信号量、条件变量等实现线程同步。

二、互斥锁

为什么需要互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

下面我们用程序模拟一下这个过程,线程一需要打印“ hello ”,线程二需要打印“ world ”,不加任何处理的话,打印出来的内容会错乱,测试程序:

#include 
#include 
#include 
#include 
#include 

// 打印机,公共资源
void printer(char *str) {
    while (*str != '\0') {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");
}

// 线程一
void *thread_fun_1(void *arg) {
    char *str = "hello";
    printer(str); //打印
}

// 线程二
void *thread_fun_2(void *arg) {
    char *str = "world";
    printer(str); //打印
}

int main() {
    pthread_t tid1, tid2;

    // 创建 2 个线程
    pthread_create(&tid1, NULL, thread_fun_1, NULL);
    pthread_create(&tid2, NULL, thread_fun_2, NULL);

    // 等待线程结束,回收其资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

运行结果如下:可以看出打印结果是混乱的,并不是想要的结果。

实际上,打印机是有做处理的,我在打印着的时候别人是不允许打印的,只有等我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当 A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好 B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当 A 打印结束后,他会开锁出来,这时候 B 才进去上锁打印。

互斥锁 Mutex 介绍

在线程里也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,以此确保同时仅有一个线程可以访问某项共享资源(可以使用互斥量来保证对任意共享资源的原子访问)。

互斥锁有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥锁。试图对已经锁定的某一互斥锁再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。

一旦线程锁定互斥锁,随即成为该互斥锁的所有者,只有所有者才能给互斥锁解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥锁,每一线程在访问同一资源时将采用如下协议:
1)访问共享资源时,在临界区域前使用互斥锁进行加锁。
2)在访问完成后释放互斥锁导上的锁。
3)对互斥锁进行加锁后,其他任何试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

如果多个线程试图执行这一块代码(一个临界区),那么事实上将只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:
Linux 线程同步_第1张图片

互斥锁相关 API

1、互斥锁的数据类型是:pthread_mutex_t

2、初始化互斥锁:pthread_mutex_init 函数

#include 

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr);
功能:
    初始化一个互斥锁。
参数:
    mutex:互斥锁地址,即需要初始化的互斥量变量。
    attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。也可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如:pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER; 这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。

返回值:
    成功:0,成功申请的锁默认是打开的。
    失败:非 0 错误码

【补充】restrict 是 c 语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

3、释放互斥锁资源:pthread_mutex_destroy 函数

#include 

int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
    销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码

4、加锁:pthread_mutex_lock 函数与 pthread_mutex_trylock 函数

#include 

int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
    对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码

int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:
	尝试加锁:如果加锁失败,不会阻塞,会直接返回。
		调用该函数时,若互斥锁未加锁,则上锁,返回 0;
		若互斥锁已加锁,则函数直接返回失败,即 EBUSY

pthread_mutex_lock() 将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock 之前,该线程一直拥有该锁。
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(死锁详细见下文)。

pthread_mutex_trylock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

5、解锁:pthread_mutex_unlock函数

#include 

int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
    对指定的互斥锁解锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非0错误码

6、 测试程序

#include 
#include 
#include 
#include 
#include 

pthread_mutex_t mutex; //互斥锁
// 打印机
void printer(char *str) {
    pthread_mutex_lock(&mutex); //上锁
    while (*str != '\0') {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");
    pthread_mutex_unlock(&mutex); //解锁
}

// 线程一
void *thread_fun_1(void *arg) {
    char *str = "hello";
    printer(str); //打印
}

// 线程二
void *thread_fun_2(void *arg) {
    char *str = "world";
    printer(str); //打印
}

int main(void) {
    pthread_t tid1, tid2;

    pthread_mutex_init(&mutex, NULL); //初始化互斥锁

    // 创建 2 个线程
    pthread_create(&tid1, NULL, thread_fun_1, NULL);
    pthread_create(&tid2, NULL, thread_fun_2, NULL);

    // 等待线程结束,回收其资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_mutex_destroy(&mutex); //销毁互斥锁

    return 0;
}

运行结果:

死锁 DeadLock

1)什么是死锁

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。

2)死锁场景

  1. 忘记释放锁
  2. 重复加锁(重复加锁是指重复加同一个锁,即同一个 mutex 变量)
  3. 多线程多锁,抢占锁资源:都在等待对方占有的不可抢占的资源。Linux 线程同步_第2张图片
    示例:多线程多锁,抢占锁资源
#include 
#include 
#include 

// 创建2个互斥量

pthread_mutex_t mutex1, mutex2;

void * workA(void * arg) {
    pthread_mutex_lock(&mutex1);
    sleep(1);
    pthread_mutex_lock(&mutex2);
    printf("workA....\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

void * workB(void * arg) {
    pthread_mutex_lock(&mutex2);
    sleep(1);
    pthread_mutex_lock(&mutex1);
    printf("workB....\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

int main() {
    // 初始化互斥量
    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);

    // 创建2个子线程
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, workA, NULL);
    pthread_create(&tid2, NULL, workB, NULL);

    // 回收子线程资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    // 释放互斥量资源
    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);

    return 0;
}
// 运行结果是:进程无法继续运行,被一直阻塞。

三、读写锁

读写锁概述

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致数据安全问题。

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点如下:

  • 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
  • 如果有其它线程写数据,则其它线程都不允许读、写操作。
  • 写是独占的,写的优先级高。

读写锁分为读锁和写锁,规则如下:
1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

读写锁应用场景:在对数据的读写操作中,更多的是读操作,写操作较少

读写锁相关 API

1、读写锁的数据类型是: pthread_rwlock_t

2、读写锁初始化:pthread_rwlock_init 函数

#include 

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
    const pthread_rwlockattr_t *restrict attr);
功能:
    用来初始化 rwlock 所指向的读写锁。

参数:
    rwlock:指向要初始化的读写锁指针。
    attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,比如:pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER; 这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。

返回值:
    成功:0,读写锁的状态将成为已初始化和已解锁。
    失败:非 0 错误码。

3、释放读写锁资源:pthread_rwlock_destroy 函数

#include 

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
    用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

4、读加锁:pthread_rwlock_rdlock 函数和 pthread_rwlock_tryrdlock 函数

#include 

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
    以阻塞方式在读写锁上获取读锁(读锁定)。
    如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
    如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
    线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 
    pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
功能:
	尝试加锁:如果加锁失败,不会阻塞,会直接返回,即用于尝试以非阻塞的方式来在读写锁上获取读锁。
	如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

5、写加锁:pthread_rwlock_wrlock 函数和 pthread_rwlock_trywrlock 函数

#include 

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
    在读写锁上获取写锁(写锁定)。
    如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
    如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
功能:
	尝试加锁:如果加锁失败,不会阻塞,会直接返回,即用于尝试以非阻塞的方式来在读写锁上获取写锁。
	如果有任何的读者或写者持有该锁,则立即失败返回。

6、解锁:pthread_rwlock_unlock 函数

#include 

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
    无论是读锁或写锁,都可以通过此函数解锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

7、测试程序

下面是一个使用读写锁来实现 4 个线程读写一段数据是实例:在此示例程序中,共创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。当某个线程读操作时,其他线程允许读操作,却不允许写操作;当某个线程写操作时,其它线程都不允许读或写操作。

#include 
#include 
#include 
#include 
#include 

pthread_rwlock_t rwlock; //读写锁
int num = 1;

//读操作,其他线程允许读操作,却不允许写操作
void *fun1(void *arg) {
    while (1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("read num first===%d\n", num);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}

//读操作,其他线程允许读操作,却不允许写操作
void *fun2(void *arg) {
    while (1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("read num second===%d\n", num);
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
}

//写操作,其它线程都不允许读或写操作
void *fun3(void *arg) {
    while (1) {
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("write thread first\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
}

//写操作,其它线程都不允许读或写操作
void *fun4(void *arg) {
    while (1) {
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("write thread second\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}

int main() {
    pthread_t ptd1, ptd2, ptd3, ptd4;

    pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁

    //创建线程
    pthread_create(&ptd1, NULL, fun1, NULL);
    pthread_create(&ptd2, NULL, fun2, NULL);
    pthread_create(&ptd3, NULL, fun3, NULL);
    pthread_create(&ptd4, NULL, fun4, NULL);

    //等待线程结束,回收其资源
    pthread_join(ptd1, NULL);
    pthread_join(ptd2, NULL);
    pthread_join(ptd3, NULL);
    pthread_join(ptd4, NULL);

    pthread_rwlock_destroy(&rwlock);//销毁读写锁

    return 0;
}

运行结果:
Linux 线程同步_第3张图片

四、生产者与消费者模型

线程同步典型的案例为生产者消费者模型,仅仅使用互斥锁也可以实现生产者与消费者模型(可以实现,但是会有很多缺点,改进版本可以看下文条件变量和信号量)。

什么是生产者与消费者模型?

假定有两个线程,一个模拟生产者行为,不断生产产品,一个模拟消费者行为,不断消费产品。所有的产品都存放在一个容器中,这个容器就是一个共享资源,两个线程同时操作这个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

  • 生产者消费者模型中的对象:1、生产者;2、消费者;3、容器
  • 生产者消费者模型中生产者可能会有多个,同理消费者也可能有多个,容器有一个
  • 实现生产者消费者模型过程中会产生的问题:
    • 问题1:数据安全问题——可以使用互斥锁、读写锁解决;
    • 问题2:生产者生产的产品占满了容器,需要等待并通知消费者消费;消费者将容器中产品消费完了,需要等待并通知生产者生产——可以使用条件变量、信号量解决(可以实现,改进版本可以看下文条件变量和信号量)

示例:仅仅使用互斥锁的生产者消费者模型

#include 
#include 
#include 
#include 

// 创建一个互斥量
pthread_mutex_t mutex;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;  //容器

void * producer(void * arg) {
    // 不断的创建新的节点,添加到链表中(头插法)
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }

    return NULL;
}

void * customer(void * arg) {
    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        
        // 判断是否有数据    
        if(head != NULL) {   // 如果容器里面没有数据了,消费者不停地循环和判断,这样浪费计算资源
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);
    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }
    
    // 情况一:主线程先退出,那么 pthread_mutex_destroy 将不会执行,所以这种写法错误。
    // for(int i = 0; i < 5; i++) {
	//     pthread_detach(ptids[i]);
	//     pthread_detach(ctids[i]);
	// }
    // pthread_exit(NULL);
    // pthread_mutex_destroy(&mutex);

    // 情况二:pthread_mutex_destroy 将互斥锁销毁,线程中无法继续使用互斥锁,所以这种写法错误。
    // for(int i = 0; i < 5; i++) {
	//     pthread_detach(ptids[i]);
	//     pthread_detach(ctids[i]);
	// }
    // pthread_mutex_destroy(&mutex);
    // pthread_exit(NULL);
    
    // 情况三,正确,使用for 循环不停的阻塞。
    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }
    
    while(1) {
        sleep(10);
    }
    pthread_mutex_destroy(&mutex);
    pthread_exit(NULL);

    // 情况四: 使用 prhread_join 回收资源,这种方法正确
	// for(int i = 0; i < 5; i++) {
	//     pthread_join(ptids[i]);
	//     pthread_join(ctids[i]);
	// }
    // pthread_mutex_destroy(&mutex);
    // pthread_exit(NULL);
    return 0;
}

运行结果:
Linux 线程同步_第4张图片

总结:对于问题1,以上的生产者消费者模型可以很好解决,但是对于问题2,以上模型使用 if else 代替了条件变量和信号量,虽然也可以实现功能,但是如果容器里面没有数据了,消费者就需要不停地循环和判断,这样浪费计算资源。

五、条件变量

条件变量概述

与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。

通常条件变量和互斥锁同时使用:条件变量变量主要用于阻塞线程,但是它不能保证线程安全,如果要保证线程安全需要与互斥锁配合使用

条件变量的两个动作:

  • 条件不满,阻塞线程
  • 当条件满足,通知阻塞的线程开始工作(解除阻塞)

条件变量相关 API

1、条件变量的类型:pthread_cond_t

2、初始化条件变量:pthread_cond_init 函数

#include 

int pthread_cond_init(pthread_cond_t *restrict cond,
    const pthread_condattr_t *restrict attr);
功能:
    初始化一个条件变量
参数:
    cond:指向要初始化的条件变量指针。
    attr:条件变量属性,通常为默认值,传 NULL即可
        也可以使用静态初始化的方法,初始化条件变量:
        pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:
    成功:0
    失败:非0错误号

3、释放条件变量资源:pthread_cond_destroy 函数

#include 

int pthread_cond_destroy(pthread_cond_t *cond);
功能:
    销毁一个条件变量
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

4、等待条件变量:pthread_cond_wait 函数

#include 

int pthread_cond_wait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex);
功能:
    阻塞等待一个条件变量
    a) 阻塞等待条件变量cond(参1)满足
    b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex)。其中 a) b) 两步为一个原子操作。
    c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁

返回值:
    成功:0
    失败:非0错误号


int pthread_cond_timedwait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex,
    const struct timespec *restrict abstime);
功能:
    限时阻塞等待一个条件变量:调用了这个函数,线程会阻塞,直到指定的时间结束
参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁
    abstime:绝对时间

返回值:
    成功:0
    失败:非0错误号

abstime 补充说明:

struct timespec {
    time_t tv_sec;      /* seconds */ // 秒
    long   tv_nsec; /* nanosecondes*/ // 纳秒
}

time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
pthread_cond_timedwait(&cond, &t);

5、唤醒条件变量(唤醒阻塞在条件变量上的线程):pthread_cond_signal函数

#include 

int pthread_cond_signal(pthread_cond_t *cond);
功能:
    唤醒至少一个阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
    唤醒全部阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号
#include 
#include 
#include 
#include 
#include 

pthread_mutex_t mutex;  // 互斥量
pthread_cond_t cond;    // 条件变量

int flag = 0;

// 改变条件的线程
void *fun1(void *arg) {
    while (1) {   
        pthread_mutex_lock(&mutex); // 枷锁
        flag = 1;
        pthread_mutex_unlock(&mutex);  //解锁

        // 唤醒因为条件而阻塞线程
        pthread_cond_signal(&cond);
        printf("线程1已经改变条件\n");
        
        sleep(2);
    }
    return NULL;
}

// 等待条件的线程,如果条件成立往下执行
void *fun2(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);

        // 条件不满足 则阻塞等条件满足
        if (0 == flag) {
            pthread_cond_wait(&cond, &mutex);
        }
        printf("线程二因为条件满足 开始运作...\n");
        flag = 0;
        pthread_mutex_unlock(&mutex); // 解锁
    }
    return NULL;
}

int main() {
    int ret = -1;
    pthread_t tid1, tid2;

    // 初始化条件变量
    ret = pthread_cond_init(&cond, NULL);
    if (0 != ret) {
        printf("pthread_cond_init failed...\n");
        return 1;
    }
    
    // 初始化互斥量
    ret = pthread_mutex_init(&mutex, NULL);
    if (0 != ret) {
        printf("pthread_mutex_init failed...\n");
        return 1;
    }

    // 创建两个线程
    pthread_create(&tid1, NULL, fun1, NULL);
    pthread_create(&tid2, NULL, fun2, NULL);

    // 回收线程资源
    ret = pthread_join(tid1, NULL); 
    if (0 != ret) {
        printf("pthread_join failed...\n");
        return 1;
    }

    ret = pthread_join(tid2, NULL); 
    if (0 != ret) {
        printf("pthread_join failed...\n");
        return 1;
    }

    // 销毁互斥量
    pthread_mutex_destroy(&mutex);

    // 销毁条件变量
    pthread_cond_destroy(&cond);
    return 0;
}

Linux 线程同步_第5张图片

生产者消费者条件变量模型

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。

Linux 线程同步_第6张图片

#include 
#include 
#include 
#include 

// 创建一个互斥量
pthread_mutex_t mutex;

// 创建条件变量
pthread_cond_t cond;

struct Node{
    int num;
    struct Node *next;
};
// 头结点
struct Node * head = NULL;

void * producer(void * arg) {
    // 不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        // 只要生产了一个,就通知消费者消费
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    return NULL;
}

void * customer(void * arg) {
    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        // 判断是否有数据
        if(head != NULL) {
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据,需要等待
            // 当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的,继续向下执行,会重新加锁。
            pthread_cond_wait(&cond, &mutex);
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    pthread_exit(NULL);
    return 0;
}


总结:相较于 mutex 而言,条件变量可以减少竞争。如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

六、信号量

信号量概述

信号量也叫信号灯,广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。

PV 原语是对信号量的操作,一次 P 操作使信号量减1(占用1个资源),一次 V 操作使信号量加1(释放1个资源)。

信号量主要用于进程或线程间的同步和互斥这两种典型情况。

  • 信号量用于互斥:Linux 线程同步_第7张图片
  • 信号量用于同步:Linux 线程同步_第8张图片

信号量和条件变量变量类似,也主要用于阻塞线程,但是它不能保证线程安全,如果要保证线程安全需要与互斥锁配合使用。

信号量相关 API

1、信号量数据类型:sem_t。

2、信号量初始化:sem_init 函数

#include 

int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
    创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
    sem:信号量的地址。
    pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
    value:信号量的初始值。
返回值:
    成功:0
    失败: - 1

3、释放信号量资源:sem_destroy 函数

#include 

int sem_destroy(sem_t *sem);
功能:
    删除 sem 标识的信号量。
参数:
    sem:信号量地址。
返回值:
    成功:0
    失败: - 1

4、信号量 P 操作(减1

#include 

int sem_wait(sem_t *sem);
功能:
    将信号量的值减 1(占用1个资源)。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:
    sem:信号量的地址。
返回值:
    成功:0
    失败: - 1

int sem_trywait(sem_t *sem);
	以非阻塞的方式来对信号量进行减 1 操作。
	若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
	限时尝试(非阻塞)将信号量的值减 1
	abs_timeout:绝对时间

abs_timeout补充说明:

struct timespec {
    time_t tv_sec;      /* seconds */ // 秒
    long   tv_nsec; /* nanosecondes*/ // 纳秒
}

time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
sem_timedwait(&cond, &t)

5、信号量 V 操作(加1)

#include 

int sem_post(sem_t *sem);
功能:
    将信号量的值加 1(释放1个资源) 并发出信号唤醒等待线程(sem_wait())。
参数:
    sem:信号量的地址。
返回值:
    成功:0
    失败:-1

6、获取信号量的值:sem_getvalue

#include 

int sem_getvalue(sem_t *sem, int *sval);
功能:
    获取 sem 标识的信号量的值,保存在 sval 中。
参数:
    sem:信号量地址。
    sval:保存信号量值的地址。
返回值:
    成功:0
    失败:-1

7、程序示例

// 信号量用于互斥
#include 
#include 
#include 
#include 
#include 
#include 

sem_t sem; //信号量

void printer(char *str) {
    sem_wait(&sem);//减一
    while (*str) {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");
    sem_post(&sem);//加一
}

void *thread_fun1(void *arg) {
    char *str1 = "hello";
    printer(str1);
}

void *thread_fun2(void *arg) {
    char *str2 = "world";
    printer(str2);
}

int main(void) {
    pthread_t tid1, tid2;

    sem_init(&sem, 0, 1); //初始化信号量,初始值为 1

    //创建 2 个线程
    pthread_create(&tid1, NULL, thread_fun1, NULL);
    pthread_create(&tid2, NULL, thread_fun2, NULL);

    //等待线程结束,回收其资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    sem_destroy(&sem); //销毁信号量

    return 0;
}

生产者消费者信号量模型

// 信号量用于同步——生产者消费者模式
#include 
#include 
#include 
#include 
#include 

// 创建一个互斥量
pthread_mutex_t mutex;

// 创建两个信号量
sem_t psem;  // 存放商品容器的个数
sem_t csem;   // 可以消费的商品的个数

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;
void * producer(void * arg) {
    // 不断的创建新的节点,添加到链表中
    while(1) {
        sem_wait(&psem);
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        // 通知消费者消费,将可以卖的商品个数加1
        sem_post(&csem);
    }
    return NULL;
}

void * customer(void * arg) {
    while(1) {
        // 申请资源  可以卖的的商品个数减1
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        head = head->next;
        printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        // 释放资源 将容器的个数加1
        sem_post(&psem);
    }
    return  NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);
    sem_init(&psem, 0, 8);  //初始化信号量,初始值为 8(最多可以生产8个商品)
    sem_init(&csem, 0, 0);  //初始化信号量,初始值为 0(开始可以买的商品为0)

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];
    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }
    
    while(1) {
        sleep(10);
    }
    pthread_mutex_destroy(&mutex);
    pthread_exit(NULL);
    return 0;
}

你可能感兴趣的:(#,Linux基础知识,ubuntu,linux,c++,c语言)