UNIXC002 线程的同步 之 mutex锁、条件变量、信号量

1. 可重入函数

在这里插入图片描述

  • 如果一个函数访问了全部变量,静态局部变量,还有堆里的内容,那么这个函数就是不可重入函数。
  • 线程之间是异步的,如果T1,T2都要访问进程数据段的资源(比如T1写,T2读),会造成不确定性。这时的T1,T2对应的函数都是不可重入函数。所以需要用线程同步来解决这种问题。

2. 临界资源

2.1 临界资源

在这里插入图片描述

2.2 临界区

在这里插入图片描述

3. 线程同步的三种方式

3.1 互斥锁(mutex)

  • 互斥锁是一种互斥设备,用来保证共享数据操作的完整性。互斥锁类型的对象标记用来保证在任何时刻,只能有一个线程访问该对象。
  • 互斥锁类型:pthread_mutex_t

下面是和互斥锁类型相关的操作。

3.1.1 pthread_mutex_init(3)

在这里插入图片描述

3.1.2 pthread_mutex_destroy(3)

在这里插入图片描述

3.1.2 pthread_mutex_lock(3)

在这里插入图片描述

3.1.3 pthread_mutex_trylock(3)

在这里插入图片描述

3.1.4 pthread_mutex_unlock(3)

在这里插入图片描述

3.1.5 代码示例

#include "t_stdio.h"
#include <pthread.h>
#include <unistd.h>

// 定义一个mutex锁类型的变量,不能写在main函数里,因为这样 mutex 就是局部变量了,handle 函数里是访问不到
pthread_mutex_t mutex;
int val = 0; // 全局变量在数据段

//线程的执行函数
void *handle(void *arg){
    int tmp;
    for(int i = 0; i < 100; i++){
        // 访问到全局变量再加锁, 尽量缩小锁的范围
        // 加锁
        pthread_mutex_lock(&mutex);
        tmp = val;
        tmp++;
        printf("tip:%lu\ttmp=%d\n",pthread_self(), tmp);
        val = tmp;
        // 解锁
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

int main(void){
    // 初始化mutex锁
    pthread_mutex_init(&mutex, NULL);

    // 创建两个线程
    pthread_t tid, pid;
    pthread_create(&tid, NULL, handle, NULL);
    // sleep(2);
    pthread_create(&pid, NULL, handle, NULL);
    // 等待线程的汇合
    pthread_join(tid, NULL);
    pthread_join(pid, NULL);

    // 销毁mutex锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

$ gcc mutex.c -lpthread
$ ./a.out 
tip:140704450934528     tmp=1
tip:140704450934528     tmp=2
....
# 两个线程交替执行,第一个线程执行完把锁释放,第二个线程就可以执行
tip:139724164568832     tmp=100
tip:139724156176128     tmp=101
....
tip:140704442541824     tmp=200

3.2 条件变量 (condition variable)

在这里插入图片描述

  • 条件不满足的情况下,线程会停下来,不会占用CPU(释放处理器)。以便其他的线程继续使用CPU。这不是盲等。
  • 条件变量为真时,发送给 等待条件变量为真 的线程。条件变量不满足时,就等待条件变量满足。等两种方式: 死等,和规定等待时间。

3.2.1 pthread_cond_t

在这里插入图片描述
下面是 pthread_cond_t 类型的相关操作函数

3.2.2 pthread_cond_init(3)

在这里插入图片描述

3.2.3 pthread_cond_destory(3)

在这里插入图片描述

3.2.4 pthread_cond_signal(3)

在这里插入图片描述

3.2.5 pthread_cond_boradcast(3)

在这里插入图片描述

3.2.6 pthreaad_cond_wait(3)

在这里插入图片描述

3.2.7 pthread_cond_timewait(3)

在这里插入图片描述

3.2.8 实例分析、代码示例

1. 生产者线程负责生产一个新节点,然后把新节点插入到链表头部。
在这里插入图片描述

  • 向链表中添加新节点的时候,有两种情况,链表头为空和头不为空.(headnew都是节点的地址)
    • head == NULL头为空时,head = new;
    • head != NULL头不为空时,head里的值是一个节点的地址(head已经指向了一个节点)。这时只需要让new所指向节点的next变为head指向节点的地址,然后再把head的值变为new的值。new就成了新的head.
      • new -> next = head;
      • head = new;
  • 第二种情况的处理方法也能用于第一种,只不过这样的话,最后链表中最后一个节点的nextNULL

2. 消费者线程负责从链表头部摘取一个节点。
在这里插入图片描述

  • 从链表头部摘取一个节点:
    • 取:tmp = head;
    • 摘:head = head -> next;

cond.c

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

// 定义一个链表节点类型
typedef struct node{
    int data;
    struct node *next;
}node_t;

// 定义链表头指针
typedef node_t *list_t;
list_t head =NULL;

// 定义锁类型的变量
pthread_mutex_t mutex;

//定义条件变量
pthread_cond_t cond;


// "生产者线程"
void *product(void *arg){
    node_t *new = NULL;
    while(1){
        // 生产一个新节点
        new = (node_t *)malloc(sizeof(node_t));
        new->data = rand()%10+1;
        new->next = NULL;
        printf("p: %d\n", new->data);

        // 访问到全局变量,加锁
        pthread_mutex_lock(&mutex);
        //并插入到链表的头部
        new -> next = head;
        head = new;
        // 解锁
        pthread_mutex_unlock(&mutex);
        // 告知那些等待条件变为真的线程 
        pthread_cond_signal(&cond);
        sleep(rand()%3+1);
    }
    return  NULL;
}

// "消费者线程"
void *consume(void *arg){
    node_t *tmp;
    while(1){
        // 加锁
        pthread_mutex_lock(&mutex);
        // 从链表头部摘取一个节点.
        while (head == NULL) {
            // pthread_cond_wait 条件不满足的情况下,线程会停下来, 然后解锁,等到条件变为真。解锁是原子操作,不解锁的话 product 是无法访问head的
            // 等条件变量为真时(即等product中 pthread_cond_signal(&cond); 执行以后,条件变为真,且head不再是NULL),
            // 重新加锁,不会在进入这个循环, 除非等消费完了,head=NULL
            pthread_cond_wait(&cond, &mutex);
        }    
        tmp = head;
        head = head -> next;
        // 解锁
        pthread_mutex_unlock(&mutex);
        printf("c: %d\n", tmp->data);
        // 消费该节点
        free(tmp);
        tmp = NULL;
        sleep(rand()%3+1);
    }
    return NULL;
}

int main(void) {
    srand(time(NULL));
    // 初始化锁类型的变量
    pthread_mutex_init(&mutex, NULL);
    // 初始化条件变量
    pthread_cond_init(&cond, NULL);
    // 创建两个线程,“生产者” “消费者”
    pthread_t pid, cid;
    pthread_create(&pid, NULL, product, NULL);
    pthread_create(&cid, NULL, consume, NULL);

    // 等待线程汇合
    pthread_join(pid, NULL);
    pthread_join(pid, NULL);

    // 销毁锁
    pthread_mutex_destroy(&mutex);
    // 销毁条件变量
    pthread_cond_destroy(&cond);
    return 0;
}
$ gcc cond.c -lpthread
$ ./a.out 
# 生产
p: 2
# 消费, 消费的一定是前面生产的
c: 2
p: 3
c: 3
p: 8
c: 8
# 连着生产了两次
p: 2
p: 10
# 又碰巧连着生产了两次
c: 10
c: 2
p: 3
p: 10
c: 10
c: 3

3.3 信号量

在这里插入图片描述

下面是有关信号量类型sem_t的操作函数

3.3.1 sem_init(3)

在这里插入图片描述

  • pshared 非0用于多进程间共享数据的保护。

3.3.2 sem_destroy(3)

在这里插入图片描述

3.3.3 sem_post(3)

在这里插入图片描述

3.3.4 sem_wait(3)

在这里插入图片描述

3.3.5 sem_trywait(3)

在这里插入图片描述

3.3.6 sem_timedwait(3)

在这里插入图片描述

3.3.7 实例分析、代码示例

在这里插入图片描述
队列: 先进先出
在这里插入图片描述
生产者线程实现:

  • 生产者线程中 sem_wait(&p) 看下可p是不是0。如果是就不能再生产了。然后就阻塞等待,消费者线程中消费以后,sem_post(&p) 给可生产数 p加1;
  • 这样p就大于0了,生产者线程就可以继续往下执行,给p减1后,给que[n] 一个随机数
  • n=(n+1)%7; 当n+1==7 时,n==0,限制生产
  • 最后sem_post(&c) 给可消费数加1, 这是如果消费者线程sem_wait(&c),正等着,那么它将被唤醒,给c减1,继续消费
    s
    消费者线程实现 :
  • 消费者线程中 sem_wait(&c), 如果c是 0,就不能再消费了,然后就阻塞等待。等待生产者线中的sem_post(&c)c加1
  • 然后消费者线程就可以继续执行,给c减1后que[m] = -1,相当于消费了,-1不在1-500随机数里。
  • 最后sem_post(&p) 给生产数加1, 如果生产者线程中 sem_wait(&p) 正等着,那么它将被唤醒,给p减1,继续生产

sem.c

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

// 定义队列
int que[7];
// 定义信号量类型变量, 分别用于生产者(可生产数量),消费者(可消费数量)
sem_t p, c;

// "生产者线程"
void *product(void *arg){
    int n = 0;
    while(1){
        // 如果p的值=0,阻塞。p不=0就减1继续执行
        sem_wait(&p);
        que[n] = rand()%500+1;
        printf("p: index: %d\tvalue: %d\n",n, que[n]);
        n = (n+1)%7;
        // 可消费数量加1
        sem_post(&c); 
        sleep(rand()%3+1);
    }
    return  NULL;
}

// "消费者线程"
void *consume(void *arg){
    int m = 0; int tmp;
    while(1){
        sem_wait(&c);
        tmp = que[m];
        printf("c: index: %d\tvalue: %d\n",m, que[m]);
        que[m] = -1;
        m = (m+1)%7;
        // 可生产数量加1
        sem_post(&p);
        sleep(rand()%3+1);
    }
    return NULL;
}

int main(void) {
    srand(time(NULL));
    // 初始化信号量,指定信号量初始值
    sem_init(&p, 0, 7);
    sem_init(&c, 0, 0);

    // 创建两个线程,“生产者” “消费者”
    pthread_t pid, cid;
    pthread_create(&pid, NULL, product, NULL);
    pthread_create(&cid, NULL, consume, NULL);

    // 等待线程汇合
    pthread_join(pid, NULL);
    pthread_join(pid, NULL);

    // 销毁信号量
    sem_destroy(&p);
    sem_destroy(&c);
    return 0;
}
$ gcc sem.c -lpthread
$ ./a.out 
p: index: 0     value: 176
c: index: 0     value: 176
p: index: 1     value: 115
c: index: 1     value: 115
p: index: 2     value: 281
c: index: 2     value: 281
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值