Linux下线程同步

文章参考:爱编程的大丙 (subingwen.cn)

一. 线程同步简介

1. 定义

所谓线程同步,其实是为了避免多个线程同时访问一个共享资源,最终导致共享资源出现偏差的情况(目前计算机大都是多核CPU,因此会出现多个进程、线程同时运行的情况)。因此,同步其实是让多个线程依次访问某一共享资源,不要出现多个线程同时访问的情况。

2. 实例分析

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 全局变量,是共享资源
int number;

void *func_a(void *arg){
    for (int i = 0; i < 10; i++){
        int var = number;
        var++;
        // 线程先阻塞10微秒,然后再将修改的数写回去
        usleep(10);
        number = var;
        printf("线程A的ID = %ld, number = %d\n", pthread_self(), number); 
    }
    return NULL;
}

void *func_b(void *arg){
    for (int i = 0; i < 10; i++){
        int var = number;
        var++;
        number = var;
        printf("线程B的ID = %ld, number = %d\n", pthread_self(), number); 
        // 线程先修改完,再休眠
        usleep(5);
    }
    return NULL;
}

int main(int argc, const char * argv[]){
    // 创建两个修改共享资源的子线程
    pthread_t p1, p2; 
    pthread_create(&p1, NULL, func_a, NULL);
    pthread_create(&p2, NULL, func_b, NULL);
    // 主线程回收子线程资源
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    return 0;
}

编译运行:

输出结果如下:

线程B的ID = 140334390478592, number = 1
线程A的ID = 140334398871296, number = 1
线程B的ID = 140334390478592, number = 2
线程A的ID = 140334398871296, number = 2
线程B的ID = 140334390478592, number = 3
线程A的ID = 140334398871296, number = 3
线程B的ID = 140334390478592, number = 4
线程B的ID = 140334390478592, number = 5
线程A的ID = 140334398871296, number = 5
线程B的ID = 140334390478592, number = 6
线程A的ID = 140334398871296, number = 6
线程B的ID = 140334390478592, number = 7
线程A的ID = 140334398871296, number = 7
线程B的ID = 140334390478592, number = 8
线程A的ID = 140334398871296, number = 8
线程B的ID = 140334390478592, number = 9
线程B的ID = 140334390478592, number = 10
线程A的ID = 140334398871296, number = 9
线程A的ID = 140334398871296, number = 10
线程A的ID = 140334398871296, number = 11

可以看出:两个线程对共享资源不论先后的写导致结果出现异常。

原理分析:

在本程序中,如果线程A先抢到的CPU时间片,获取了number(假设为0)的值并传递給var变量,随后A对var进行了++操作,下一步A休眠了,并没有将立即将var的新值传给number,也就是说做A对number的修改还没有写回去,就被阻塞了。(此时number仍为0)

此时B如果抢到了时间片,就会去读取内存中number的值,结果获得的number是0,然后修改了number,令number=1,随后B线程休眠。(此时number=1)

此时A线程如果抢到时间片,由于CPU保留了A线程之前的状态,所以A线程会从休眠指令后一句开始执行,也就是将新的number写回到CPU,于是我们发现,number还是1。

此时两个线程均进行了number++操作,结果却是number只增加了一个1。这就是多线程同时操作临界资源(就是共享资源)带来的隐患。

二. 互斥锁

1. 简介

互斥锁是一种线程同步的常用手法。它通过为操作临界资源的代码块(也就是临界区,一般包含所有涉及临界资源的代码)加锁,从而保证同一时间只有一个线程能够操纵临界资源,避免了多个线程同时操作临界资源的带来的隐患。

注意:

  • 一个临界资源最好对应一个锁。
  • 加锁后记得释放,否则线程会阻塞。
  • 注意死锁现象。

2. 函数

数据类型:

linux中互斥锁的类型为pthread_mutex_t

#include <pthread.h>
pthread_mutex_t mutex;

这个锁对象中包含了当前这把锁的状态信息:锁定还是打开,如果锁上了,上锁的线程的ID是多少。一个互斥锁变量只能被一个线程加锁,如果一个线程企图对一个已经加锁的锁进行加锁,那么该线程会阻塞,直到这个锁被其上锁的线程释放,本线程才能将这个锁锁定,随后线程继续运行。

初始化:

#include <pthread.h>
// restrict:一个用于修饰指针的关键字,限定只有当前指针能够访问其指向的内存。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
  • 参数:
    • mutex:互斥锁变量的地址。
    • attr:互斥锁的属性,一般默认即可,也就是NULL。
  • 返回值:初始化成功返回0,失败返回错误码。

释放互斥锁资源:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 参数:
    • mutex:互斥锁变量的地址。
  • 返回值:释放资源成功返回0,失败返回错误码。

锁定互斥锁:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 参数:
    • mutex:互斥锁变量的地址。
  • 返回值:锁定互斥锁成功返回0;失败返回错误码,且当前线程进入阻塞状态。
  • 特点:如果上锁成功,会继续向下运行。如果失败,线程会阻塞在此处,直到这个锁被释放,当前线程抢到这个锁,会再次将其锁定,然后继续向下运行。

尝试锁定互斥锁:

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 参数:
    • mutex:互斥锁变量的地址。
  • 返回值:锁定互斥锁成功返回0;失败返回错误码,但当前线程不会阻塞。

为互斥锁解锁

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 参数:
    • mutex:互斥锁变量的地址。
  • 返回值:锁定互斥锁成功返回0;失败返回错误码。
  • 注意:只有为这个锁加锁的线程能够将其解锁,否则解锁会失败。

3. 实例

这里对1.2中的代码尽心修改。

代码如下:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 全局变量,是共享资源
int number;
// 互斥锁
pthread_mutex_t mutex;

void *func_a(void *arg){
    for (int i = 0; i < 10; i++){
        // 为临界区加锁
        pthread_mutex_lock(&mutex);
        int var = number;
        var++;
        usleep(10);
        number = var;
        printf("线程A的ID = %ld, number = %d\n", pthread_self(), number); 
        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void *func_b(void *arg){
    for (int i = 0; i < 10; i++){
        // 为临界区加锁
        pthread_mutex_lock(&mutex);
        int var = number;
        var++;
        number = var;
        printf("线程B的ID = %ld, number = %d\n", pthread_self(), number); 
        // 解锁
        pthread_mutex_unlock(&mutex);
        usleep(5);
    }
    return NULL;
}

int main(int argc, const char * argv[]){
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    // 创建两个修改共享资源的子线程
    pthread_t p1, p2; 
    pthread_create(&p1, NULL, func_a, NULL);
    pthread_create(&p2, NULL, func_b, NULL);
    // 主线程阻塞回收子线程资源
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    // 回收互斥锁资源
    pthread_mutex_destroy(&mutex);
    return 0;
}

编译运行

# 编译:
gcc synchronous.c -o app -l pthread
./app
# 运行结果:
线程B的ID = 140005370177280, number = 1
线程A的ID = 140005378569984, number = 2
线程A的ID = 140005378569984, number = 3
线程A的ID = 140005378569984, number = 4
线程A的ID = 140005378569984, number = 5
线程A的ID = 140005378569984, number = 6
线程A的ID = 140005378569984, number = 7
线程A的ID = 140005378569984, number = 8
线程A的ID = 140005378569984, number = 9
线程A的ID = 140005378569984, number = 10
线程A的ID = 140005378569984, number = 11
线程B的ID = 140005370177280, number = 12
线程B的ID = 140005370177280, number = 13
线程B的ID = 140005370177280, number = 14
线程B的ID = 140005370177280, number = 15
线程B的ID = 140005370177280, number = 16
线程B的ID = 140005370177280, number = 17
线程B的ID = 140005370177280, number = 18
线程B的ID = 140005370177280, number = 19
线程B的ID = 140005370177280, number = 20

可以看到,运行结果不再出错。

三. 死锁

所谓死锁,就是由于锁的使用不当,导致所有线程搜被阻塞,并且线程的阻塞无法解开,程序就此宕机。

1. 死锁场景

  • 加锁后忘记解锁。

  • 重复加锁。

  • 程序中有多个共享资源,因此有多把锁,线程对齐任意加锁,可能导致死锁。例如:

    有两个共享资源:x、y。
    线程A对x加锁,想要申请资源y。同时线程B对y加锁,想要申请资源x。此时死锁发生。
    

2. 解决方法

  • 避免多次锁定。
  • 对共享资源加锁后,一定要记得解锁。
  • 如果程序中有多把锁,要对加锁顺序进行规定,但有些情况下无法找到绝对不产生死锁的顺序
  • 如果程序中有多把锁,也可以在对其它互斥锁进行加锁之前,先释放本线程所拥有的锁。
  • 项目程序引入专门用于死锁检测的模块。

四. 读写锁

1. 引入

在多个线程对临界资源进行操作时,有三种情况:

  1. 全部都在读临界资源。
  2. 一部分读临界资源,一部分修改临界资源。
  3. 全部都在修改临界资源。

这其中,2、3两种情况会产生同步问题,可能导致临界资源异常,但1这种情况是不会产生异常的。那么,我们可不可以将锁的类型进行划分,如果当前临界资源上的是读锁,那么其他线程想读该临界资源时也可以读,但不能写;如果当前临界资源上的是写锁,那么其他线程对该临界资源即不可以读,也不可以写。这样不就能将多线程的效率进一步提高了吗?读写锁应运而生。

2. 函数

linux中读写锁类型为pthread_rwlock_t

pthread_rwlock_t

这把锁记录信息如下:

  • 锁的状态:锁定、开启
  • 锁定的操作:读操作、写操作
  • 上锁的线程ID。

使用逻辑如下:

  • 当这把锁被锁定且锁定的操作为读操作时,那么其余线程仍可以读其锁定的临界资源,但不能修改。也就是说:读锁是共享的
  • 当这把锁被锁定且锁定的操作为写操作时,其余线程不能读也不能写其锁定的临界资源。也就是说:写锁是独占的,且写的优先级大于读的优先级

使用场景:

当所有线程对临界资源的操作都是读时,读写锁和互斥锁的效率没有区别。而当有写的操作时,读写锁就有了优势,且写的操作越多,读写锁的优势越大,效率越高。

2.1 初始化

#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
  • 参数:
    • rwlock:读写锁变量的地址。
    • attr:读写锁的属性,一般使用默认属性,即NULL。

2.2 销毁读写锁

#include <pthread.h>
pthread_rwlock_t rwlock;
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 参数:
    • rwlock:读写锁变量的地址。

2.3 加锁

阻塞式加读锁:

// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 参数:
    • rwlock:读写锁变量的地址。

非阻塞式加读锁

// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
  • 参数:
    • rwlock:读写锁变量的地址。

阻塞式加写锁:

// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • 参数:
    • rwlock:读写锁变量的地址。

非阻塞式加写锁:

// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
  • 参数:
    • rwlock:读写锁变量的地址。

2.4 释放锁

// 解锁,不论是读锁还是写锁,都使用该函数释放
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 参数:
    • rwlock:读写锁变量的地址。

3. 实例

题目:使用八个线程操作一个变量,其中五个读,一个写。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 全局变量,是共享资源
int number = 0;
// 读写锁
pthread_rwlock_t rwlock;

// 子线程读操作
void *rd_num(void *arg){
    while (1){
        pthread_rwlock_rdlock(&rwlock);
        printf("read operation, thread id = %ld, number = %d\n", pthread_self(), number);
        pthread_rwlock_unlock(&rwlock);
        sleep(rand()%5);    // 5m内随机取一个整数
    } 
    return NULL;
}
// 子线程写操作
void *wr_num(void *arg){
    while(1) {
        pthread_rwlock_wrlock(&rwlock);
        int var = number;
        var++;
        number = var;
        printf("write operation, thread id = %ld, number = %d\n", pthread_self(), number);
        pthread_rwlock_unlock(&rwlock);
        sleep(rand()%5);
    }
    return NULL;
}

int main(int argc, const char * argv[]){
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);
    // 子线程,其中3个写,5个读
    pthread_t rtid[5];
    pthread_t wtid[3];
    // 子线程初始化
    for (int i = 0; i < 5; ++i){
        pthread_create(&rtid[i], NULL, rd_num, NULL);
    }
    for (int i = 0; i < 3; ++i){
        pthread_create(&wtid[i], NULL, wr_num, NULL);
    }
    // 释放子线程
    for (int i = 0; i < 5; ++i){
        pthread_join(rtid[i], NULL);
    }
    for (int i = 0; i < 3; ++i){
        pthread_join(wtid[i], NULL);
    }
    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

五. 条件变量

1. 概述

作用:

条件变量的作用在于根据一定条件来批量阻塞线程,他无法实现线程同步,因此在多线程中往往与互斥锁配合使用。

与互斥锁的区别:

  • 互斥锁:一次只通过一个线程,阻塞其余所有线程。
  • 条件变量:只要不满足条件,一次会阻塞N个线程。如果满足条件,N个线程都可以通过。

使用场景:

一般用于生产者——消费者模型,并且和互斥锁配合使用。

2. 函数

2.1 变量

pthread_cond_t

#include <pthread.h>
pthread_cond_t cond;

被条件变量阻塞的线程的线程信息会记录在这个变量中,以便于在解除阻塞时使用。

2.2 初始化

#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
  • 参数:
    • cond:条件变量的地址
    • attr:条件变量的属性,一般默认为NULL。

2.3 销毁

// 销毁释放资源        
int pthread_cond_destroy(pthread_cond_t *cond);
  • 参数:
    • cond:条件变量的地址

2.4 阻塞函数

// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
  • 参数:

    • cond:条件变量的地址。
    • mutex:互斥锁变量的地址。
  • 功能:

    • 在对线程进行阻塞时,如果线程已经对mutex上锁,那么会将互斥锁打开,从而避免死锁。
    • 当线程解除阻塞时,会尝试对mutex上锁,如果上锁成功,则可以访问临界区,否则继续阻塞。

2.5 阻塞指定时间

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
  • 参数:

    • cond:条件变量的地址。
    • mutex:互斥锁变量的地址。
    • abstime:一个结构体,用于表示到什么时候解除阻塞。
  • 注意:在使用时,如果timespec中只是用一项,那么一定要将另一项设为0,否则会影响结果。

    time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
    struct timespec tmsp;
    tmsp.tv_nsec = 0;
    tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s
    

2.6 解除阻塞

至少解除一个被阻塞的线程:

// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);

解除所有被阻塞的线程:

// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

3. 实例

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include "list.h"

// 互斥锁
pthread_mutex_t mutex;
// 条件变量
pthread_cond_t empty;
pthread_cond_t full;
// 链表最长
int MAX = 50;
// 链表头节点
Node *head = NULL;

// 消费者线程工作
void *consume(void *arg){
    while (1) {
        pthread_mutex_lock(&mutex);
        while (list_length(head) == 0){
            list_destroy(&head);
            pthread_cond_wait(&empty, &mutex);
        }
        int data = list_get(head);
        printf("consumer get data: %d\n", data);
        // 消费了产品,因此要提醒生产者生产
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&full);
        sleep(rand()%5);
    }
    return NULL;
}
// 生产者线程工作
void *produce(void *arg){
    while (1) {
        pthread_mutex_lock(&mutex);
        // 判断是否已经满了
        // 这里必须使用while,不能用if,if会报错。
        while (list_length(head) == MAX){
            pthread_cond_wait(&full, &mutex);
        }
        if(head == NULL){
            list_init(&head);
        }
        int data = rand()%100;
        printf("producer make data %d\n", data);
        list_insert(head, data);
        // 生产了产品,因此要提醒消费者消费
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&empty);
        sleep(rand()%3);
    }
    return NULL;
}
int main(int argc, const char * argv[]){
    // 初始化互斥锁、条件变量
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&empty, NULL);
    pthread_cond_init(&full, NULL);
    // 子线程,其中5个消费,5个生产
    pthread_t p_tid[5];
    pthread_t c_tid[5];
    // 子线程初始化
    for (int i = 0; i < 5; ++i){
        pthread_create(&p_tid[i], NULL, produce, NULL);
        pthread_create(&c_tid[i], NULL, consume, NULL);
    }
    // 释放子线程
    for (int i = 0; i < 5; ++i){
        pthread_join(p_tid[i], NULL);
        pthread_join(c_tid[i], NULL);
    }
    // 销毁互斥锁、条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&empty);
    pthread_cond_destroy(&full);
    return 0;
}

注意:

关于条件变量的判断,必须使用while而不是if,这是由于如果使用if的话,我们以生产者为例,

  1. 假设当前生产者线程A获取了互斥锁,但此时商品已经满了,条件变量会对其进行阻塞(阻塞在pthread_cond_wait()处),锁也随之释放。
  2. 假设此时生产者线程B再抢到了互斥锁,但此时商品仍是满的,条件变量也会对其进行阻塞(阻塞在pthread_cond_wait()处),锁也随之释放。
  3. 假设此时消费者线程X抢到了锁,消费了产品,并释放了锁,同时通过pthread_cong_signal()释放了至少一个被条件变量阻塞的生产者线程。
  4. 假设此时A和B都被唤醒,A、B开始抢锁,B没抢到,继续阻塞(阻塞在pthread_cond_wait()处)。A抢到了,由于是if判断,无需再次进行判断,因此直接向下执行,生产一个商品,商品又满了。此时A释放锁。
  5. A、B继续抢锁(A通过pthread_mutex_lock()抢锁,B通过pthread_cong_wati()抢锁),结果B抢到了,此时商品虽然已经满了,但由于对于条件变量的判断是if,此时已经判断过了,因此无需判断,直接执行,B又生产了一份。商品溢出了。如果是while循环,那么B抢到锁后会再次判断,结果发现当前商品仍旧是满的,于是继续阻塞在pthread_cong_wati()处,同时释放锁,自然不会再生产了。

六. 信号量

1. 概述

信号量类似于观察者模式,一个线程完成某个任务后,可以通过信号量通知另一个线程,让它继续运行或是阻塞,从而实现对线程状态的控制。

此外,信号量所关注的并不仅仅是临界资源,也包括一些其他操作,比如说输入输出等。信号量主要用于阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要配合互斥锁,一起使用。

信号量和条件变量一样用于“生产者——消费者”模型。

2. 信号量函数

2.1 头文件和信号类型

#include <semaphore.h>		// 头文件
sem_t sem;					// 信号类型

2.2 操作函数

2.2.1 初始化

#include <semaphore.h>
// 信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 参数:
    • sem:信号量变量地址。
    • pshared:
      • 0:线程同步
      • 非0:进程同步。
    • value:初始化当前信号量拥有的资源数(资源数>=0)。如果资源数=0,线程会被阻塞。

2.2.2 销毁

#include <semaphore.h>
// 信号量资源释放
int sem_destroy(sem_t *sem);
  • 参数:
    • sem:信号量变量地址。

2.2.3 调用信号量

#include <semaphore.h>
int sem_wait(sem_t *sem);
  • 参数:
    • sem:信号量变量地址。
  • 效果:当sem资源数>0,时本线程抢占一个资源,sem资源--,本线程正常运行。如果sem资源数==0,本线程阻塞。

注意:和条件变量不同,调用信号量阻塞线程不会释放锁。因此和互斥锁搭配使用时必须先抢占信号量,然后再抢占互斥锁,否则会造成死锁。

2.2.4 尝试调用信号量

#include <semaphore.h>
int sem_trywait(sem_t *sem);
  • 参数:
    • sem:信号量变量地址。
  • 效果:当sem资源数>0,时本线程抢占一个资源,sem资源--,本线程正常运行。如果sem资源数==0,本线程不会阻塞,该函数会返回一个错误号,因此可以在程序中添加判断分支,用于处理获取资源失败后的情况。

2.2.5 指定时长

#include <semaphore.h>
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • 效果:当sem资源数>0,时本线程抢占一个资源,sem资源--,本线程正常运行。如果sem资源数==0,本线程阻塞指定时长,如果阻塞期间抢到资源了,那么阻塞也会结束。

2.2.6 新增资源

#include <semaphore.h>
int sem_post(sem_t *sem);
  • 效果:令sem中的资源数量++,如果有线程因为sem资源不足阻塞,那么它们会开始抢资源,谁抢到谁向下运行,抢不到继续阻塞。

2.2.7 查看资源数

#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
  • 作用:传出参数。通过sval,传出信号量sem当前所拥有的资源数量。

3. 生产者、消费者

3.1 分析

生产者和消费者是两类线程,运行逻辑不一样,因此需要定义两个信号量:

  • 生产者信号量:信号量当中的资源象征着同时有多少个生产者可以生产,因此根据需要初始化,如初始化为5,意味着有5个生产者可以参与生产。
  • 消费者信号量:资源初始化为0,生产者线程生产资源,从而唤醒消费者线程。

3.2 实例

3.2.1 总资源数为1

当生产者信号量资源+消费者信号量资源==1,此时不论有多少线程,同一时间只有一个线程可以访问任务队列,因此无需使用互斥锁维护临界资源。

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

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        // 生产者拿一个信号灯
        sem_wait(&psem);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());

        // 通知消费者消费, 给消费者加信号灯
        sem_post(&csem);
        

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        sem_wait(&csem);
        // 取出链表的头结点, 将其删除
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        free(pnode);
        // 通知生产者生成, 给生产者加信号灯
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化信号量
    // 生产者和消费者拥有的信号灯的总和为1
    sem_init(&psem, 0, 1);  // 生成者线程一共有1个信号灯
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    sem_destroy(&psem);
    sem_destroy(&csem);

    return 0;
}

3.2.2 多个资源

当生产者信号量资源+消费者信号量资源>1,此时说明存在多个线程同时访问临界资源的情况,因此需要配合互斥锁,实现线程同步。

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

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        // 生产者拿一个信号灯
        sem_wait(&psem);
        // 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
        pthread_mutex_lock(&mutex);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
        pthread_mutex_unlock(&mutex);

        // 通知消费者消费
        sem_post(&csem);
        
        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        // 取出链表的头结点, 将其删除
        free(pnode);
        pthread_mutex_unlock(&mutex);
        // 通知生产者生成, 给生产者加信号灯
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化信号量
    sem_init(&psem, 0, 5);  // 生成者线程一共有5个信号灯
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    sem_destroy(&psem);
    sem_destroy(&csem);
    pthread_mutex_destroy(&mutex);

    return 0;
}

注意:

不论是消费者还是生产者,必须先抢到信号量资源,然后才能去抢锁:

// 消费者
sem_wait(&csem);
pthread_mutex_lock(&mutex);

// 生产者
sem_wait(&csem);
pthread_mutex_lock(&mutex);

如果顺序颠倒,可能会造成死锁,这是因为信号量阻塞线程时不会释放该线程抢占的锁资源,从而导致死锁。例如:

  1. 程序启动时,消费者先抢到了任务队列的锁。
  2. 此时消费者资源数为0,因此信号量阻塞了消费者线程,但互斥锁没有释放。
  3. 此时生产者无法抢到互斥锁,线程阻塞,也就无法生产新商品。
  4. 没有新商品,消费者持续阻塞。死锁出现了。
posted @   BinaryPrinter  阅读(146)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示