Linux线程实用场景

news2024/11/25 6:38:35

文章目录

  • 前言
  • 生产者消费者模型
    • 1.基于阻塞队列
      • 特点
      • 实现
      • 使用
    • 2.基于环形队列和信号量
      • 实现
      • 使用
  • 读者写者模型
    • 实现思想
  • 线程池
    • 实现


前言

    生产者消费者模型和读者写者模型这些模型是用于在线程间协调和管理资源访问的模式, 我们在之前已经理解了线程的概念以及同步与互斥, 现在我们来学习几个常见的线程实用场景

生产者消费者模型

    这是一个经典的线程同步问题,通常用来解决多线程间的缓冲区共享问题。生产者线程负责生成数据并将其放入缓冲区,而消费者线程从缓冲区中取出数据进行处理。为了防止缓冲区的竞争访问,通常使用信号量、互斥锁或条件变量等同步机制来管理线程之间的协调
生产者消费者模型

1.基于阻塞队列

    阻塞队列是一种线程安全的数据结构,广泛用于多线程编程中

特点

线程安全
阻塞队列通常是线程安全的,可以在多个线程间安全地进行操作。无论是多个生产者线程还是多个消费者线程,都可以并发地进行数据的放入或取出操作,而不会引发竞态条件。
阻塞操作
阻塞的放入操作:如果队列已满,当生产者线程试图将数据放入队列时,它会被阻塞,直到队列有空余空间。
阻塞的取出操作:如果队列为空,当消费者线程试图从队列中取出数据时,它会被阻塞,直到队列中有数据可取。

实现

完整代码(GitHub)
整体框架

template<class T>//资源类型
class blockqueue
{
public:
    blockqueue(int cap=5):_cap(cap)//初始化,设定阻塞队列最大资源数
    {}
    void push(const T& data)//生产者向阻塞队列生产资源
    {}
    T pop()//消费资源资源
    {}
    bool empty()//判断阻塞队列是否为空
    {}
    bool full()//判断阻塞队列是否为满
    {}
    ~blockqueue()
    {}
private:
    std::queue<T> _q;//现成的队列,直接拿来用
    pthread_mutex_t _mutex;//互斥锁
    pthread_cond_t _con_cond;//消费者等待
    pthread_cond_t _pro_cond;//生产者等待
    int _con_wait_num=0;//等待的消费者数量
    int _pro_wait_num=0;//等待的生产者的数量
    int _cap;//队列最大资源数
};

初始化

blockqueue(int cap=5):_cap(cap)
{
    pthread_mutex_init(&_mutex,nullptr);
    pthread_cond_init(&_con_cond,nullptr);
    pthread_cond_init(&_pro_cond,nullptr);
}

生产

void push(const T& data)
{
    pthread_mutex_lock(&_mutex);//生产和消费都在访问共享资源,加锁
    while(full())//队列满, 生产者线程就要去条件变量下等
    {
        _con_wait_num++;
        pthread_cond_wait(&_pro_cond,&_mutex);
        _con_wait_num--;
    }
    _q.push(data);//生产
    if(_pro_wait_num>0) pthread_cond_broadcast(&_con_cond);//已经生产资源了,如果但消费者还在等待,叫醒它们
    pthread_mutex_unlock(&_mutex);
}

注意: while(full())这里一定是while循环, 而不能单独判断一次, pthread_cond_broadcast会一次叫醒所有的等待的线程, 如果只是判断一次, 被叫醒的线程在队列满的时候竞争到锁就直接去生产资源了
消费

T pop()
{
    pthread_mutex_lock(&_mutex);
    while(empty())//队列为空, 消费者线程就要去等待
    {
        _pro_wait_num++;
        pthread_cond_wait(&_con_cond,&_mutex);
        _pro_wait_num--;
    }
    T temp=_q.front();
    _q.pop();
    if(_con_wait_num>0) pthread_cond_broadcast(&_pro_cond);//消费了,代表有空间了,叫醒等待的生产者线程
    pthread_mutex_unlock(&_mutex);
    return temp;
}

其他操作

bool empty()
{
    return _q.size()==0;
}
bool full()
{
    return _q.size()==_cap;
}
~blockqueue()
{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_con_cond);
    pthread_cond_destroy(&_pro_cond);
}

使用

    可以在多线程环境下使用此阻塞队列,例如多个生产者线程不断push数据到队列中,多个消费者线程不断从队列中pop数据。由于队列操作被锁定,多个线程可以安全地共享这个队列, 当时在构建阻塞队列的时候用了模板, 阻塞队列的资源可以是一个个任务
    多个线程不断生产任务到队列中,多个线程不断从队列中拿到任务并执行, 下面是一个示例
简单封装一下pthread库的线程

template<class T>
class Thread
{
public:
    Thread()
    {}
    Thread(const std::function<void(T)>& func,T args,const std::string& name):_thread_name(name),_func(func),_args(args)
    {}
    bool start()
    {
        int n=pthread_create(&_tid,nullptr,_thread,(void*)this);
        if(n!=0) return false;
        return true;
    }
    void join()
    {
        pthread_join(_tid,nullptr);
    }
    std::string get_thread_name()
    {
        return _thread_name;
    }
    pthread_t gettid()
    {
        return _tid;
    }
private:
    std::string _thread_name;
    T _args;
    pthread_t _tid;
    std::function<void(T)> _func;
    static void* _thread(void*args)
    {
        Thread<T>* p=(Thread<T>*)args;
        p->_func(p->_args);
        return nullptr;
    }
};

这个封装就不做解释了
简单的一个调用阻塞队列的模型

class cp_thread {
public:
    cp_thread(int consumer_num = 1, int productor_num = 1)
        : _consumer_num(consumer_num), _productor_num(productor_num) {
        _con_tid.resize(consumer_num);
        _pro_tid.resize(productor_num);
    }

    // 启动消费者和生产者线程
    bool start() {
        // 启动消费者线程
        for (int i = 0; i < _consumer_num; i++) {
            // 创建线程对象并绑定消费者线程函数
            _con_tid[i] = Thread<cp_thread*>([=](cp_thread* p) { c_thread(p, i); }, this, "con_thread_" + std::to_string(i));
            _con_tid[i].start();  // 启动线程
        }

        // 启动生产者线程
        for (int i = 0; i < _productor_num; i++) {
            // 创建线程对象并绑定生产者线程函数
            _pro_tid[i] = Thread<cp_thread*>([=](cp_thread* p) { p_thread(p, i); }, this, "pro_thread_" + std::to_string(i));
            _pro_tid[i].start();  // 启动线程
        }

        return true;
    }

    ~cp_thread() {
        // 等待所有消费者线程结束
        for (auto& i : _con_tid) {
            i.join();
        }
        // 等待所有生产者线程结束
        for (auto& i : _pro_tid) {
            i.join();
        }
    }

private:
    int _consumer_num; // 消费者线程数量
    int _productor_num; // 生产者线程数量
    blockqueue<task> _q; // 阻塞队列,存储任务
    std::vector<Thread<cp_thread*>> _con_tid; // 消费者线程 ID 向量
    std::vector<Thread<cp_thread*>> _pro_tid; // 生产者线程 ID 向量
    static int temp; // 静态变量,用于任务编号

    // 消费者线程函数,不断向队列中添加任务
    static void c_thread(cp_thread* p, int i) {
        while (true) {
            // 模拟创建任务并推入队列,任务内容是输出任务编号
            p->_q.push(task([=]() { std::cout << "task---" << std::to_string(temp) << " running" << std::endl; temp++; }));
            // std::cout << p->_con_tid[i].get_thread_name() << ":" << "consumer......" << std::endl;
        }
    }

    // 生产者线程函数,不断从队列中取出任务并执行
    static void p_thread(cp_thread* p, int i) {
        while (true) {
            sleep(3); // 模拟任务处理的时间延迟
            auto num = p->_q.pop(); // 从队列中取出任务
            num.task_start(); // 执行任务
            // std::cout << p->_pro_tid[i].get_thread_name() << ":" << "productor......" << std::endl;
        }
    }
};

int cp_thread::temp = 1; // 静态变量初始化,任务编号从1开始

2.基于环形队列和信号量

    信号量是一种资源预定机制, 只有申请到了信号量, 就一定能拿到资源, 环形队列是一种特殊的队列数据结构,通常用于处理固定大小的缓冲区。与普通的线性队列不同,环形队列在达到队列的边界时不会停止插入操作,而是会从队列的开始位置继续插入, 直到满, 我们可以通过这个实现生产者消费者模型
    生产可以看作申请空间资源, 消费可以看作申请数据资源, 使用POSIX信号量实现
环形队列

实现

完整代码(GitHub)
环形队列

template<class T>
class ring_queue
{
public:
    ring_queue(int cap=5): _cap(cap), _v(cap) 
    {
        pthread_mutex_init(&_consumer_mutex, nullptr);
        pthread_mutex_init(&_productor_mutex, nullptr);
        
        sem_init(&_room, 0, cap);//空间资源, 初始化为队列大小
        
        sem_init(&_data, 0, 0);//数据资源, 初始化0
    }

    void push(T& data) 
    {
        sem_wait(&_room);//预定空间资源
        //只要预定到了资源, 就一定有一个资源
        pthread_mutex_lock(&_consumer_mutex);
        
        _v[_consumer_index++] = data;
        _consumer_index %= _cap;

        // 模拟数据变化, 假设生产一个整形
        data++;

        pthread_mutex_unlock(&_consumer_mutex);
        
        sem_post(&_data);//已经生产了一个数据, 数据资源增多
    }

    T pop() 
    {
        sem_wait(&_data);//预定数据资源
        //只要预定到了资源, 就一定有一个资源
        pthread_mutex_lock(&_productor_mutex);
        
        const T& temp = _v[_productor_index++];
        _productor_index %= _cap;

        pthread_mutex_unlock(&_productor_mutex);
        
        sem_post(&_room);//已经消费了一个数据资源, 空间资源增多
        
        return temp; 
    }

    ~ring_queue() 
    {
        pthread_mutex_destroy(&_consumer_mutex);
        pthread_mutex_destroy(&_productor_mutex);
        sem_destroy(&_room);
        sem_destroy(&_data);
    }

private:
    std::vector<T> _v; // 现成的数组, 直接拿来用
    pthread_mutex_t _consumer_mutex; // 保护消费者操作的互斥锁
    pthread_mutex_t _productor_mutex; // 保护生产者操作的互斥锁
    sem_t _room; // 表示环形队列空间的信号量
    sem_t _data; // 表示数据资源的信号量
    int _consumer_index = 0; // 消费者索引
    int _productor_index = 0; // 生产者索引
    int _cap; // 队列的容量
};

    考虑极端情况, 当环形队列没有数据的时候, 只有生产者能够申请信号量成功, 同理, 数据满的时候, 只有消费者能够申请信号量成功, 所以生产者和消费者的下标不可能对同一位置进行访问, 所以我们不用像阻塞队列那样只要访问共享资源就加锁, 只用给生产者和消费者单独配锁即可, 这样生产者和消费者能够真正并发运行

使用

    使用和阻塞队列的使用类似, 引入任务队列即可, 把使用上文阻塞队列的代码拿过来改改就能用

读者写者模型

    读者写者模型是一种经典的并发控制问题,主要用来解决在多线程环境下对共享数据的读写操作的同步问题。这个模型关注的是如何在多个读者(读取共享数据的线程)和写者(修改共享数据的线程)之间进行调度,以保证数据的一致性,同时最大限度地提高系统的并发性

关系
读者和写者 : 互斥&&同步, 写者和读者不能同时访问共享资源
写者和写者 : 互斥&&同步, 写者与写者不能同时访问共享资源
读者和读者 : 没有关系

实现思想

    读者优先或者写者优先都可能出现线程饥饿问题, 所以采取线程请求的顺序来处理, pthread库也有读写锁

线程池

    线程池是一种设计模式,用于在多线程环境下管理和重用线程。它的核心思想是提前创建一组线程(即线程池),然后根据需要将任务分配给这些线程执行,而不是每次需要新任务时都创建一个新的线程

线程池优点

  • 提高性能 : 避免频繁创建和销毁线程:创建和销毁线程是开销较大的操作,特别是在高并发的应用程序中。线程池通过重用已经存在的线程,减少了这些开销。
  • 控制线程数量 :线程池可以限制同时运行的线程数量,避免系统资源(如内存、CPU)被过多的线程消耗,从而保持系统稳定性。
  • 任务分配:线程池通常会提供一个任务队列,任务可以被动态地加入队列,然后由线程池中的线程处理。线程池负责从队列中取出任务并执行,开发者不需要手动管理每个线程的生命周期。
  • 自动化管理 :线程池可以自动处理线程的创建、调度、任务分配、销毁等细节,使得开发者能够专注于任务逻辑的实现,而不必关注底层的线程管理。
  • 提高资源利用率 : 由于线程在执行完任务后不会被立即销毁,而是回到线程池中待命,可以提高线程的利用率,减少资源浪费。
  • 负载均衡 :线程池可以通过合适的调度算法,平衡线程之间的任务负载,确保每个线程都能均匀地分担任务。

实现

完整代码(GitHub)
    首先来封装pthread库的线程, 这次参考了c++线程库线程的模板可变参数构造
线程的简单封装

class Thread
{
public:
    Thread()
    {}
    //模板可变参数构造, 使用起来更方便
    template<class Fn,class ...Args>
    Thread(Fn && func,Args ... args):_func([=](){std::invoke(func,args...);})
    {}
    bool start()
    {
        int n=pthread_create(&_tid,nullptr,_thread,(void*)this);
        if(n!=0) return false;
        return true;
    }
    void join()
    {
        pthread_join(_tid,nullptr);
    }
    pthread_t gettid()
    {
        return _tid;
    }
private:
    pthread_t _tid;
    std::function<void()> _func;
    static void* _thread(void*args)
    {
        Thread* p=(Thread*)args;
        p->_func();
        return nullptr;
    }
};

简单线程池框架

class Threadpool
{
public:
    Threadpool(int thread_num,tasks& tasks) : _thread_num(thread_num), _v(thread_num),_q(tasks)
    {}
    //启动线程池
    void start()
    {}
    //暂停线程池
    void stop()
    {}
    //让所有线程退出
    void quit()
    {}
    ~Threadpool()
    {}

private:
    int _thread_num;//线程池数量
    std::vector<Thread> _v;//使用数组管理线程
    tasks& _q;//引用外面的任务队列
    pthread_mutex_t _mutex;//访问任务队列互斥锁
    pthread_cond_t _cond;//线程等待
    int _wait_num=0;//等待线程的数量
    bool _isrunning = false;//线程池是否运行
    bool _isquit=false;//线程池是否要退出
    //线程都执行的方法
    void work(Thread*p)
    {}
};

初始化

Threadpool(int thread_num,tasks& tasks) : _thread_num(thread_num), _v(thread_num),_q(tasks)
{
    pthread_mutex_init(&_mutex, nullptr);
    pthread_cond_init(&_cond, nullptr);
    for (int i = 0; i < _thread_num; i++)
    {
        _v[i] = Thread(&Threadpool::work,this,&_v[i]);//初始化每个线程
    }
}

启动每个线程

void start()
{
    _isrunning=true;
    for(auto& i:_v)
    {
        i.start();
    }
}

work
    这是所有线程都执行的方法

void work(Thread*p)
{
    while (1)
    {
        pthread_mutex_lock(&_mutex);
        //线程池运行, 但任务队列没有任务, 线程就等待
        while(_isrunning && _q.empty())
        {
            _wait_num++;
            pthread_cond_wait(&_cond,&_mutex);
            _wait_num--;
        }
        //线程池停止并且任务队列为空或者线程池退出, 线程直接退出
        if ((!_isrunning&&_q.empty())||(_isquit))
        {
            INFO("thread is quit...\n");//这是写的日志工具里的
            pthread_mutex_unlock(&_mutex);
            break;
        }
        //到这里就能拿取任务队列的任务了
        INFO("thread is running...\n");
        task temp = _q.front();
        _q.pop();
        //有任务, 线程还在睡觉, 直接叫醒
        if(!_q.empty()&&_wait_num>0) pthread_cond_signal(&_cond);
        pthread_mutex_unlock(&_mutex);
        temp();//执行任务
        sleep(2);//模拟长时任务
    }
}

控制线程池

//设置停止状态
void stop()
{
    _isrunning=false;
    if(_wait_num>0&&_q.empty()) 
    pthread_cond_broadcast(&_cond);//如果任务为空,线程没必要等待
}
//设置退出状态
void quit()
{
    _isquit=true;
    pthread_cond_broadcast(&_cond);//线程没必要再等待任务了
}

销毁

~Threadpool()
{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
    for(auto& i:_v)
    {
        i.join();//等待所有线程
    }
}

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

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

相关文章

无人机之消费级和工业级,两者区别分析

消费级无人机和工业级无人机在多个方面存在显著差异&#xff0c;这些差异主要体现在搭载设备、应用领域、针对用户、使用条件、性能要求、营销模式以及价格等方面。以下是对两者区别的详细分析&#xff1a; 1. 搭载设备 消费级无人机&#xff1a;主要搭载相机&#xff0c;并配…

C++ | Leetcode C++题解之第337题打家劫舍III

题目&#xff1a; 题解&#xff1a; struct SubtreeStatus {int selected;int notSelected; };class Solution { public:SubtreeStatus dfs(TreeNode* node) {if (!node) {return {0, 0};}auto l dfs(node->left);auto r dfs(node->right);int selected node->val…

Windows禁止应用联网

转自两种方法阻止电脑上的软件彻底联网&#xff01; - 知乎 (zhihu.com) 但为了稳妥&#xff0c;自己还是稍微记录一下 1、创建bat脚本文件 创建文本-将下面的代码填入-保存为.bat文件 Echo Off SetLocal:beginecho: echo ****** 禁止文件夹联网 ****** echo:set /p folder…

Qt报“libpng warning: iCCP: known incorrect sRGB profile”问题解决方法

Qt开发应用程序&#xff0c;界面加载图片或按钮加载图标时&#xff0c;会遇到编译器报“libpng warning: iCCP: known incorrect sRGB profile”问题&#xff0c;原因为色彩配置问题&#xff0c;需要修正图像的ICC配置文件&#xff0c;将其转换成sRGB类型。不同操作系统解决方法…

停车场拓扑(parking lot topology)中的 bbr 与 aimd

bbr 讨论组有个有趣的问题&#xff1a;[bbr-dev] Parking lot topology 我此前也意识到这个问题(参见 pacing 之对错)&#xff0c;但几乎所有 cc 的建模都基于 dumbbell topology&#xff0c;parking lot topology 因其太 “不理想”&#xff0c;“不规则” 而无人讨论&#x…

11.2.软件系统分析与设计-数据库分析与设计

文章目录 数据库分析与设计步骤ER图和关系模型 需求分析阶段概念结构设计逻辑结构设计物理结构设计数据库实施与运维 数据库分析与设计 数据库设计属于系统设计的范畴。通常把使用数据库系统的系统统称为数据库应用系统&#xff0c;把对数据库应用系统的设计简称为数据库设计。…

轻松拿捏自动添加好友

释放双手&#xff0c;一键导入数据&#xff01; 通过好友后可以自动备注 轻松自动添加好友&#xff0c;更可以个性化设置验证信息 手动点击“开始”&#xff0c;后台可以看到数据使用情况和添加情况&#xff0c;频繁了会自动停止

【STM32】ADC模拟数字转换(规则组多通道)+ DMA数据转运(外设到存储器)

本篇博客重点在于标准库函数的理解与使用&#xff0c;搭建一个框架便于快速开发 目录 前言 ADC规则组扫描模式DMA 定义变量 规则组配置 ADC初始化 连续模式 扫描模式 规则组通道个数 ADC初始化框架 DMA初始化 ADC和DMA使能 软件触发转运 代码框架 ADC扫描转换与DM…

一眼心动的HAProxy高级功能配置

目录 一.haproxy-基于cookie的会话保持 二.七层IP透传 三.四层IP透传 四.访问控制列表ACL 五.acl做动静分离访问控制 六.基于自定义的错误页面文件 七.HAProxy 四层负载 八.HAProxy https 实现 九.让文件编写更简单的方法 一.haproxy-基于cookie的会话保持 cookie va…

C语言程序设计(初识C语言后部分)

1024M1GB&#xff0c;1GB1级棒。关爱一级棒的程序员们&#xff0c;宠TA没商量&#xff01; 5&#xff09;函数的嵌套调用和链式访问 函数和函数之间可以根据实际的需求进行组合的&#xff0c;也就是相互调用的。 1.嵌套调用 函数可以嵌套调用&#xff0c;但不可以嵌套定义&a…

【网络】UDP和TCP之间的差别和回显服务器

文章目录 UDP 和 TCP 之间的差别有连接/无连接可靠传输/不可靠传输面向字节流/面向数据报全双工/半双工 UDP/TCP API 的使用UDP APIDatagramSocket构造方法方法 DatagramPacket构造方法方法 回显服务器&#xff08;Echo Server&#xff09;1. 接收请求2. 根据请求计算响应3. 将…

html+css+js网页制作 纳尔多珠宝40个页面

htmlcssjs网页制作 纳尔多珠宝40个页面 网页作品代码简单&#xff0c;可使用任意HTML编辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源码 1&#…

用python制作88键赛博钢琴(能用鼠标键盘进行弹奏)

用python制作88键赛博钢琴 前言 恭喜这位博主终于想起了自己的账号密码&#xff01; 时光荏苒&#xff0c;转眼间已逾一年未曾在此留下墨香。尽管这一年间&#xff0c;博主投身于无尽的忙碌与挑战之中&#xff0c;但令人欣慰的是&#xff0c;那份初心与热情似乎并未因岁月的流…

谷歌浏览器网页底图设置为全黑

输入网址&#xff1a;chrome://flags/ 搜索dark&#xff0c;选择Enabled&#xff0c;重启浏览器即可

C#使用SharGL实现PUMA560机械臂

1、四轴机械臂 下载链接&#xff1a;https://download.csdn.net/download/panjinliang066333/89645225 关键代码 public void DrawRobot1(ref OpenGL gl,float[] angle,float[] yLength,bool isPuma560_Six){//坐标系说明&#xff1a;//①X轴正向&#xff1a;屏幕朝右//②Y轴…

【运维系列】windows虚拟机作为服务器,将服务启动作为脚本设置为开机自启,服务中断、手动操作的烦恼通通滚蛋!

文章目录 前言一、开机启动文件夹&#xff08;StartUp&#xff09;是否可行&#xff1f;二、任务计划程序1.编写脚本2.打开任务计划程序3.创建任务4.配置常规选项5.配置触发器选项6. 配置操作选项7.配置条件选项8.配置设置选项 总结 前言 在实际应用过程中&#xff0c;我们难免…

有没有电脑桌面监控软件|大佬都在用的7大电脑屏幕监控软件!

当谈到电脑桌面监控软件时&#xff0c;确实有许多受欢迎且功能强大的选项。 这些软件在企业管理、远程办公、家庭监控等多个领域都有广泛应用。 以下是大佬常用的7大电脑屏幕监控软件推荐&#xff1a; 1. Teramind 特点&#xff1a;它是一款功能强大的企业级监控软件&#x…

永久旋转 PDF 文件的 3 种简便方法

PDF 文件通常由扫描仪创建&#xff0c;用于呈现文档或书籍。当您输出 PDF 作品时&#xff0c;打开它&#xff0c;会发现有几页是颠倒的。 你应该做什么&#xff1f; 将这些页面倒置扫描&#xff0c;按顺序排列&#xff0c;最后创建正确的 PDF 文件&#xff1f; 当然&#xf…

<数据集>安全帽和安全背心识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;22141张 标注数量(xml文件个数)&#xff1a;22141 标注数量(txt文件个数)&#xff1a;22141 标注类别数&#xff1a;3 标注类别名称&#xff1a;[helmet, vest, head] 序号类别名称图片数框数1helmet15937572402v…

轻量级TinyDB数据库文件写入和增删改查操作

1. TinyDB 数据库简介 TinyDB 是一个轻量级的 NoSQL 文档型数据库&#xff0c;由 Python 实现&#xff0c;无需额外的配置&#xff0c;以 JSON 文件作为存储&#xff0c;默认使用文件系统来存储数据。 2. 安装基本库 pip install tinydb pip install Faker3. 数据库操作 im…