【Linux进程篇】并发艺术:Linux条件变量与生产消费者模型的完美融合

news2024/9/20 20:22:12

W...Y的主页 😊

代码仓库分享💕 


前言:我们上篇博客中提到了线程互斥、引出了互斥锁解决了线程中的未将临界资源保护的问题。但是随之出来的问题——竞争锁是自由竞争的,竞争锁的能力太强的线程会导致其他线程抢不到票,所以导致有些版本下进程会出现饥饿问题,这就要引出线程同步问题。

首先我们用一个实例来说明一下饥饿问题:有一个图书自习室一次只能进去一个人去学习,但是进入必须要有一个钥匙。当有个人进去时就其余的人就必须在外面等待,当有资格学习的人出来看到外面排着长队时就觉得下次如果在想进入自习室很难,于是拿着钥匙又进去了,就这样卡bug占用自习室一天,剩下的人只能是在自习室外等候。这就造成了其余人的饥饿问题,根本抢不到进入自习室的钥匙。
但是这个bug没有违反自习室一次只能进去一个人的规则,所以学校看到这个现象就修复了这个bug。当一个人从自习室出来不能马上再返回自习室,而是要到队尾进行排队进入。这样的方法让不同的人保证自习室只有一个人的前提下,让所有人都有去自习室学习的顺序性!!!

这个工作被我们称为同步。

目录

Linux线程同步

条件变量

同步概念与竞态条件

条件变量函数 初始化

销毁

等待条件满足

唤醒等待

常见锁概念

死锁

生产者消费者模型

 基于BlockingQueue的生产者消费者模型


Linux线程同步

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

那我们如何做到线程的同步呢?这就要使用我们的条件变量!

我们先来说明一下什么是条件变量:桌子上放了一个盘子,有两个参与人员一个瞎子和一个哑巴。哑巴会往盘子中放入苹果,瞎子要去盘子中取苹果。因为哑巴会不定时的在盘子中放入苹果但是不会说话,所以瞎子就会不断的确认盘子中是否有苹果,有苹果就将苹果取出来。首先我们必须保证再哑巴放苹果时瞎子不会去取苹果(互斥),这个苹果就像临界资源,而临界资源应该被保护,所以就会有一个mutex。但是瞎子不知道什么时候有苹果,所以导致瞎子就会一直访问临界资源申请mutex。所以哑巴就申请不到锁访问不了临界资源也就是放不了苹果,瞎子做的都是无效动作,导致哑巴有了饥饿问题。

所以哑巴在身边放了一个铃铛,当自己放入苹果后就会敲响铃铛告诉瞎子盘子里有苹果了,这样瞎子就可以取苹果了。当一群瞎子去取苹果,发现没有时就不会去继续访问,而是在一旁排队等待,直到哑巴放入苹果后敲响铃铛可以将我们队头的瞎子,或者所有的瞎子都叫过来去访问资源!!

这里的铃铛和队列等整体组合就是条件变量。 

条件变量函数 初始化

pthread_cond_t pthread_cond_t cond = PTHREAD_COND_INITIALIZER,这个与互斥锁中的规则一样在全局、静态使用上述宏初始化。局部使用ptjread_cond_init函数初始化。 

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL 

销毁

int pthread_cond_destroy(pthread_cond_t *cond) 

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释 

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);

唤醒所有在cond等待的线程
int pthread_cond_signal(pthread_cond_t *cond); 

唤醒一个在cond等待的线程

简单案例: 

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<string>
using namespace std;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void *SlaverCore(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        // 1. 加锁
        pthread_mutex_lock(&gmutex);
        // 2. 一般条件变量是在加锁和解锁之间使用的
        pthread_cond_wait(&gcond, &gmutex); // gmutex:这个是,是用来被释放的[前一半]
        std::cout << "当前被叫醒的线程是: " << name << std::endl;
        // 3. 解锁
        pthread_mutex_unlock(&gmutex);
    }
}

void *MasterCore(void *args)
{
    sleep(3);
    std::cout << "master 开始工作..." << std::endl;
    std::string name = static_cast<const char *>(args);
    while (true)
    {
         pthread_cond_signal(&gcond);// 唤醒其中一个队列首部的线程
        //pthread_cond_broadcast(&gcond);// 唤醒队列中所有的线程
        std::cout << "master 唤醒一个线程..." << std::endl;
        sleep(1);
    }
}
void StartMaster(std::vector<pthread_t> *tidsptr)
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, MasterCore, (void *)"Master Thread");
    if (n == 0)
    {
        std::cout << "create master success" << std::endl;
    }
    tidsptr->emplace_back(tid);
}

void StartSlaver(std::vector<pthread_t> *tidsptr, int threadnum = 3)
{
    for (int i = 0; i < threadnum; i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "slaver-%d", i + 1); // thread-1
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, SlaverCore, name);
        if (n == 0)
        {
            std::cout << "create success: " << name << std::endl;
            tidsptr->emplace_back(tid);
        }
    }
}
void WaitThread(std::vector<pthread_t> &tids)
{
    for (auto &tid : tids)
    {
        pthread_join(tid, nullptr);
    }
}
int main()
{
    vector<pthread_t> tids;
    StartMaster(&tids);
    StartSlaver(&tids);
    WaitThread(tids);
    return 0;
}

我们创建两个函数,第一个函数创建一个master线程用来唤醒对列中的子线程。第二个函数创建三个线程,其中公共资源就是cout输出,我们在cout周围加锁,然后进行pthread_cond_wait等待,等待master线程使用pthread_cond_signal唤醒一个线程循环。我们可以在运行结果中看到1、2、3分别被唤醒,这就说明这些线程都是在一个队列中存储的。

为什么 pthread_cond_wait 需要互斥量?

条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

我们先来认识一下什么是锁:

常见锁概念

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

最常见的场景就比如两个线程需要访问同一块公共资源,但是访问公共资源时必须要有两把锁才可以进行访问。但是因为程序员的问题,导致两个进程在争夺锁的顺序不同。线程1先争夺锁1再争夺锁2。而线程2刚好反过来导致线程1争夺下锁1时线程2争夺了锁2。但是接下来他们两个就想争夺自己没有的锁,但是这两个锁分别被两个进程都持有1把,然后进入休眠没有释放锁,最终导致死锁问题。 

死锁四个必要条件:

互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系 

如何避免死锁:
破坏死锁的四个必要条件
加锁顺序一致(最好实现)
避免锁未释放的场景
资源一次性分配

 避免死锁算法有:死锁检测算法、银行家算法。

所以再回到上述唤醒等待时,我们需要再pthread_cond_wait函数中加入互斥锁!

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,
会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。

生产者消费者模型

什么是生产消费者模型呢?就比如超市就是一个典型的生产消费者模型,供货商、超市、消费者就是三种关系。供货商将商品提供给超市,而超市将商品卖给消费者。这种关系就是生产消费者模型。而提供的商品就是数据,超市是可以临时保存数据的一个场所,我们一般使用一种数据结构进行存储。

而在这种模型下肯定会有其并发问题。比如当生产者放数据时消费者直接拿走,导致数据不统一等等。而最终会归结为三种关系:生产者VS生产者、消费者VS消费者、生产者VS消费者。他们直接肯定都会有同步和互斥的关系,两种角色和一个交易场所。 为何要使用生产者消费者模型 

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型优点:解耦、支持并发、支持忙闲不均。

 基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

 

我们先从单生产单消费模型开始,因为单生产单消费只需要维护生产者与消费者之间的关系,然后进行多生产多消费:

blockqueue.hpp 

#ifndef __BLOCK_QUEUE_HPP__
#define __BLOCK_QUEUE_HPP__

#include <iostream>
#include <unistd.h>
#include <queue>
#include <pthread.h>
using namespace std;

template <typename T>
class BlockQueue
{
public:
    BlockQueue(int cap)
        :_cap(cap)
    {
        _productor_wait_num = 0;
        _consumer_wait_num = 0;
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_product_cond,nullptr);
        pthread_cond_init(&_consum_cond,nullptr);
    }
    bool IsFull()
    {
        return _block_queue.size() == _cap;
    }
    bool IsEmpth()
    {
        return _block_queue.empty();
    }
    void Enqueue(const T& in)
    {
        pthread_mutex_lock(&_mutex);
        while(IsFull())
        {
            _productor_wait_num++;
            pthread_cond_wait(&_product_cond, &_mutex);
            _productor_wait_num--;
        }
        _block_queue.push(in);
        if(_consumer_wait_num > 0)pthread_cond_signal(&_consum_cond);
        pthread_mutex_unlock(&_mutex);
    }
    void Pop(T* out)
    {
        pthread_mutex_lock(&_mutex);
        while(IsEmpth())//健壮性
        {
            _consumer_wait_num++;
            pthread_cond_wait(&_consum_cond, &_mutex);
            _consumer_wait_num--;
        }
        *out = _block_queue.front();
        _block_queue.pop();
        if(_productor_wait_num > 0)pthread_cond_signal(&_product_cond);
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue() 
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_product_cond);
        pthread_cond_destroy(&_consum_cond);
    }
    
private:
   std::queue<T> _block_queue;   // 阻塞队列,是被整体使用的!!!
    int _cap;                     // 总上限
    pthread_mutex_t _mutex;       // 保护_block_queue的锁
    pthread_cond_t _product_cond; // 专门给生产者提供的条件变量
    pthread_cond_t _consum_cond;  // 专门给消费者提供的条件变量

    int _productor_wait_num;
    int _consumer_wait_num;
};

#endif

 我们需要两把锁和一个条件变量,当往阻塞队列中放入和拿出东西时必须是有锁的加持。但是当队列为空或为满时,消费者和生产者就必须阻塞等待,这时我们就必须要有一个条件变量进行控制,当生产者生产了一个任务时,我们可以通知消费者去拿去对应的内容。

这里需要强调的点是Enqueue与pop函数中的判断队列是否为空的while语句,很多人觉得应该使用if,但是我们的代码中唤醒队列是每添加一个或消费一个内容就会调用唤醒函数,所以使用if只能有一次判断的机会,但是当有五个消费者时,我们使用pthread_cond_broadcast函数将所有消费者全部唤醒,只有一个任务时,只有一个消费者可以拿到任务,其余消费者被异常唤醒也再竞争锁,线程1继续往下执行代码,其余线程不会在条件变量下等待,而是在等待锁释放。当线程1将锁释放完后其余四个线程会直接从pthread_cond_wait函数下竞争锁然后继续执行内容,但是阻塞队列中没有数据从而产生问题。

这个代码中通知次数肯定远远大于休眠次数,所以我们肯定就会有过剩的通知来唤醒刚休眠的线程,所以就有可能被异常唤醒。所以我们就要使用while来一直进行判断。

main.cc 

#include "BlockQueue.hpp"
#include "Thread.hpp"
#include "Task.hpp"
#include <string>
#include <vector>
#include <unistd.h>
#include <ctime>
int a = 10;
using namespace ThreadModule;
using namespace std;
using blockqueue_t = BlockQueue<Task>;

void PrintHello()
{
    cout << "hello" << endl;
}
void Consumer(blockqueue_t &bq)
{
    while (true)
    {
        // 1. 从blockqueue取下来任务
        Task t;
        bq.Pop(&t);
        // 2. 处理这个任务
        t(); //消费者私有
        //std::cout << "Consumer Consum data is : " << t.ResultToString() << std::endl;
    }
}
void Productor(blockqueue_t &bq)
{
    // srand(time(nullptr)^pthread_self());
    // int cnt = 10;
    while (true)
    {
        sleep(1);
        // // 1. 获取任务
        // int a = rand() % 10 + 1;
        // usleep(1234);
        // int b = rand() % 20 + 1;
        // Task t(a, b);
        // 2. 把获取的任务,放入blockqueue
        Task t = PrintHello;
        bq.Enqueue(t);
        //std::cout << "Productor product data is : " << t.DebugToString() << std::endl;
    }
}
void StartComm(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq, func_t<blockqueue_t> func)
{
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads->emplace_back(func, bq, name);
        //(*threads)[threads->size()-1].Start();
        threads->back().Start();
    }
}
void StartConsumer(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq)
{
    StartComm(threads, num, bq, Consumer);
}

void StartProductor(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq)
{
    StartComm(threads, num, bq, Productor);
}
void WaitAllThread(std::vector<Thread<blockqueue_t>> threads)
{
    for(auto thread : threads)
    {
        thread.Join();
    }
}
int main()
{
    blockqueue_t *bq = new blockqueue_t(5);
    std::vector<Thread<blockqueue_t>> threads;
    StartConsumer(&threads, 1, *bq);
    StartProductor(&threads, 1, *bq);
    WaitAllThread(threads);
    return 0;
}

我们所使用的线程不是原生线程库,而是对其进行了分装处理。以上是部分代码需要全部代码的可以到博主代码库中自行拿去。

而多生产多消费只需要在main函数中将参数从1改为多就可以了,代码不需要任何修改,因为其进入函数都需要竞争一把锁,是串行的。 


以上就是本次全部内容,感谢大家观看!!!

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

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

相关文章

Linux——线程互斥与同步

一、线程互斥 1.1 线程间互斥的概念 在学习管道的时候&#xff0c;管道是自带同步与互斥的。而在线程中&#xff0c;当多个线程没有加锁的情况下同时访问临界资源时会发生混乱。在举例之前&#xff0c;先了解几个概念。 临界资源&#xff1a;多个线程执行流共享的资源叫做临…

软甲测试定义和分类

软件测试定义 使用人工和自动手段来运行或测试某个系统的过程&#xff0c;其目的在于检验他是否满足规定的需求或弄清预期结果与实际结果之间的差别 软件测试目的 为了发现程序存在的代码或业务逻辑错误 – 第一优先级发现错误为了检验产品是否符合用户需求 – 跟用户要求实…

WPF学习(3)- WrapPanel控件(瀑布流布局)+DockPanel控件(停靠布局)

WrapPanel控件&#xff08;瀑布流布局&#xff09; WrapPanel控件表示将其子控件从左到右的顺序排列&#xff0c;如果第一行显示不了&#xff0c;则自动换至第二行&#xff0c;继续显示剩余的子控件。我们来看看它的结构定义&#xff1a; public class WrapPanel : Panel {pub…

【网页设计】基于HTML+CSS上海旅游网站网页作业制作

一、&#x1f468;‍&#x1f393;网站题目 旅游&#xff0c;当地特色&#xff0c;历史文化&#xff0c;特色小吃等网站的设计与制作。 二、✍️网站描述 &#x1f468;‍&#x1f393;静态网站的编写主要是用HTML DIVCSS 等来完成页面的排版设计&#x1f469;‍&#x1f39…

CSP初赛知识点讲解(一)

CSP初赛知识点讲解&#xff08;一&#xff09; 信息学竞赛哈夫曼树 哈夫曼编码冯.诺依曼理论计算机奖项例题训练&#xff08;一&#xff09;操作系统例题训练&#xff08;二&#xff09;计算机语言例题训练&#xff08;三&#xff09; 信息学竞赛 全国青少年计算机程序设计竞赛…

VINS-Fusion 多传感器全局位姿估计的一种通用优化框架

摘要 对于自动导航的机器人来说,精确的状态估计是基本问题。为了实现局部精确和全局无漂移的位姿估计,通常将具有互补属性的多个传感器进行融合。在一个小的区域内,局部传感器,如相机、IMU、Lidar等,提供了精确的位姿,而在一个大场景环境下,全局传感器,如gps、magneto…

java 变量及其常量

变量 数据类型关键字内存占用取值范围字节型byte1个字节-128 至 127 定义byte变量时超出范围,废了短整型short2个字节-32768 至 32767整型int&#xff08;默认&#xff09;4个字节-231 至 231-1 正负21个亿-2147483648——2147483647长整型long8个字节-263 至 263-1 19位数字-9…

【51单片机DS1302时钟芯片读取数码管显示打造小成本高品质】2022-12-23

缘由https://ask.csdn.net/questions/7867303 /*写回复缘由https://ask.csdn.net/questions/7867303*/ #include "reg52.h" sbit RSTP3^5;//DS1302允许(读/写)当RST为高电平时&#xff0c;所有的数据传送被初始化&#xff0c;允许对DS1302进行操作。如果在传送过程中…

不用PS也能抠图?点哪抠哪,简直是职场人的最强助手

抠图你还在用 PS 一点点抠吗&#xff1f; 不仅费时费力&#xff0c;还常常达不到理想效果&#xff0c;真的太让人崩溃了 但别担心&#xff0c;我找到了一个超棒的工具——千鹿设计助手的AI智能抠图插件。它就像你的私人设计小助手&#xff0c;能快速帮你把想要的元素抠出来&…

Mendix 创客访谈录|Mendix 如何化解工业企业数字化转型的复杂性

本期创客 田月萍 西门子 Advanta研发部门 大家好&#xff0c;我是田月萍&#xff0c;来自西门子Advanta的研发部门&#xff0c;专注于工业数字化转型。在我的职业生涯中&#xff0c;参与了多个关键项目的开发&#xff0c;涵盖了制造执行系统&#xff08;MES&#xff09;的实施&…

ECMA6Script学习笔记(六)

【摘要】 本文是对自己学习ES6的学习笔记回顾,后面是概要:文章深入探讨了ES6模块化处理&#xff0c;强调模块化在提高代码可维护性、可复用性和可扩展性方面的重要性。介绍了ES6模块化的三种导出方式&#xff1a;分别导出、统一导出和默认导出&#xff0c;并通过具体的代码示例…

3.特征工程-特征抽取、特征预处理、特征降维

文章目录 环境配置&#xff08;必看&#xff09;头文件引用1.数据集: sklearn代码运行结果 2.字典特征抽取: DictVectorizer代码运行结果稀疏矩阵 3.文本特征抽取(英文文本): CountVectorizer()代码运行结果 4.中文文本分词(中文文本特征抽取使用)代码运行结果 5.中文文本特征抽…

一款功能强大且免费的Windows系统优化工具

TweakPower是一款功能强大的Windows系统优化工具&#xff0c;旨在帮助用户提升电脑性能、清理垃圾文件、备份数据以及修复系统问题。该软件提供了多种实用功能&#xff0c;包括内存管理、垃圾清理、数据备份、数据擦除、硬盘维护和性能调度调整等。 TweakPower的主要界面或仪表…

如意玲珑支持发行版再添新成员,openEuler安装使用如意玲珑操作指南

查看原文 如意玲珑&#xff08;Linyaps&#xff09;项目已与开放原子开源基金会完成捐赠协议签署&#xff0c;目前如意玲珑已成为基金会的正式孵化期项目。 如意玲珑是开源软件包格式&#xff0c;用于替代 deb、rpm等包管理工具&#xff0c;实现应用包管理、分发、容器、集成开…

数据产品价值评估体系搭建

00前言 随着数据在企业的重要性越来越高&#xff0c;数据赋予的价值和意义在企业内部也深入人心&#xff0c;不仅纳入到了企业战略中去&#xff0c;在日常的工作中&#xff0c;各个业务部门也会不断的提出五花八门的数据需求&#xff08;数据分析、数据治理、数据应用等等&…

qt-01安装

qt5.15安装 版本链接5.15Qt5.15.2镜像QTCreater Launching Debugger 错误 版本链接5.15 https://download.qt.io/archive/online_installers/4.5/ Qt5.15.2镜像 https://mirrors.tuna.tsinghua.edu.cn/qt/online/qtsdkrepository/windows_x86/desktop/qt5_5152/ https://m…

插入数据优化 ---大批量数据插入建议使用load

一.insert优化 1.批量插入 2.手动提交事务 3.主键顺序插入 二.大批量插入数据 如果一次性需要插入大批量数据,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令进行插入。操作如下 1.客户端连接服务端时,加入参数 --local-infine mysql --local-infine…

AllReduce通信库;Reduce+LayerNorm+Broadcast 算子;LayerNorm(层归一化)和Broadcast(广播)操作;

目录 AllReduce通信库 一、定义与作用 二、常见AllReduce通信库 三、AllReduce通信算法 四、总结 Reduce+LayerNorm+Broadcast 算子 1. Reduce 算子 2. LayerNorm 算子 3. Broadcast 算子 组合作用 LayerNorm(层归一化)和Broadcast(广播)操作 提出的创新方案解析 优点与潜在…

私有化部署 Dify+Ollama并使用qwen2快速搭建 AI 应用

私有化部署 DifyOllama并使用qwen2快速搭建 AI 应用 Dify 是一款开源的大语言模型(LLM) 应用开发平台。它融合了后端即服务&#xff08;Backend as Service&#xff09;和 LLMOps 的理念&#xff0c;使开发者可以快速搭建生产级的生成式 AI 应用。即使你是非技术人员&#xff…

5.8软件工程基础知识-项目管理

项目管理 范围管理产品范围和项目范围管理过程WBS练习题 进度管理基本原则过程活动资源估算 软件规模估算方法进度安排关键路径法练习题 成本管理过程成本的类型练习题 软件配置管理配置项配置基线配置数据库练习题 质量管理过程质量模型软件评审软件容错技术练习题 风险管理宏…