线程同步(三)

news2024/11/24 9:06:14

目录

条件变量

条件变量操作函数函数原型:

线程阻塞函数:

唤醒阻塞线程: 

生产者和消费者模型

信号量函数

生产者和消费者模型

 总结


条件变量

        条件变量是一种线程间同步的机制,用于协调线程之间的操作。当一个线程正在等待某个条件变成真,而另一个线程修改了该条件时,条件变量就可以通知等待的线程。这样,等待的线程就可以继续执行,而不必浪费时间轮询条件是否成立。

        在使用条件变量时,通常需要先定义一个互斥量,保护共享资源的访问。然后,等待线程通过调用条件变量的wait函数来等待条件的成立。当其他线程修改了条件并通过调用条件变量的signal函数或broadcast函数来通知时,等待线程就会被唤醒,重新获取互斥量并检查条件是否成立。如果条件仍然不成立,等待线程就会再次进入等待状态。

        条件变量通常与互斥量一起使用,以确保线程安全,并避免条件竞争的发生。

条件变量操作函数函数原型:

#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
// 销毁释放资源        
int pthread_cond_destroy(pthread_cond_t *cond);

//cond: 条件变量的地址

//attr: 条件变量属性,一般使用默认属性,指定为 NULL

线程阻塞函数:

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

        通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:

        在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
        当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区

// 表示的时间是从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);

        这个函数的前两个参数和 pthread_cond_wait 函数是一样的,第三个参数表示线程阻塞的时长,但是需要额外注意一点:struct timespec 这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点:

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

唤醒阻塞线程: 

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

        调用上面两个函数中的任意一个,都可以唤醒被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程,区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast 是唤醒所有被阻塞的线程

生产者和消费者模型

        生产者消费者模型是一种经典的线程同步机制,用于解决多线程间共享资源的问题。

        在生产者消费者模型中,生产者线程负责生成数据并将其存入一个共享的缓冲区中,而消费者线程则负责从缓冲区中取出数据并对其进行处理。生产者和消费者通过共享的缓冲区来进行通信,因此需要进行线程同步来保证数据的正确性和一致性。

        生产者消费者模型通常使用一个有限大小的队列来作为共享的缓冲区。当队列已满时,生产者线程需要等待;当队列为空时,消费者线程需要等待。为了避免死锁和资源浪费,需要使用条件变量和互斥量来进行线程同步。

        一般来说,生产者线程将数据放入队列之前需要获取互斥量,以防止多个线程同时访问队列造成数据冲突。然后,如果队列已满,生产者线程就需等待条件变量发出队列有空位的信号。待收到信号后,生产者线程就会将生产的数据放入队列中,释放互斥量并通知等待的消费者线程。消费者线程也是类似的过程,只不过相反。

        生产者消费者模型是一种常见的多线程编程范例,被广泛应用于计算机系统、操作系统和数据库等领域中。它可以有效地解决多线程间共享资源的同步和协作问题。

代码:

        使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。

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

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

// 定义条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        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);

        // 生产了任务, 通知消费者消费
        pthread_cond_broadcast(&cond);

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

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        // 一直消费, 删除链表中的一个节点
//        if(head == NULL)   // 这样写有bug
        while(head == NULL)
        {
            // 任务队列, 也就是链表中已经没有节点可以消费了
            // 消费者线程需要阻塞
            // 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开
            // 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁
            // 这函数会自动将线程拥有的锁解开
            pthread_cond_wait(&cond, &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);        

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

int main()
{
    // 初始化条件变量
    pthread_cond_init(&cond, NULL);
    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);
    }

    // 销毁条件变量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

运行结果: 

       可以看到生产者将商品放到任务队列中,任务队列满了就阻塞,不满的时候就工作。消费者读任务队列,将任务或者数据取出,任务队列中有数据就消费,没有数据就阻塞。

信号量函数

信号量(Semaphore)是一种线程间同步的机制,它是一个整数,用于控制对共享资源的访问。信号量有两个基本操作:等待和通知(或称为 P 操作和 V 操作)。等待操作使信号量的值减 1,如果信号量的值为负,则进程或线程将阻塞,直到其它进程或线程释放资源并使信号量的值变为非负为止。通知操作则使信号量的值加 1,如果此时有其它进程或线程正在等待,则其中一个将被唤醒。

在 POSIX 标准中,提供了以下几个信号量函数:

sem_init()//初始化一个信号量。
sem_destroy()//销毁一个信号量。
sem_post()//执行 V 操作,使信号量的值加 1。
sem_wait()//执行 P 操作,如果信号量的值为正,则将其减 1;否则将当前线程或进程阻塞,直到其它线程或进程通知并唤醒它。
sem_trywait()//尝试执行 P 操作,如果信号量的值为正,则将其减 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;
}

运行结果: 

 总结:

信号量和条件变量都是用于线程间同步和互斥的机制,但它们的作用和使用方式略有不同:

        1. 信号量主要用于控制对共享资源的访问,它的值可以表示共享资源的数量或者可用的资源数量。信号量的增减操作是原子性的。P 操作和 V 操作可以分别用于申请和释放资源,从而控制线程的访问。信号量不关心具体的资源内容,只关心资源的可用性和数量。
        2. 条件变量用于等待某个条件的成立,通常和互斥量结合使用。条件变量的等待操作和通知操作分别对应于 wait 和 signal 函数。wait 函数将线程挂起,直到条件成立或者被其它线程唤醒;signal 函数则用于唤醒等待线程中的一个或多个,从而满足某个条件的成立。

        因此,信号量和条件变量有着不同的适用场景和用法,常常用于不同的同步和互斥问题中。一般来讲,如果只需要控制对共享资源的访问,可以使用信号量;如果需要等待某个条件的成立再进行操作,可以使用条件变量。实际上,在某些情况下,信号量和条件变量可以结合起来使用,实现更复杂的同步和互斥问题的解决。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/644675.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅谈银桥乳业局域网设计与实现_kaic

摘 要 迈入二十一世纪&#xff0c;在互联网智能制造的加持下。各公司企业不断提升管理制造能力。云计算、新基建、大数据等技术日新月异。不断冲击着管理方式。企业局域网作为企业基建基础到越来越变得重要的。伴随着企业财务业务一体化的需求。ERP系统、CRM系统、HR系统、MES…

动态域名服务 DDNS,YYDS(四)

来源&#xff1a;公众号【鱼鹰谈单片机】 作者&#xff1a;鱼鹰Osprey ID &#xff1a;emOsprey 前面的笔记《如何像访问百度一样访问家里的服务器&#xff1f;&#xff08;三&#xff09;》已经通过公网 IP 桥接&#xff0c;完成了基本功能&#xff0c;已经可以通过公网 IP…

面试问题总结---SLAM部分

1、本栏用来记录社招找工作过程中的内容,包括基础知识学习以及面试问题的记录等,以便于后续个人回顾学习; 暂时只有2023年3月份,第一次社招找工作的过程; 2、个人经历: 研究生期间课题是SLAM在无人机上的应用,有接触SLAM、Linux、ROS、C/C++、DJI OSDK等; 3、参加工作后…

嵌入式软件开发岗位----求职过程记录(基础知识和面经总结)

1、本栏用来记录社招找工作过程中的内容&#xff0c;包括基础知识以及面试问题等&#xff0c;以便于后续个人回顾学习&#xff1b; 暂时只有2023年3月份&#xff0c;第一次社招找工作的过程&#xff1b; 2、个人经历&#xff1a; 研究生期间课题是SLAM在无人机上的应用&#xf…

深入理解 JavaScript Promise

1. 引言 JavaScript中的Promise是一种处理异步操作的机制&#xff0c;它提供了一种优雅的方式来处理回调函数地狱和异步代码的流程控制。本文将深入介绍JavaScript中的Promise&#xff0c;帮助读者更好地理解和应用Promise。 2. Promise的基本概念 Promise是一个代表异步操作…

唯一客服系统(独立部署无限多开)-知识库ChatGPT-支持微信公众号小程序-钉钉-PC和H5全渠道客服系统...

产品介绍 唯一客服系统是基于Golang语言自主开发的在线客服系统。创立于2019年初&#xff0c;是一款连接企业与客户的即时通讯项目&#xff0c;遵循快速、简洁的开发原则&#xff0c;是为中小企业量身定制的全渠道客服系统&#xff0c;致力于帮助广大开发者/公司快速部署整合私…

第四十七章 液态网络

如弗洛格老师所料&#xff0c;巴哥奔果真倒头睡掉了一夜一昼又一夜。 再次醒来&#xff0c;浑身酸痛仍在&#xff0c;却是以鸡皮疙瘩的形式存在于皮肤上。临鸾连续弹出两个数字&#xff0c;其一是时间&#xff0c;其二是任务量。 时间很快得到室友们的确认&#xff0c;没错&…

Vue中如何进行移动端手势操作?

Vue中如何进行移动端手势操作&#xff1f; 在移动端开发中&#xff0c;手势操作是非常常见的功能&#xff0c;例如滑动、缩放、旋转等操作。在Vue.js中&#xff0c;我们可以使用第三方插件或者自己编写指令来实现手势操作。本文将介绍如何在Vue.js中实现移动端手势操作。 使用…

OpenGL蓝宝书第十章学习笔记:计算着色器

前言 本篇在讲什么 OpenGL蓝宝书第十章学习笔记计算着色器 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C语法有简单认知 对OpenGL有简单认知 最好是有OpenGL超级宝典蓝宝书 依赖Visual Studio编辑器 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻…

Idea+maven+springboot项目搭建系列--2 整合Rabbitmq完成客户端服务器端消息收发

前言&#xff1a;本文通过springBoot -maven 框架&#xff0c;对Rabbitmq 进行整合&#xff0c;完成客户端消息的发送和消费&#xff1b; 1 为什么要使用Rabbitmq&#xff1a; RabbitMQ 是一个可靠的、灵活的、开源的消息中间件&#xff0c;具有以下优点&#xff1a; 异步通信…

【C++】C++前言

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 1.什么是C 2.C的发展史 3.C的重要性 a.使用广泛 b.C/C的应用 1.操作系统以及大型系统软件开发 2.服务器端开发 3.游戏开发 4.嵌入式和物联网领域 5.数字图像处理 6.人工智…

AVL树的解析

我们在之前的学习里面已经发现了&#xff0c;搜索二叉树是有一些问题的。它可能会存在单边树的问题&#xff0c;如果你插入的值是有序的话&#xff0c;就会导致这个问题。 那我们肯定是要来解决一下的&#xff0c;如何解决呢&#xff1f; 》一种解决方案是AVL树&#xff0c;还有…

【云原生 | 54】Docker三剑客之Docker Compose应用案例二:大数据Spark集群

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 &#x1f3c5;阿里云ACE认证高级工程师 &#x1f3c5;阿里云开发者社区专…

天下苦 Spring 久矣,Solon v2.3.3 发布

Solon 是什么框架&#xff1f; 一个&#xff0c;Java 新的生态型应用开发框架。它从零开始构建&#xff0c;有自己的标准规范与开放生态&#xff08;全球第二级别的生态&#xff09;。与其他框架相比&#xff0c;它解决了两个重要的痛点&#xff1a;启动慢&#xff0c;费资源。…

HarmonyOS学习路之开发篇—Java UI框架(PositionLayoutAdaptiveBoxLayout)

PositionLayout 在PositionLayout中&#xff0c;子组件通过指定准确的x/y坐标值在屏幕上显示。(0, 0)为左上角&#xff1b;当向下或向右移动时&#xff0c;坐标值变大&#xff1b;允许组件之间互相重叠。 PositionLayout示意图 布局方式 PositionLayout以坐标的形式控制组件的…

基于Hexo和Butterfly创建个人技术博客,(4) 使用通用的Markdown语法编写博客文章

Hexo官司网查看 这里 hexo的博文建议是用markdown语法来写&#xff0c;原因markdown简单通用&#xff0c;比如很多博客平台都会提供md编辑器&#xff0c;这样如果我们想把同一篇文章发到多个博客平台上(事实上很多人也是这样做的)&#xff0c;md应该是最好的编写方法了&#xf…

目标检测数据集---交通信号数据集

✨✨✨✨✨✨目标检测数据集✨✨✨✨✨✨ 本专栏提供各种场景的数据集,主要聚焦:工业缺陷检测数据集、小目标数据集、遥感数据集、红外小目标数据集,该专栏的数据集会在多个专栏进行验证,在多个数据集进行验证mAP涨点明显,尤其是小目标、遮挡物精度提升明显的数据集会在该…

js控制台 console.log 输出美化,及其他操作

1.格式美化 console.log(%c红色%c蓝色%c绿色, color: red;, color: blue;, color: green;) console.log(%c一段文字\n换行一下\n%c SmileSay %c 版本&#xff1a;1.0.0 ,color: #3eaf7c; font-size: 16px;line-height:30px;,background: #35495e; padding: 4px; border-radius…

数仓数据质量保障方法

一、有赞数据链路 1、数据链路介绍 首先介绍有赞的数据总体架构图&#xff1a; 自顶向下可以大致划分为应用服务层、数据网关层、应用存储层、数据仓库&#xff0c;并且作业开发、元数据管理等平台为数据计算、任务调度以及数据查询提供了基础能力。 以上对整体架构做了初步…

射频电路layout总结

射频电路板设计由于在理论上还有很多不确定性&#xff0c;因此常被形容为一种“黑色艺术”&#xff0c;但这个观点只有部分正确&#xff0c;RF电路板设计也有许多可以遵循的准则和不应该被忽视的法则。在实际设计时&#xff0c;真正实用的技巧是当这些准则和法则因各种设计约束…