【linux】基于单例模式实现线程池

news2024/11/18 17:28:18

文章目录

  • 一、线程池
    • 1.1 池化的概念
    • 1.2 线程池的实现
    • 1.3 线程池的使用场景
  • 二、单例模式
    • 2.1 单例模式概念
    • 2.2 单例模式的线程池
    • 2.3 防过度优化
  • 三、源码

一、线程池

1.1 池化的概念

当我们处理任务的时候,一般就是来一个任务我们就创建一个线程来处理这个任务。这里首先的一个问题就是效率会降低(创建线程需要成本)。

接下来我们可以类比STL的扩容机制,当我们使用vector申请扩容的时候,就算我们只多申请一块空间,它也会给我们直接按照扩容机制给我们多扩容1.5倍或两倍,这样当我们后边还想扩容的时候就不用申请资源了。

而我们可以借助这种思想,我们先创建一批线程,当任务队列里面没任务时,每个线程都先休眠,一旦任务队列来了任务,就会唤醒线程来处理。
唤醒一个线程的成本要比创建一个线程的成本要小。

而我们把这个模型就叫做线程池

1.2 线程池的实现

先把之前对原生线程库封装的组件引入,再做一些小小的调整。

// mythread.hpp
#pragma once

#include <iostream>
#include <pthread.h>
#include <cstring>
#include <string>
#include <cassert>
#include <functional>
#include <unistd.h>

class Thread
{
    typedef std::function<void*(void*)> func_t;
private:
    // 不加static就会有this指针
    static void* start_routine(void* args)
    {
        //return _func(args);
        // 无this指针,无法调用
        Thread* pct = static_cast<Thread*>(args);
        pct->_func(pct->_args);
        return nullptr;
    }
public:
    Thread(func_t fun, void* args = nullptr)
        : _func(fun)
        , _args(args)
    {
        char buf[64];
        snprintf(buf, sizeof buf, "thread-%d", _number++);
        _name = buf;
    }

    void start()
    {
        // int n = pthread_create(&_tid, nullptr, _func, _args);
        // _func是C++函数,pthread_create是C接口,不能混编
        int n = pthread_create(&_tid, nullptr, start_routine, this);
        assert(n == 0);
        (void)n;
    }

    std::string GetName()
    {
        return _name;
    }

    void join()
    {
        int n = pthread_join(_tid, nullptr);
        assert(n == 0);
        (void)n;
    }
private:
    std::string _name;// 线程名
    pthread_t _tid;// 线程id
    func_t _func;// 调用方法
    void *_args;// 参数
    static int _number;// 线程编号
};

int Thread::_number = 1;


// Main.cc
#include "mythread.hpp"

using std::cout;
using std::endl;

void* threadhandler(void* args)
{
    std::string ret = static_cast<const char*>(args);
    while(true)
    {
        cout << ret << endl;
        sleep(1);
    }
}

int main()
{
    Thread t1(threadhandler, (void*)"thead1");
    Thread t2(threadhandler, (void*)"thead2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    return 0;
}

在这里插入图片描述

验证过没有问题以后就可以实现线程池创建一批线程。

既然需要多个线程访问这个任务队列,那么就需要用锁来保护资源,而我们直接可以把之前写过的锁的小组件引入进来:

// mymutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>

class Mutex 
{
public:
    Mutex(pthread_mutex_t* plock = nullptr)
        : _plock(plock)
    {}

    void lock()
    {
        // 被设置过
        if(_plock)
        {
            pthread_mutex_lock(_plock);
        }
    }

    void unlock()
    {
        if(_plock)
        {
            pthread_mutex_unlock(_plock);
        }
    }
private:
    pthread_mutex_t *_plock;
};

// 自动加锁解锁
class LockAuto
{
public:
    LockAuto(pthread_mutex_t *plock)
        : _mutex(plock)
    {
        _mutex.lock();
    }

    ~LockAuto()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

在创建一批线程的时候,我们要实现线程的运行函数,因为是要传给Thread类里面的_func中,所以不能有this指针,必须是静态成员函数。但是设置成静态成员函数的时候,就没有this指针,无法访问成员变量(锁和任务队列等),所以我们要封装这些接口。

template <class T>
class ThreadPool
{
private:
    static void* handlerTask(void* args)
    {
        ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
        while(true)
        {
            tp->lockqueue();
            while(tp->isqueueempty())
            {
                tp->threadwait();
            }
            T t = tp->pop();
            tp->unlockqueue();
            t();// 处理任务
        }
    }

    void lockqueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    void unlockqueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    bool isqueueempty()
    {
        return _tasks.empty();
    }

    void threadwait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    T pop()
    {
        T res = _tasks.front();
        _tasks.pop();
        return res;
    }
public:
    ThreadPool(int num = 5)
        : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        // 创建线程
        for(int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread(handlerTask, this));
        }
    }

    void start()
    {
        for(auto& t : _threads)
        {
            t->start();
            cout << t->GetName() << " start..." << endl;
        }
    }

    void push(const T& in)
    {
        LockAuto lock(&_mutex);
        _tasks.push(in);
        // 唤醒池中的一个线程
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto & e : _threads)
        {
            delete e;
        }
    }
private:
    int _num;// 线程数量
    std::vector<Thread*> _threads;
    std::queue<T> _tasks;// 任务队列
    pthread_mutex_t _mutex;// 保护任务队列
    pthread_cond_t _cond;
};

一些细节:

1️⃣ 在构造函数的时候,线程要传递参数,为了让线程函数(静态成员函数)能够获取到成员变量,所以要把this对象传递过去。
2️⃣ 在线程函数中,处理任务t()要放在解锁后,因为pop()的本质是将任务从公共资源中拿到当前进程的独立栈结构中,首先它已经不需要被保护了,其次如果放到加锁和解锁之间也不知道会运行多久,造成资源浪费。现在的情况就是线程拿到任务后就把锁释放掉,自己处理任务,不影响其他线程拿取任务。

现在我们想要像之前一样处理各种数据的计算,那么先引入任务组件:

// Task.hpp
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
#include <unordered_map>

class Task
{
    typedef std::function<int(int, int, char)> func_t;
public:
    Task()
    {}

    Task(int x, int y, char op, func_t func)
        : _x(x)
        , _y(y)
        , _op(op)
        , _func(func)
    {}

    std::string operator()()
    {
        int res = _func(_x, _y, _op);
        char buf[64];
        snprintf(buf, sizeof buf, "%d %c %d = %d", _x, _op, _y, res);
        return buf;
    }
    std::string tostringTask()
    {
        char buf[64];
        snprintf(buf, sizeof buf, "%d %c %d = ?", _x, _op, _y);
        return buf;
    }
private:
    int _x;
    int _y;
    char _op; 
    func_t _func;
};

const std::string oper = "+-*/";

std::unordered_map<char, std::function<int(int, int)>> hash = {
        {'+', [](int x, int y)->int{return x + y;}},
        {'-', [](int x, int y)->int{return x - y;}},
        {'*', [](int x, int y)->int{return x * y;}},
        {'/', [](int x, int y)->int{
            if(y == 0)
            {
                std::cerr << "除0错误" << endl;
                return -1;
            }
            return x / y;}},
    };

int myMath(int x, int y, char op)
{
    int res = hash[op](x, y);
    return res;
}

现在我们想要线程处理任务的时候知道是哪个线程进行处理的,我们可以把传进参数的位置进行改变,不要在构造的时候传递,而是在运行的时候传递,这样就能传进线程启动函数中。
在这里插入图片描述

在这里插入图片描述
实现代码:

// ThreadPool.hpp
#pragma once

#include <vector>
#include <queue>
#include "mythread.hpp"
#include "mymutex.hpp"
#include "Task.hpp"

using std::cout;
using std::endl;

const int N = 5;

template <class T>
class ThreadPool;

template <class T>
struct ThreadData
{
    ThreadPool<T>* _tp;
    std::string _name;
    ThreadData(ThreadPool<T>* tp, const std::string& name)
        : _tp(tp)
        , _name(name)
    {}
};

template <class T>
class ThreadPool
{
private:
    static void* handlerTask(void* args)
    {
        ThreadData<T>* tdp = static_cast<ThreadData<T>*>(args);
        while(true)
        {
            tdp->_tp->lockqueue();
            while(tdp->_tp->isqueueempty())
            {
                tdp->_tp->threadwait();
            }
            T t = tdp->_tp->pop();
            tdp->_tp->unlockqueue();
            cout << tdp->_name << " 获取任务: " << t.tostringTask() << " 结果是: " << t() << endl;
            //t();// 处理任务
        }
    }

    void lockqueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    void unlockqueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    bool isqueueempty()
    {
        return _tasks.empty();
    }

    void threadwait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    T pop()
    {
        T res = _tasks.front();
        _tasks.pop();
        return res;
    }
public:
    ThreadPool(int num = 5)
        : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        // 创建线程
        for(int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }

    void start()
    {
        for(auto& t : _threads)
        {
            ThreadData<T>* td = new ThreadData<T>(this, t->GetName());
            t->start(handlerTask, td);
        }
    }

    void push(const T& in)
    {
        LockAuto lock(&_mutex);
        _tasks.push(in);
        // 唤醒池中的一个线程
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto & e : _threads)
        {
            delete e;
        }
    }
private:
    int _num;// 线程数量
    std::vector<Thread*> _threads;
    std::queue<T> _tasks;// 任务队列
    pthread_mutex_t _mutex;// 保护任务队列
    pthread_cond_t _cond;
};

// Main.cc
#include "mythread.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <cstdlib>

int main()
{
    ThreadPool<Task>* tp = new ThreadPool<Task>();
    tp->start();
    srand(time(0));
    int x, y;
    char op;
    while(true)
    {
        x = rand() % 100;
        y = rand() % 50;
        op = oper[rand() % 4];
        Task t(x, y, op, myMath);
        tp->push(t);
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

1.3 线程池的使用场景

1️⃣ 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2️⃣对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3️⃣接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

二、单例模式

2.1 单例模式概念

关于单例模式在之前的文章【C++】特殊类设计(单例模式)有过详细介绍。
这里用一句话总结:单例模式的特点就是全局只有一个唯一对象。
实现的模式有饿汉模式和懒汉模式。

饿汉就是程序一开始运行就加载对象,而懒汉是要用的时候才会加载。

举个例子:

我们平时malloc/new就是懒汉模式,我们申请的时候并没有真正的在物理内存中被申请,而是我们要写入的时候才会申请物理内存并且建立虚拟地址空间和物理空间的映射关系(页表)。

所以懒汉模式的最核心的思想就是延时加载

2.2 单例模式的线程池

我们要做的第一步就是把构造函数私有,再把拷贝构造和赋值运算符重载delete。
在这里插入图片描述
接下来就要在成员变量中定义一个静态指针,方便获取单例对象。
在这里插入图片描述
在设置获取单例对象的函数的时候,注意要设置成静态成员函数,因为在获取对象前根本没有对象,无法调用非静态成员函数(无this指针)。

static ThreadPool<T>* GetSingle()
{
    if(_tp == nullptr)
    {
        _tp = new ThreadPool<T>();
    }
    return _tp;
}

在调用的时候:

int main()
{
    ThreadPool<Task>::GetSingle()->start();
    srand(time(0));
    int x, y;
    char op;
    while(true)
    {
        x = rand() % 100;
        y = rand() % 50;
        op = oper[rand() % 4];
        Task t(x, y, op, myMath);
        ThreadPool<Task>::GetSingle()->push(t);
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述
不过也许会出现多个线程同时申请资源的场景,所以还需要一把锁来保护这块资源,而这把锁也得设置成静态,因为GetSingle()函数是静态的,访问不到成员函数。

在这里插入图片描述

static ThreadPool<T>* GetSingle()
{
    _singlelock.lock();
    if(_tp == nullptr)
    {
        _tp = new ThreadPool<T>();
    }
    _singlelock.unlock();
    return _tp;
}

运行结果:
在这里插入图片描述

2.3 防过度优化

当我们有多个线程的时候,很可能会把资源的指针放入寄存器中,如果有个线程修改了,那么就会出问题。
所以我们要用volatile关键字保证内存可见性。

volatile static ThreadPool<T>* _tp

三、源码

// ThreadPool.hpp
#pragma once

#include <vector>
#include <queue>
#include <mutex>
#include "mythread.hpp"
#include "mymutex.hpp"
#include "Task.hpp"

using std::cout;
using std::endl;

const int N = 5;

template <class T>
class ThreadPool;

template <class T>
struct ThreadData
{
    ThreadPool<T>* _tp;
    std::string _name;
    ThreadData(ThreadPool<T>* tp, const std::string& name)
        : _tp(tp)
        , _name(name)
    {}
};

template <class T>
class ThreadPool
{
private:
    static void* handlerTask(void* args)
    {
        ThreadData<T>* tdp = static_cast<ThreadData<T>*>(args);
        while(true)
        {
            tdp->_tp->lockqueue();
            while(tdp->_tp->isqueueempty())
            {
                tdp->_tp->threadwait();
            }
            T t = tdp->_tp->pop();
            tdp->_tp->unlockqueue();
            cout << tdp->_name << " 获取任务: " << t.tostringTask() << " 结果是: " << t() << endl;
            //t();// 处理任务
        }
    }

    void lockqueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    void unlockqueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    bool isqueueempty()
    {
        return _tasks.empty();
    }

    void threadwait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    T pop()
    {
        T res = _tasks.front();
        _tasks.pop();
        return res;
    }

    ThreadPool(int num = 5)
        : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        // 创建线程
        for(int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }

    ThreadPool(const ThreadPool<T>& ) = delete;
    ThreadPool<T> operator=(const ThreadPool<T>&) = delete;

public:

    void start()
    {
        for(auto& t : _threads)
        {
            ThreadData<T>* td = new ThreadData<T>(this, t->GetName());
            t->start(handlerTask, td);
        }
    }

    void push(const T& in)
    {
        LockAuto lock(&_mutex);
        _tasks.push(in);
        // 唤醒池中的一个线程
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto & e : _threads)
        {
            delete e;
        }
    }

    static ThreadPool<T>* GetSingle()
    {
        if(_tp == nullptr)
        {
            _singlelock.lock();
            if(_tp == nullptr)
            {
                _tp = new ThreadPool<T>();
            }
            _singlelock.unlock();
        }
        return _tp;
    }

private:
    int _num;// 线程数量
    std::vector<Thread*> _threads;
    std::queue<T> _tasks;// 任务队列
    pthread_mutex_t _mutex;// 保护任务队列
    pthread_cond_t _cond;
    volatile static ThreadPool<T>* _tp;
    static std::mutex _singlelock;
};

template <class T>
ThreadPool<T>* ThreadPool<T>::_tp = nullptr;

template <class T>
std::mutex ThreadPool<T>::_singlelock;


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

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

相关文章

类实例化对象的存储与内存对齐,this指针与调用成员函数传参的本原面目(两种调用方式)

类的实例化对象的内存存储方式与内存对齐 对于类当中定义的成员函数&#xff0c;是放在公共代码区的&#xff0c;在公共代码区有类成员函数表。对于不同的实例化对象而言&#xff0c;它里面的各个成员变量都是不一样的&#xff0c;但是如果他们分别调用相同名字的成员函数&…

cmd切换壁纸 适用windows10

文章目录 代码代码讲解参考文章菜鸟的目录结构注意 昨天菜鸟上班但是真的没活干&#xff0c;闲着无聊&#xff0c;突然发现自己壁纸好久都是一个&#xff0c;看着真的烦了&#xff0c;但是下载一个壁纸软件又感觉实际用处不大还占着内存&#xff0c;所以菜鸟就想&#xff0c;要…

Java Type接口出现的原因以及它和泛型的关系

Java泛型很多人都用过&#xff0c;但是对于其中的原理可能很多人可能都不太清楚。 首先给出一个结论&#xff1a;Java的泛型是伪泛型&#xff0c;因为JVM在编译以后会将所有泛型参数擦除掉&#xff0c;这个就叫类型擦除。 下面用一段代码来证明&#xff0c;毕竟千言万语BB不如…

【软考数据库】第三章 数据结构与算法

目录 3.1 数据结构 3.1.1 线性结构 3.1.2 数组 3.1.3 矩阵 3.1.4 树与二叉树 3.1.5 图 3.2 查找 3.2.1 顺序查找 3.2.2 折半查找 3.2.3 哈希表 3.3 排序 3.3.1 直接插入排序 3.3.2 希尔排序 …

Win10任务栏卡死怎么办?这3个方法快收藏!

案例&#xff1a;win10任务栏卡死 【姐妹们&#xff0c;我的win10任务栏一直卡着&#xff0c;我完全没法使用计算机了&#xff0c;遇到这种情况&#xff0c;我应该怎么做呢&#xff1f;求大家给我支支招&#xff01;感谢感谢&#xff01;】 我们使用电脑的过程中&#xff0c;…

MyBatis的添加和简单使用

什么是MyBatis mybatis是一个方便我们更简单的操作数据库的框架&#xff0c;让我们不用再使用JDBC操作数据库。 MyBatis的创建 老项目添加mybatis&#xff0c;首先要安装好editstarters插件&#xff0c;然后在pom.xml中右键generate选择edit插件&#xff0c;注意不仅要添加m…

多维时序 | MATLAB实现BO-CNN-BiLSTM贝叶斯优化卷积双向长短期记忆网络数据多变量时间序列预测

多维时序 | MATLAB实现BO-CNN-BiLSTM贝叶斯优化卷积双向长短期记忆网络数据多变量时间序列预测 目录 多维时序 | MATLAB实现BO-CNN-BiLSTM贝叶斯优化卷积双向长短期记忆网络数据多变量时间序列预测效果一览基本介绍模型搭建程序设计参考资料 效果一览 基本介绍 基于贝叶斯优化卷…

C/C++|物联网开发入门+项目实战|宏定义|数据声明|位操作|类型修饰符|访问固定内存位置|嵌入式C语言高级|常见面试题目讲解-学习笔记(13)

文章目录 常见面试题目讲解宏定义数据声明类型修饰符的使用总结位操作访问固定内存位置 参考&#xff1a; 麦子学院-嵌入式C语言高级-C语言函数的使用-常见面试题目讲解 参考&#xff1a; 嵌入式程序员应该知道的0x10个基本问题 常见面试题目讲解 宏定义 1 .用预处理指令#d…

ERD Online 4.1.0对接ChatGPT,实现AI建模、SQL自由

ERD Online 是全球第一个开源、免费在线数据建模、元数据管理平台。提供简单易用的元数据设计、关系图设计、SQL查询等功能&#xff0c;辅以版本、导入、导出、数据源、SQL解析、审计、团队协作等功能、方便我们快速、安全的管理数据库中的元数据。 4.1.0 ❝ :memo: fix(erd): …

CARIS11.3使用一段时间后的经验和总结

虽然CARIS11.4存在一些小bug&#xff0c;但CARIS11.3使用没有什么问题&#xff0c;相对于CARIS9而言&#xff0c;在导入数据和程序界面有些改进。用过CARIS9的同学都知道其建立项目和导入数据的步骤比较繁琐。而CARIS11.3导入数据的过程比较简洁&#xff0c;基本步骤如下&#…

把阿里大鸟花3个月时间整理的软件测试面经偷偷给室友,差点被他开除了···

写在前面 “这份软件测试面经看起来不错&#xff0c;等会一起发给他吧”&#xff0c;我看着面前的面试笔记自言自语道。 就在这时&#xff0c;背后传来了leder“阴森森”的声音&#xff1a;“不错吧&#xff0c;我可是足足花了三个月整理的” 始末 刚入职阿里的我收到了大学…

牛客网Verilog刷题——VL2

牛客网Verilog刷题——VL2 题目答案 题目 要求用verilog实现两个串联的异步复位的T触发器的逻辑&#xff0c;如下图所示。   模块的输入输出信号如下表&#xff0c;需要注意的是&#xff1a;这里rst是低电平复位&#xff0c;且采用异步复位的方式复位。 信号类型输入/输出c…

2023年淮阴工学院五年一贯制专转本大学语文考试大纲

2023年淮阴工学院五年一贯制专转本大学语文考试大纲 一、考试目标 淮阴工学院五年一贯制高职专转本入学考试秘书学专业《大学语文》考试是我校为招收五年一贯制高职专转本学生设置的具有选拔性质的考试科目。其目的是科学、公平、有效地测试考生是否具备攻读秘书学本科学位所…

( “树” 之 BST) 530. 二叉搜索树的最小绝对差 ——【Leetcode每日一题】

二叉查找树&#xff08;BST&#xff09;&#xff1a;根节点大于等于左子树所有节点&#xff0c;小于等于右子树所有节点。 二叉查找树中序遍历有序。 ❓ 530. 二叉搜索树的最小绝对差 难度&#xff1a;简单 给你一个二叉搜索树的根节点 root &#xff0c;返回 树中任意两不同…

特征选择算法 | Matlab 基于无限潜在特征选择算法(ILFS)的分类数据特征选择

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 特征选择算法 | Matlab 基于无限潜在特征选择算法(ILFS)的分类数据特征选择 部分源码 %

Vite 4.3 is out!

原文地址 本次迭代中&#xff0c;我们专注于改善开发服务器的性能。我们优化了解析逻辑&#xff0c;改进了热路径&#xff0c;并实现了更智能的缓存&#xff0c;用于查找 package.json、TS 配置文件和解析的 URL 等。 你可以在 Vite 的贡献者之一的博客文章中详细了解本次性能…

数据结构之二分搜索树

树在我们底层结构中是被广泛运用的,但是为什么会选择它却是我们需要了解的东西,接下来 让我们一起走进树的世界 请看下图&#xff1a; 在我们生活中&#xff0c;有很多关于树的存在&#xff0c;比如电脑中的磁盘&#xff08;C D盘&#xff09;&#xff0c;在文章中写的目录都是…

LangChain与大型语言模型(LLMs)应用基础教程:记忆力组件

如果您还没有看过我之前写的两篇博客&#xff0c;请先看一下&#xff0c;这样有助于对本文的理解&#xff1a; LangChain与大型语言模型(LLMs)应用基础教程:Prompt模板 LangChain与大型语言模型(LLMs)应用基础教程:信息抽取 LangChain与大型语言模型(LLMs)应用基础教程:角色…

在线甘特图制作教程

在线甘特图制作教程 很多的甘特图工具都是需要下载到本地&#xff0c;并且做好了之后也不方便分享给别人。给大家分享一个在线的甘特图制作工具 不需要登录注册 知竹甘特图 https://www.yxsss.com/ 打开知竹甘特图 https://www.yxsss.com/gatt/3b7d1ecb7211b9473e7d1ecb72 …

015:Mapbox GL绘制修改多边形,实时更新面积

第015个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中添加draw组件,绘制多边形,编辑多边形,实时显示面积值。这里使用turf来计算面积值。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共92行)安装…