Linux——线程互斥与同步

news2024/9/20 20:39:16

一、线程互斥

1.1 线程间互斥的概念

在学习管道的时候,管道是自带同步与互斥的。而在线程中,当多个线程没有加锁的情况下同时访问临界资源时会发生混乱。在举例之前,先了解几个概念。

  • 临界资源:多个线程执行流共享的资源叫做临界资源
  • 临界区:每个线程内部访问临界资源的代码叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完

1.2 互斥量

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

我们可以通过一个买票的例子,来看这块问题。

int ticket = 2000;
void *STicket(void *asg)
{
    while (1)
    {
        if (ticket > 0)
        {
            usleep(100);
            printf("%s sang ticket:%d \n", (char *)asg, ticket--);
        }
        else
        {
            break;
        }
    }
    return NULL;
}

int main()
{
    pthread_t t[4];
    int i;
    for (i = 0; i < 4; i++)
    {
        char *p = (char *)malloc(sizeof(char) * 64);
        sprintf(p, "pthread t%d", i);
        pthread_create(&t[i], NULL, STicket, (void *)p);
    }
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);
    pthread_join(t[2], NULL);
    pthread_join(t[3], NULL);

    return 0;
}

我们在运行结果中可以看到,票的数量本不可能出现负数的,但是在结果中出现了,那么这就是一个问题。
多个线程并发的访问同一块临界资源,我们用t1,t2,t3,t4,来表示四个线程。一开始票的数量有1000张。

《出现问题1》当t1首先访问到票时,判断票还有剩余,于是拿走一张票,票还剩999张。但是这些线程是并发执行的,有可能多个线程同时拿到票,且通过对票进行减减操作,那么这个票是重复了。

《出现问题2》当t3拿到票的时候,刚准备对票进行减减,时间片就到了,线程退出,那么在t3这个线程内把读取到的票的数量保存起来,当t3这个线程有运行时,先恢复上下文数据,然后对山下文数据中保存票的数量进行减减,当t3这个线程完成了操作后,把剩余票的数量进行更新,那么在t3没有运行前,票已经抢完了,但是t3它不知道,然后又把票的数量进行更新了,票又回来了,这个时候又出错了。出现负数的情况就是这样。

在我们判断票是否有剩余的时候,和对票减减的时候,并不是具有原子性的,因为这个时候,其他线程也在进行抢票,可能拿到重复的票。我们可以通过汇编来验证是否具有原子性。

int main()
{
    int a = 5;
    a--;
    return 0;
}

--操作并不是原子性,而是对应了三条汇编:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

想要解决上面的问题,需要做到三点:

  • 代码必须要有互斥行为:当一个线程的代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
  • 果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

而以上的三点本质就是加一把锁,在Linux上提供的这把锁叫做互斥量

先要理解这个锁。当多个线程同时要执行临界区的代码,那么谁先申请到这把锁,谁就执行,其他的线程就开始进行等待,等待这把锁被释放,然后申请这把锁。

1.3 互斥量的接口

初始化互斥量有两中方法:

  • 方法1,静态分配
    pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER
  • 方法2,动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量
attr:设置属性,一般设置NULL,用默认设置
返回值:成功返回0,错误返回错误号

功能:销毁互斥量
原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex:要销毁的互斥量
返回值:成功返回0,错误返回错误号

【注意】

  1. 使用PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
  2. 不要销毁一个已经加锁的互斥量
  3. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

功能:加锁
原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:mutex:要加锁的互斥量
返回值:成功返回0,错误返回错误号

功能:解锁
原型:int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:mutex:要解锁的互斥量
返回值:成功返回0,错误返回错误号

调用pthread_mutex_lock会遇到的情况

  • 互斥量处于没锁的状态,该函数将互斥量锁定,同时返回成功。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

现在我们对之前的买票系统进行改进

int ticket = 2000;
pthread_mutex_t lock;

void *STicket(void *asg)
{
    while (1)
    {
        // 在执行临界区的代码前,先申请锁(加锁)
        pthread_mutex_lock(&lock);
        if (ticket > 0)
        {
            usleep(100);
            printf("%s sang ticket:%d \n", (char *)asg, ticket--);
        }
        else
        {
            // 当没有票的时候,也释放锁(解锁)
            pthread_mutex_unlock(&lock);
            break;
        }
        // 访问完了临界资源时,释放锁(解锁)
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main()
{
    // 动态的初始化锁
    pthread_mutex_init(&lock, NULL);
    pthread_t t[4];
    int i;
    for (i = 0; i < 4; i++)
    {
        char *p = (char *)malloc(sizeof(char) * 64);
        sprintf(p, "pthread t%d", i);
        pthread_create(&t[i], NULL, STicket, (void *)p);
    }
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);
    pthread_join(t[2], NULL);
    pthread_join(t[3], NULL);
    // 最后销毁锁
    pthread_mutex_destroy(&lock);
    return 0;
}

1、一个线程拿到了锁,会不会被其他线程切换?
答:会被切换,当这个拿到锁的线程切换到了其他线程,其他线程依然没有锁,依然要等待,然而当拿到锁的线程又开始运行时,首先要先恢复上下文数据,这个线程依然是拿到锁的状态(这个线程是拿着锁被切走的),可以继续执行临界区的代码。

2、申请锁的过程是不是原子性的?
答:申请锁的原子性的,要么没有申请到锁,要么锁已经释放了,可以申请锁。

3、锁本身就是临界资源,那么谁来保护锁?
答:锁是来保护临界资源的,但是锁也是临界资源的呀。但是锁本身就具有原子性,申请锁的过程必须是原子性的。

1.4 可重入VS线程安全

概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见的不可重入情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见的可重入情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全的联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全的区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

二、线程同步

在上面的买票系统中,如果线程1的优先级非常高,那么会不会出现票都被线程A给抢完了。线程1申请锁后抢票完成,释放锁,释放完后线程A又申请到锁,如此往复,直到票买完了。按理说这样没有错,各凭本事买票嘛,但这样没有高效的让多个执行流使用这个资源,那么多执行流就没有意义了。线程同步就是来解决这个问题的。要申请锁的所有线程依次排队申请,使用完锁的线程去队尾排队,这样就防止了一个优先级高的线程抢完所以资源。

2.1 同步概念和竞态条件

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

2.2 条件变量

条件变量我们可以理解为:条件变量使我们可以睡眠等待某种条件的出现。

饥饿问题:多个执行流,在保证互斥地访问同一块资源时,该资源一直被同一个执行流访问,就会导致其他执行流形成饥饿,这种现象就做饥饿问题。

初始化条件变量

功能:初始化条件变量
原型: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);
参数:cond:要销毁的条件变量

等待条件满足

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

唤醒等待

功能:唤醒等待队列中队头线程
原型:int pthread_cond_signal(pthread_cond_t *cond);
参数:cond:在这个条件变量上唤醒

功能:唤醒所以线程
原型	:int pthread_cond_broadcast(pthread_cond_t *cond);
参数:cond:在这个条件变量上唤醒

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

pthread_cond_t

我们设置条件变量的类型是pthread_cond_t。

struct pthread_cond_t
{
	int flag;//0表示没有钥匙,1表示有钥匙
	task_struct *queue;//等待队列
}

简单的案例:

// 定义锁
pthread_mutex_t lock;
// 定义条件变量
pthread_cond_t cond;
// 设置票的数量为6张
int ticket = 6;

void *RunRoute(void *arg)
{
    // 分离自己,线程退出自动释放
    pthread_detach(pthread_self());
    while (true)
    {
        // 申请锁
        pthread_mutex_lock(&lock);
        // 等待条件变量
        pthread_cond_wait(&cond, &lock);
        if (ticket > 0)
        {

            std::cout << (char *)arg << "抢到了" << ticket << "号票" << std::endl;
            ticket--;
        }
        else
        {
            std::cout << "票卖完了" << std::endl;
            // 释放锁
            pthread_mutex_unlock(&lock);
            break;
        }
        // 释放锁
        pthread_mutex_unlock(&lock);
    }
}
int main()
{
    // 初始化锁
    pthread_mutex_init(&lock, nullptr);
    // 初始化条件变量
    pthread_cond_init(&cond, nullptr);
    pthread_t t1, t2, t3;
    // 创建线程
    pthread_create(&t1, NULL, RunRoute, (void *)"thread t1");
    pthread_create(&t2, NULL, RunRoute, (void *)"thread t2");
    pthread_create(&t3, NULL, RunRoute, (void *)"thread t3");
    // 主线程控制其他线程
    while (true)
    {
        // 通过回车来唤醒等待的线程
        getchar();
        // 唤醒等待队列中队头的线程
        pthread_cond_signal(&cond);
    }
    return 0;
}

看运行结果,t1、t2、t3线程轮流抢票。

pthread_cond_wait为什么需要互斥量

看上面的代码。在pthread_cond_wait函数最后一个参数是互斥量。
看代码,当一个线程申请到锁时,就开始执行pthread_cond_wait进行等待,在等待的过程中,该线程的锁会被释放,等线程被唤醒的时候,该线程的锁又会回到手上。

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

条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&lock);
while (条件为假)
	pthread_cond_wait(&cond, &lock);
//修改条件
pthread_mutex_unlock(&mutex);
  • 给条件发送信号代码
pthread_mutex_lock(&lock);
//设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);

三、死锁

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

3.1 死锁的四个必要条件

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

自己也可以让自己死锁。

pthread_mutex_t lock;
int count = 10;
void *Rounit(void *asg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        pthread_mutex_lock(&lock);
        if (count > 0)
        {
            count--;
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
        pthread_mutex_unlock(&lock);
    }
}

int main()
{
    pthread_t t1;
    pthread_mutex_init(&lock, NULL);
    pthread_create(&t1, NULL, Rounit, NULL);
    pthread_join(t1, NULL);
    pthread_mutex_destroy(&lock);
    return 0;
}

这会行成死锁,因为线程申请了两次锁,第二次申请锁的时候锁已经在你手上了,但因为你还要等待锁被释放,所以一直等待,形成了死锁。

3.2 避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

3.3 避免死锁的算法

  • 死锁检测算法
  • 银行家算法

四、POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

使用信号量要包semaphore.h头文件

功能:初始化信号量
原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:sem:信号量
     pshared:0表示线程间共享,非零表示进程间共享
     value:信号量初始值

功能:销毁信号量
原型:int sem_destroy(sem_t *sem);
参数:sem:信号量

功能:等待信号量,会将信号量的值减1(P操作)
原型:int sem_wait(sem_t *sem);
参数:sem:信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1(V操作)
原型:int sem_post(sem_t *sem);
参数:sem:信号量

写个二元信号量来熟悉这些函数。二元信号量的使用类似锁。
二元信号量: value为1,当 value经过P操作变成0时,线程要等待 value又变成1。

class Sem
{
public:
    Sem()
    {
        sem_init(&sem, 0, 1);
    }
    ~Sem()
    {
        sem_destroy(&sem);
    }
    void P()
    {
        sem_wait(&sem);
    }
    void V()
    {
        sem_post(&sem);
    }

private:
    sem_t sem;
};
Sem sem;
int tickets = 10;
void *GetTickets(void *asg)
{
    while (true)
    {
        sleep(1);
        sem.P();
        if (tickets > 0)
        {
            std::cout << (char *)asg << "抢到了" << tickets << "号票" << std::endl;
            tickets--;
            sem.V();
        }
        else
        {
            break;
        }
    }
}

int main()
{
    pthread_t t1, t2;
    pthread_create(&t1, nullptr, GetTickets, (void *)"thread 1");
    pthread_create(&t2, nullptr, GetTickets, (void *)"thread 2");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    return 0;
}

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

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

相关文章

软甲测试定义和分类

软件测试定义 使用人工和自动手段来运行或测试某个系统的过程&#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练习题 进度管理基本原则过程活动资源估算 软件规模估算方法进度安排关键路径法练习题 成本管理过程成本的类型练习题 软件配置管理配置项配置基线配置数据库练习题 质量管理过程质量模型软件评审软件容错技术练习题 风险管理宏…

java基础概念10-数组

一、什么是数组 二、数组的定义和初始化 2-1、数组的定义 2-2、数组的初始化 初始化&#xff1a;就是在内存中&#xff0c;为数组容器开辟空间&#xff0c;并将数据存入容器中的过程。 1、静态初始化 【注意】&#xff1a; 数字一旦初始化后&#xff0c;数组的长度不可改变&a…