Linux下的多线程编程:原理、工具及应用(3)

                                               🎬慕斯主页:修仙—别有洞天

                                              ♈️今日夜电波:Flower of Life—陽花

                                                                0:34━━━━━━️💟──────── 4:46
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


目录

条件变量再理解

pthread_cond_t

PTHREAD_COND_INITIALIZER

pthread_cond_init()

pthread_cond_destroy()

pthread_cond_wait()

pthread_cond_signal()

pthread_cond_broadcast()

示例代码

生产者消费者模型再理解

BlockingQueue 概念

BlockingQueue 的实现

示例代码

进一步封装


        从前面的理解中,我们对于死锁和条件变量有了一定程度的了解。对此,下面继续对于死锁和条件变量的共同作用加深理解。

条件变量再理解

pthread_cond_t

    pthread_cond_t是POSIX线程库中用于线程同步和通信的一种条件变量类型。它允许一个或多个线程等待直到其他线程通过信号通知特定条件已满足,从而实现线程间的协调工作。以下是关于pthread_cond_t的详细说明:

  1. 初始化:在开始使用pthread_cond_t之前,需要对其进行初始化。可以通过静态或动态的方式初始化条件变量。静态初始化通常在声明时直接赋予PTHREAD_COND_INITIALIZER值。如果选择动态初始化,则需要调用pthread_cond_init函数。
  2. 等待与唤醒机制:线程在等待某个条件变量时会进入睡眠状态,并释放其持有的互斥锁,这样其他线程可以执行相应的条件改变操作。当条件满足后,其他线程将通过pthread_cond_signalpthread_cond_broadcast函数唤醒等待该条件的线程。被唤醒的线程在返回前通常会再次获得互斥锁,以确保同步访问共享资源。
  3. 配合互斥锁使用:条件变量通常与互斥锁一起使用。线程在等待条件变量之前必须先锁定互斥锁,并在调用pthread_cond_wait之后解锁,以便其他线程可以访问共享资源并修改条件。在从pthread_cond_wait返回之前,线程会重新锁定互斥锁,以继续其工作。
  4. 销毁:当条件变量不再使用时,应调用pthread_cond_destroy函数进行清理,以避免资源泄露。
  5. 注意事项:在使用条件变量时要注意避免竞态条件和死锁,确保在检查条件和调用等待函数之间的操作是原子性的。
  6. 用途举例:条件变量常用于生产者-消费者问题、读写锁实现等多线程同步场景。

PTHREAD_COND_INITIALIZER

    PTHREAD_COND_INITIALIZER是POSIX线程库中用于初始化条件变量的宏。它的作用是将条件变量初始化为一个已定义的状态,以便在后续使用中进行比较和操作。

        具体来说,PTHREAD_COND_INITIALIZER是一个静态初始化器,可以在声明条件变量时直接将其赋值给条件变量。这个宏会将条件变量的内存设置为0,表示该条件变量尚未被初始化。

        以下是使用PTHREAD_COND_INITIALIZER进行条件变量初始化的示例代码:

#include <pthread.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

        在这个例子中,我们声明了一个名为cond的条件变量,并使用PTHREAD_COND_INITIALIZER将其初始化为0。这样,我们就可以在后续的代码中使用cond来进行线程同步和通信的操作了。

        需要注意的是,PTHREAD_COND_INITIALIZER只能用于静态初始化,不能用于动态初始化。如果需要在运行时动态创建条件变量,需要使用pthread_cond_init()函数进行初始化。

pthread_cond_init()

    pthread_cond_init()是POSIX线程库中的一个函数,用于初始化条件变量。条件变量是一种同步机制,允许线程等待某个条件满足后再继续执行。

        该函数的原型如下:

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

参数说明:

  • cond:指向要初始化的条件变量的指针。
  • attr:指向条件变量属性对象的指针,可以设置为NULL表示使用默认属性。

返回值:

  • 成功时返回0;失败时返回错误码。

注意事项:

  • 在使用完条件变量后,需要调用pthread_cond_destroy()函数进行销毁,以释放相关资源。
  • 如果使用了自定义的属性对象,也需要在适当的时候调用pthread_condattr_destroy()函数进行销毁。

pthread_cond_destroy()

    pthread_cond_destroy()是POSIX线程库中的一个函数,用于销毁条件变量

        该函数的原型如下:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • cond:指向要销毁的条件变量的指针。

返回值:

  • 成功时返回0;失败时返回错误码。

注意事项:

  • 在使用完条件变量后,需要调用pthread_cond_destroy()函数进行销毁,以释放相关资源。
  • 如果条件变量正在被其他线程等待,则无法销毁该条件变量,直到所有等待该条件的线程已经返回。

pthread_cond_wait()

    pthread_cond_wait()是POSIX线程库中的一个函数,用于等待条件变量满足

        该函数的原型如下:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

参数说明:

  • cond:指向要等待的条件变量的指针。
  • mutex:指向与条件变量关联的互斥锁的指针。

返回值:

  • 成功时返回0;失败时返回错误码。

注意事项:

  • pthread_cond_wait()函数会自动释放传入的互斥锁,并使当前线程进入阻塞状态,直到其他线程调用pthread_cond_signal()pthread_cond_broadcast()函数唤醒该线程。
  • 在调用pthread_cond_wait()函数之前,必须先加锁互斥锁,否则会导致未定义的行为。

pthread_cond_signal()

    pthread_cond_signal()是POSIX线程库中的一个函数,用于唤醒等待在条件变量上的一个线程。

        该函数的原型如下:

int pthread_cond_signal(pthread_cond_t *cond);

参数说明:

  • cond:指向要操作的条件变量的指针。

返回值:

  • 成功时返回0;失败时返回错误码。

注意事项:

  • pthread_cond_signal()函数只会唤醒等待在条件变量上的一个线程,如果有多个线程在等待,其他线程将继续等待。
  • 如果当前没有线程在等待条件变量,pthread_cond_signal()函数的行为是未定义的。
  • 在多线程编程中,通常需要结合互斥锁和条件变量来实现同步,确保线程安全。

pthread_cond_broadcast()

    pthread_cond_broadcast()是POSIX线程库中的一个函数,用于唤醒等待在条件变量上的所有线程。

        该函数的原型如下:

int pthread_cond_broadcast(pthread_cond_t *cond);

参数说明:

  • cond:指向要操作的条件变量的指针。

返回值:

  • 成功时返回0;失败时返回错误码。

注意事项:

  • pthread_cond_broadcast()函数会唤醒等待在条件变量上的所有线程,而不仅仅是一个线程。如果有多个线程在等待,它们都将被唤醒并继续执行。
  • 如果当前没有线程在等待条件变量,pthread_cond_broadcast()函数的行为是未定义的。
  • 在多线程编程中,通常需要结合互斥锁和条件变量来实现同步,确保线程安全。

示例代码

        使用互斥锁与条件等待来使得代码高效运行,以防某个线程一直占用锁从而占用资源!

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

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;

int tickets=1000;

void *threadRoutine(void *args)
{
    std::string name=static_cast<const char*>(args);

    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(tickets>0)
        {
            std::cout << name<< ", get a ticket: " << tickets-- << std::endl; // 模拟抢票
            usleep(1000);
        }else 
        {
            std::cout << "没有票了," << name << std::endl;
           // 1. 让线程在进行等待的时候,会自动释放锁 
           // 2. 线程被唤醒的时候,是在临界区内唤醒的,当线程被唤醒, 线程在pthread_cond_wait返回的时候,要重新申请并持有锁
           // 3. 当线程被唤醒的时候,重新申请并持有锁本质是也要参与锁的竞争的!!
            pthread_cond_wait(&cond,&mutex);
        }

        pthread_mutex_unlock(&mutex);

    }
}

int main()
{
    //child pthread
    pthread_t t1,t2,t3;
    pthread_create(&t1,nullptr,threadRoutine,(void*)"thread-1");
    pthread_create(&t2,nullptr,threadRoutine,(void*)"thread-2");
    pthread_create(&t3,nullptr,threadRoutine,(void*)"thread-3");

    //main pthread

    while(true)
    {

        sleep(5);
        pthread_mutex_lock(&mutex);
        tickets+=1000;
        pthread_mutex_unlock(&mutex);
        pthread_cond_broadcast(&cond);//给全部发信号
        //pthread_cond_signal(&cond);//给其中一个发信号
    }

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

        return 0;
    
}

生产者消费者模型再理解

BlockingQueue 概念

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

BlockingQueue 的实现

        主要是基于还是基于库中queue来进行包装,push作为生产者的生产操作,而pop作为消费者的消费操作其中的细节:当queue到达我们设定的满队列值时,需要根据条件变量来等待,而发生这个信号在消费者的pop函数。相对的pop函数中如果队列为空了,那么也需要等待push函数中的信号

BlockQueue.hpp

#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>

const int defaultcap = 5;//首先默认队列大小为5

template <class T>
class BlockQueue
{
public:
    BlockQueue(int cap = defaultcap)
        : _capacity(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
    }

    bool IsFull()
    {
        return _q.size() == _capacity;
    }

    bool IsEmpty()
    {
        return _q.size() == 0;
    }

    void Push(const T &in) // 生产者生产
    {
        pthread_mutex_lock(&_mutex);

    //本来使用的是if,但是为了防止pthread_cond_wait伪唤醒从而使用while
        while (IsFull()) // 写出来的代码,具有较强的鲁棒、健壮性
        {
            // 阻塞等待
            pthread_cond_wait(&_p_cond, &_mutex); // 1. 关于pthread_cond_wait在进一步理解!
        }
        _q.push(in);
        // if(_q.size() > _productor_water_line) pthread_cond_signal(&_c_cond);
        pthread_cond_signal(&_c_cond);
        pthread_mutex_unlock(&_mutex);
    }

    void Pop(T *out) // 消费者的
    {
    
        pthread_mutex_lock(&_mutex);
        while (IsEmpty())
        {
            // 阻塞等待
            pthread_cond_wait(&_c_cond, &_mutex);
        }

        *out = _q.front();
        _q.pop();
        //if(_q.size() < _consumer_water_line) pthread_cond_signal(&_p_cond);
        pthread_cond_signal(&_p_cond);
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }

private:
    std::queue<T> _q;
    int _capacity;
    pthread_mutex_t _mutex; // 锁
    pthread_cond_t _p_cond; // 生产者条件
    pthread_cond_t _c_cond; // 消费者的条件

};

示例代码

        需要特别注意其中的sleep,如果在消费者函数或者生产者函数中表示为另外一方先行执行!但是根据上面我们push和pop函数的相互等待条件。如果是消费者先执行,那么他会等待生产者生产,每生产一个就消费一个。而如果生产者先执行,则会在一瞬间生产很多,而后消费者每消费一个,生产者生产一个。

#include "BlockQueue.hpp"
#include <pthread.h>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

void *consumer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);

    while (true)
    {
        int data;

        //sleep(1);
        
        bq->Pop(&data);

        std::cout << "consumer data: " << data << std::endl;
    }

    return nullptr;
}

void *productor(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        int data = rand() % 10;
        std::cout << "productor task: " << data << std::endl;
        bq->Push(data);

        sleep(1);
    }
}

int main()
{
    srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); // 只是为了形成更随机的数据
    BlockQueue<int> *bq = new BlockQueue<int>();
    pthread_t c, p; // 消费者和生产者

    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    
    return 0;
}

进一步封装

        进一步封装,延续第一篇的代码,把锁也一同封装了!让代码更加简洁!

LockGuard.hpp

#pragma once

#include <pthread.h>

// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock):_lock(lock)
    {}
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex()
    {}

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock): _mutex(lock)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.Unlock();
    }
private:
    Mutex _mutex;
};

BlockQueue.hpp

#pragma  once

#include <iostream>
#include <queue>
#include <pthread.h>
#include "LockGuard.hpp"

const int defaultcap = 5; // for test

template<class T>
class BlockQueue
{
public:
    BlockQueue(int cap = defaultcap):_capacity(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
    }
    bool IsFull()
    {
        return _q.size() == _capacity;
    }
    bool IsEmpty()
    {
        return _q.size() == 0;
    }
    void Push(const T &in) // 生产者的
    {
        LockGuard lockguard(&_mutex);
        // pthread_mutex_lock(&_mutex); // 2. lockguard 3. 重新理解生产消费模型(代码+理论) 4. 代码整体改成多生产,多消费
        while(IsFull()) // 写出来的代码,具有较强的鲁棒、健壮性
        {
            // 阻塞等待
            pthread_cond_wait(&_p_cond, &_mutex); // 1. 关于pthread_cond_wait在进一步理解!
        }

        _q.push(in);
        // if(_q.size() > _productor_water_line) pthread_cond_signal(&_c_cond);
        pthread_cond_signal(&_c_cond);
        // pthread_mutex_unlock(&_mutex);
    }
    void Pop(T *out)       // 消费者的
    {
        LockGuard lockguard(&_mutex);
        // pthread_mutex_lock(&_mutex);
        while(IsEmpty())
        {
            // 阻塞等待
            pthread_cond_wait(&_c_cond, &_mutex);
        }

        *out = _q.front();
        _q.pop();
        // if(_q.size() < _consumer_water_line) pthread_cond_signal(&_p_cond);
        pthread_cond_signal(&_p_cond);
        // pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }
private:
    std::queue<T> _q;
    int _capacity; // _q.size() == _capacity, 满了,不能在生产,_q.size() == 0, 空,不能消费了
    pthread_mutex_t _mutex;
    pthread_cond_t _p_cond; // 给生产者的
    pthread_cond_t _c_cond; // 给消费者的


};


                      感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从零到一构建短链接系统(五)

1.修改UserService Service public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService {public UserRespDTO getUserByUsername(String username) {LambdaQueryWrapper<UserDO> queryWrapper Wrappers.lambdaQuery(UserDO.c…

【python】集合

前言 简洁整理&#xff0c;无废话 集合概念 含义&#xff1a;跟数学中的基本一样 形式&#xff1a;{1,a,(1,2)} 性质&#xff1a;不重复性&#xff0c;集合中每个元素不会有重复&#xff1b;集合中必须是不可变元素&#xff0c;不能有列表可以有元组 创建&#xff1a;{}或…

如何引入ElementUI组件库,快速上手Element

前言&#xff1a;在上篇文章中分享了如何快速上手Vue框架项目&#xff0c;本篇文章则介绍的是Element的使用&#xff0c;通过本篇文章的分享&#xff0c;我们就可以将Vue和Element结合使用&#xff0c;快速构建出精美的网页界面 目录 一.Element和ElementUI 二.如何引入Eleme…

算法打卡day19|二叉树篇08|Leetcode 235. 二叉搜索树的最近公共祖先、701.二叉搜索树中的插入操作、450.删除二叉搜索树中的节点

算法题 Leetcode 235. 二叉搜索树的最近公共祖先 题目链接:235. 二叉搜索树的最近公共祖先 大佬视频讲解&#xff1a;二叉搜索树的最近公共祖先视频讲解 个人思路 昨天做过一道二叉树的最近公共祖先&#xff0c;而这道是二叉搜索树&#xff0c;那就要好好利用这个有序的特点…

Luckysheet + Exceljs:H5实现Excel在线编辑、导入、导出及上传服务器的示例代码(完整版demo)

创建xeditor.html <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>Hello World!</title><!-- <link relstylesheet href./luckysheet/plugins/css/pluginsCss.css /><link relstylesheet href./luck…

【嵌入式实践】【芝麻】【硬件篇-3】从0到1给电动车添加指纹锁:光耦+继电器电路设计及讲解

0. 前言 该项目是基于stm32F103和指纹模块做了一个通过指纹锁控制电动车的小工具。支持添加指纹、删除指纹&#xff0c;电动车进入P档等待时计时&#xff0c;计时超过5min则自动锁车&#xff0c;计时过程中按刹车可中断P档状态&#xff0c;同时中断锁车计时。改项目我称之为“芝…

Chapter 13 Techniques of Design-Oriented Analysis: The Feedback Theorem

Chapter 13 Techniques of Design-Oriented Analysis: The Feedback Theorem 从这一章开始讲负反馈Control系统和小信号建模. 13.2 The Feedback Theorem 首先介绍 Middlebrook’s Feedback Theorem 考虑下面负反馈系统 传输函数 Guo/ui G ( s ) u o u i G ∞ T 1 T G…

7.Java整合MongoDB—项目创建

整合MongoDB MongoDB的基本知识有所了解之后&#xff0c;我们开始着手上代码了&#xff0c;进来先来项目创建&#xff0c;如何引入mongodb&#xff0c;以及测试一下能否连接数据库。 1 新建springboot项目 其实只需要spring boot mongodb这个依赖就行&#xff0c;加那么多纯属…

sparksql简介

什么是sparksql sparksql是一个用来处理结构话数据的spark模块&#xff0c;它允许开发者便捷地使用sql语句的方式来处理数据&#xff1b;它是用来处理大规模结构化数据的分布式计算引擎&#xff0c;其他分布式计算引擎比较火的还有hive&#xff0c;map-reduce方式。 sparksql…

基于单片机的智能小车泊车系统设计

摘 要:随着信息技术的进步,汽车逐渐朝着安全、智能方向发展,智能泊车系统的出现不仅能帮助人们更加快速、安全地完成泊车操作,而且适用于狭小空间的泊车操作,降低驾驶员泊车负担,减轻泊车交通事故发生率。文章基于单片机设计自动泊车系统,以单片机为核心来实现信息收集及…

文件系统I/O FATFS RW 源码分析

文件系统I/O FATFS RW 源码分析 0 参考 FatFs 是用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块。FatFs 整个项目都按照 ANSI C (C89) 编写。与存储器 I/O 解耦良好&#xff0c;便于移植到 8051、PIC、AVR、ARM、Z80、RX 等小型微控制器中。 下面是关于 FAT 文件系统格式…

【Exception系列】SocketTimeoutException

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

ADO .Net操作SQL Server数据库

//ADO.NET是.NET Framework提供的数据访问服务的类库&#xff0c;应用程序可以使用ADO.NET连接到这些数据源&#xff0c;并检索、处理和更新数据 //常用的数据源包括四种&#xff1a;&#xff08;1&#xff09;Microsoft SQL Server数据源&#xff1a;使用System.Data.SqlClien…

STM32---SG90舵机控制(HAL库,含源码)

写在前面&#xff1a;在嵌入式的项目中&#xff0c;舵机是一个十分常见的元器件模块&#xff0c;其主要的功能是实现机械转动&#xff0c;实质上舵机是一个伺服的驱动器&#xff0c;适用于那些需要角度不断变化并可以保持的控制系统。例如在机器人的电控制器系统中&#xff0c;…

Java8中Stream流API最佳实践Lambda表达式使用示例

文章目录 一、创建流二、中间操作和收集操作筛选 filter去重distinct截取跳过映射合并多个流是否匹配任一元素&#xff1a;anyMatch是否匹配所有元素&#xff1a;allMatch是否未匹配所有元素&#xff1a;noneMatch获取任一元素findAny获取第一个元素findFirst归约数值流的使用中…

在线BLOG网|基于springboot框架+ Mysql+Java+JSP技术的在线BLOG网设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 目录 前台功能效果图 管理员功能登录前台功能效果图 系统功能设计 数据库E-R图设计 lunwen参考 摘要 研究…

C语言学习过程总结(18)——指针(6)

一、数组指针变量 在上一节中我们提到了&#xff0c;指针数组的存放指针的数组&#xff0c;那数组指针变量是什么呢&#xff1f; 显而易见&#xff0c;数组指针变量是指针 同样类比整型指针变量和字符指针变量里面分别存放的是整型变量地址和字符变量地址&#xff0c;我们可以…

每周AI新闻(2024年第11周)Meta公布Llama 3集群细节 | Sora将于年内推出 | 全球首个AI软件工程师发布

这里是陌小北&#xff0c;一个正在研究硅基生命的碳基生命。正在努力成为写代码的里面背诗最多的&#xff0c;背诗的里面最会写段子的&#xff0c;写段子的里面代码写得最好的…厨子。 每周日解读每周AI大事件。 欢迎关注同名公众号【陌北有棵树】&#xff0c;关注AI最新技术…

第二十四天-数据可视化Matplotlib

目录 1.介绍 2.使用 1. 安装&#xff1a; 2.创建简单图表 3.图表类型 1.一共分为7类 2.变化 1.折线图 3.分布 ​编辑 1.直方图 2.箱型图 4.关联 1. 散点图&#xff1a; 2.热力图&#xff1a; 5.组成 1.饼图 2.条形图 6.分组 1.簇型散点图 2.分组条形图 3.分…

【ollama】linux、window系统更改模型存放位置,全网首发2024!

首先是window系统 so easy 直接进入系统环境变量添加就行 其次是linux系统 全靠自己试出来的,去Ollama官网找半天文档不知道在哪,而且linux也没有说:【 https://github.com/ollama/ollama/blob/main/docs/README.md https://github.com/ollama/ollama/blob/main/docs/li…