【Linux系统】POSIX信号量 线程池

news2024/11/17 4:46:35

PISIX信号量

概念

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

引入环形队列的概念 

 

 环形队列:当队列为空||为满时

head == end,我们发现这样无法区分为空为满两种情况

所以:我们需要1.计数器 2.牺牲一个空位置,判满就变为if(head == end + 1)

 如果队列不为空不为满就是head != end

问题:多线程如何在环形队列中进行生产和消费1.单生产,单消费 2.多生产,多消费

1.队列为空,让谁先访问?生产者先生产

2.队列为满,让谁先访问?消费者来消费

3.队列不为空&&队列不为满,生产和消费同时进行!!!(并发的!!!)

第三点要满足条件:a.不然生产者把消费者套一个圈b.不能让消费者超过生产者

所以前面两点不仅要有顺序,还要互斥。

三点放在一起就是互斥与同步的!!! ---- 所以这些信号量就能做到这些!!!

消费者只关心的资源,数据资源!生产者只关心的资源,空间资源!!!

sem_t data_sem = 0;
sem_t space_sem = N;

 生产与消费的关系是通过PV操作来实现的!

//生产者
P(space_sem);
//生产
V(data_sem);

//消费者
P(data_sem);
//消费
V(space_sem);

实现

 信号量接口

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:指向信号量 的指针
  • pshared:0表示线程间共享,非零表示进程间共享
  • value:信号量初始值 

 销毁信号量

int sem_destroy(sem_t *sem);

等待信号量(P操作)

功能:等待信号量,会将信号量的值减1

会将信号量的值减一,如果信号量的值为零,调用线程将被阻塞,直到信号量的值大于零

int sem_wait(sem_t *sem); //P()

发布信号量 (V操作)

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

会将信号量的值加一,如果有任何线程在等待该信号量,它们将被唤醒。

int sem_post(sem_t *sem);//V()

基于环形队列的生产消费模型

生产消费模型的高效率核心问题是:构建数据和后来的处理数据实现了多线程并发执行!!!因为这两个部分是最耗时的!

  • 环形队列采用数组模拟,用模运算来模拟环状特性
  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
  • 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程

RingQueue.hpp

#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>
template<class T>
class RingQueue
{
    void P(sem_t& s)
    {
        sem_wait(&s);
    }
    void V(sem_t& s)
    {
        sem_post(&s);
    }
public:
    RingQueue(int max_cap):_max_cap(max_cap), _ring_queue(max_cap), _c_step(0), _p_step(0)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, max_cap);

        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }
    void Pop(T* out)//消费
    {
        //信号量:信号量是一个计数器,是资源的预订机制。预订:在外部,可以不判断资源是否满足,就可以知道内部资源的情况。
        P(_data_sem);//信号量这里,对资源进行使用,申请,为什么不判断一下条件是否满足???信号量本身就是判断条件!!!
        pthread_mutex_lock(&_c_mutex);
        *out = _ring_queue[_c_step];
        _c_step++;
        _c_step %= _max_cap;
        pthread_mutex_unlock(&_c_mutex);
        V(_space_sem);
    }
    void Push(const T &in)//生产
    {
        P(_space_sem);
        pthread_mutex_lock(&_p_mutex);
        _ring_queue[_p_step] = in;
        _p_step++;
        _p_step %= _max_cap;
        pthread_mutex_unlock(&_p_mutex);
        V(_data_sem);
    }
    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    std::vector<T> _ring_queue;
    int _max_cap;

    int _c_step;
    int _p_step;

    sem_t _data_sem;//消费者关心
    sem_t _space_sem;//生产者关心

    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};

线程池

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

 下面我们使用打印日志的方式,并且运用单例模式中的懒汉模式实现了一个简单的线程池!!!

ThreadPool.hpp --- 线程池主逻辑封装

#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <string>
#include <unistd.h>
#include "Thread.hpp"
#include "Task.hpp"
#include "Log.hpp"

using namespace ThreadMoudle;
using namespace log_ns;
static const int gdefaultnum = 5;
void test()
{
    while (true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }
}
template <typename T>
class ThreadPool
{
private:
    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void UnlockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }
    void Wakeup()
    {
        pthread_cond_signal(&_cond);
    }
    void WakeupAll()
    {
        pthread_cond_broadcast(&_cond);
    }
    void Sleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    bool IsEmpty()
    {
        return _task_queue.empty();
    }
    void HandlerTask(const std::string &name) // this
    {
        while (true)
        {
            // 取任务
            LockQueue();
            while (IsEmpty() && _isrunning)
            {
                _sleep_thread_num++;
                LOG(INFO, "%s thread sleep begin!\n", name.c_str());
                Sleep();
                LOG(INFO, "%s thread wakeup begin!\n", name.c_str());
                _sleep_thread_num--;
            }
            // 只需判断一种情况
            if (IsEmpty() && !_isrunning)
            {
                UnlockQueue();
                LOG(INFO, "%s thread quie!\n", name.c_str());
                break;
            }
            // 有任务
            T t = _task_queue.front();
            _task_queue.pop();
            UnlockQueue();
            // 处理任务,此处不用/不能在临界区中处理
            t();
            // std::cout << name << ":" << t.result() << std::endl;
            LOG(DEBUG, "hander task done, task is:%s\n", t.result().c_str());
        }
    }
    void Init()
    {
        func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
        for (int i = 0; i < _thread_num; i++)
        {
            std::string threadname = "thread-" + std::to_string(i + 1);
            _threads.emplace_back(threadname, func);
            LOG(DEBUG, "construct thread %s done, init success!\n", threadname.c_str());
        }
    }
    void Start()
    {
        _isrunning = true;
        for (auto &thread : _threads)
        {
            LOG(DEBUG, "start thread %s done.\n", thread.Name().c_str());
            thread.Start();
        }
    }
    ThreadPool(int thread_num = gdefaultnum) : _thread_num(thread_num), _isrunning(false), _sleep_thread_num(0)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    ThreadPool<T> operator=(const ThreadPool<T> &) = delete;

public:
    void Stop()
    {
        LockQueue();
        _isrunning = false;
        WakeupAll();
        UnlockQueue();
        LOG(DEBUG, "Thread Pool Stop Success!\n");
    }
    // 如果是多线程获取单例呢?
    static ThreadPool<T> *GetInstance()
    {
        LockGuard lockguard(&_sig_mutex);
        if (_tp == nullptr)
        {
            if (_tp == nullptr)
            {
                LOG(INFO, "create threadpool!\n");
                // thread-1,thread-2...
                _tp = new ThreadPool();
                _tp->Init();
                _tp->Start();
            }
            else
            {
                LOG(INFO, "get threadpool!\n");
            }
        }
        return _tp;
    }
    void Equeue(const T &in)
    {
        LockQueue();
        if (_isrunning)
        {
            _task_queue.push(in);
            if (_sleep_thread_num > 0)
                Wakeup();
        }
        UnlockQueue();
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

private:
    int _thread_num;
    std::vector<Thread> _threads;
    std::queue<T> _task_queue;
    bool _isrunning;

    int _sleep_thread_num;

    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    // 单例模式
    static ThreadPool<T> *_tp;
    static pthread_mutex_t _sig_mutex;
};

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

Thread.hpp --- 封装了我们自己的线程库

#pragma once
#include<iostream>
#include<string>
#include<pthread.h>
#include<functional>

namespace ThreadMoudle
{
    //线程要执行的方法
    //typedef void (*func_t)(ThreadData *td);
    using func_t = std::function<void(const std::string&)>;
    class Thread
    {
    public:
        void Excute()
        {
            _isrunning = true;
            _func(_name);
            _isrunning = false;
        }
    public:
        Thread(const std::string& name, func_t func) :_name(name), _func(func)
        {
        }
        static void* ThreadRoutine(void* args)//新线程都会执行该方法
        {
            Thread* self = static_cast<Thread*>(args);//获得了当前对象
            self->Excute();
            return nullptr;
        }
        bool Start()
        {
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if(n != 0) return false;
            return true;
        }
        std::string Status()
        {
            if(_isrunning) return "running";
            else return "sleep";
        }
        void Stop()
        {
            if(_isrunning)
            {
                ::pthread_cancel(_tid);
                _isrunning = false;
            }
        }
        std::string Name()
        {
            return _name;
        }
        void Join()
        {
            ::pthread_join(_tid, nullptr);
        }
        ~Thread()
        {

        }
    public:
        std::string _name;
        pthread_t _tid;
        bool _isrunning;
        func_t _func; //线程要执行的回调函数
    };   
};

main.cc --- 不断推送任务到线程池并且测试

#include"ThreadPool.hpp"
#include"Task.hpp"
#include"Log.hpp"

using namespace log_ns;

int main()
{
    EnableScreen();
    // LOG(DEBUG, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);
    // LOG(WARNING, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);
    // LOG(FATAL, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);
    // LOG(ERROR, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);
    // EnableFile();
    // LOG(DEBUG, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);
    // LOG(DEBUG, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);
    // LOG(DEBUG, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);
    // LOG(DEBUG, "hello %d, world: %c, hello: %f\n", 1000, 'A', 3.14);

    // ThreadPool<Task>* tp = new ThreadPool<Task>();
    // tp->Init();
    // tp->Start();
    int cnt = 10;
    while(cnt)
    {
        //不断向线程池推送任务
        sleep(1);
        Task t(1, 1);
        ThreadPool<Task>::GetInstance()->Equeue(t);
        LOG(INFO, "equeue a task, %s\n", t.debug().c_str());
        sleep(1);
        cnt--;
    }
    ThreadPool<Task>::GetInstance()->Stop();
    LOG(INFO, "thread pool stop!\n");
    return 0;
}

Task.hpp --- 任务的来源我们这里是一个加法任务

 

#pragma once
#include<iostream>
#include<string>
#include<functional>

//typedef std::function<void()> task_t;
// using task_t = std::function<void()>;

// void Download()
// {
//     std::cout << "我是一个下载任务" << std::endl;
// }
class Task
{
public:
    Task()
    {}
    Task(int x, int y):_x(x), _y(y)
    {}
    void Excute()
    {
        _result = _x + _y;
    }
    std::string debug()
    {
        std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?";
        return msg;
    }
    void operator()()
    {
        Excute();
    }
    std::string result()
    {
        std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);
        return msg;
    }
private:
    int _x;
    int _y;
    int _result;
};

Log.hpp --- 封装一个我们自己的日志库
 

#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include <cstring>
#include "LockGuard.hpp"

namespace log_ns
{
    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL // 致命的
    };
    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }
    std::string GetCurrTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }
    class logmessage
    {
    public:
        std::string _level;
        pid_t _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    class log
    {
    public:
        log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }
        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._id,
                   lg._filename.c_str(),
                   lg._filenumber,
                   lg._curr_time.c_str(),
                   lg._message_info.c_str());
        }
        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;
            char logtxt[2048];
            snprintf(logtxt, sizeof logtxt, "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._id,
                     lg._filename.c_str(),
                     lg._filenumber,
                     lg._curr_time.c_str(),
                     lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }
        void FlushLog(const logmessage &lg)
        {
            LockGuard lockguard(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }
        void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._message_info = log_info;

            // 打印出来日志
            FlushLog(lg);
        }
        ~log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    log lg;

#define LOG(level, Format, ...)                                        \
    do                                                                 \
    {                                                                  \
        lg.logMessage(__FILE__, __LINE__, level, Format, ##__VA_ARGS__); \
    } while (0)
#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)
#define EnableFile()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)
};

 

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

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

相关文章

Springboot2.6.13整合flowable6.8.1

背景 项目上需要使用到工作流相关内容&#xff0c;比对了好久采用flowable实现&#xff0c;该插件和activiti等很相似&#xff0c;基本上可以直接移植 代码如下 <!-- 父引用用--><parent><groupId>org.springframework.boot</groupId><artifactI…

LLM的训练与推断

LLM的训练与推断 目前比较流行的大模型一般都是自回归模型。在推理时&#xff0c;它类似于RNN&#xff0c;每次计算下一个token的概率。也就是说&#xff0c;如果除去最开始的输入情况下&#xff0c;最终推理长度为n的话&#xff0c;就需要计算n次。但是训练却是并行化的。 在…

你想活出怎样的人生?我只活一次,所以想做自己

你好&#xff0c;我是腾阳。 在这纷繁复杂的世界中&#xff0c;我们每个人都像是一颗颗星星&#xff0c;闪烁着自己的光芒。 然而&#xff0c;在这光芒背后&#xff0c;有多少人真正了解自己&#xff0c;又有多少人敢于追随内心的声音&#xff0c;去追寻那些看似遥不可及的梦…

arduino程序-程序函数2(led电路及相关函数)(基础知识)

arduino程序-程序函数2&#xff08;led电路及相关函数&#xff09;&#xff08;基础知识&#xff09; 1-9 程序函数2&#xff08;led电路及相关函数&#xff09;点亮LED需要Blink程序PinMode(LED_BUTTIN,OUTPUT)DigitalWrite(LED_BUILTIN,HIGH)第一个参数(13/LED_BUILTIN)第二个…

由浅入深的了解进程(1)

进程 1、冯诺依曼体系结构(硬件)2、操作系统(软件)2、1、概念2、2、结构示意图(简略版)3、3、尝试理解操作系统 1、冯诺依曼体系结构(硬件) 大多数常见的计算机&#xff0c;类似笔记本或者是台式电脑&#xff0c;包括不常见的计算机&#xff0c;类似服务器&#xff0c;大多遵循…

安装python插件命令集合

安装python插件pyecharts库 pip install pyecharts -i https://pypi.tuna.tsinghua.edu.cn/simple 安装python插件pandas库 pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple PyCharm 中安装步骤&#xff1a;

Linux安装TrueNAS(网络附加存储)教程 –第1部分

TrueNAS CORE&#xff08;原名FreeNAS&#xff09;是一款流行的存储系统&#xff0c;可帮助您构建自己的高质量存储设置&#xff0c;而无需支付软件费用。您可以将其安装在计算机硬件或虚拟机 (VM) 上&#xff0c;以获得开源存储的好处。 您可以在家中、办公室或数据中心使用T…

微信小程序云开发订单微信支付与小票和标签打印的完整高效流程

一个字“全”&#xff01;&#xff01;&#xff01; 前言一、流程设定1、如何开通云支付流程2、以订单下单为例的支付流程2.1 业务场景介绍2.2 业务场景流程图 二、代码与代码文件组成1、页面JS2、云函数payPre3、支付回调函数pay_cb3.1 准备条件3.2 必要认知3.3 pay_cb 完整函…

day03 3.文件IO 4.文件属性函数

作业 1> 使用文件IO完成&#xff0c;将源文件中的所有内容进行加密&#xff08;大写转小写、小写转大写&#xff09;后写入目标文件中 源文件内容不变 #include <myhead.h>int main(int argc, const char *argv[]) {if(argc ! 3) //判断打开的文件个数{printf(&quo…

【Python学习手册(第四版)】学习笔记10-语句编写的通用规则

个人总结难免疏漏&#xff0c;请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文较简单&#xff0c;5-10分钟即可阅读完成。介绍Python基本过程语句并讨论整体语法模型通用规则&#xff08;冒号、省略、终止、缩进、其他特殊情况&#xff0…

JavaFX布局-StackPane

JavaFX布局-StackPane 常用属性alignmentpadding 实现方式Java实现fxml实现 所有子节点堆叠在一起&#xff0c;通常最后一个添加的子节点会显示在最上面 常用属性 alignment 对齐方式 stackPane.setAlignment(Pos.CENTER_RIGHT); public enum Pos {/*** Represents positioni…

LeYOLO,一种用于目标检测的新型可扩展且高效的CNN架构

摘要 在目标检测中&#xff0c;深度神经网络的计算效率至关重要&#xff0c;尤其是随着新型模型越来越注重速度而非有效计算量&#xff08;FLOP&#xff09;。这一发展趋势在某种程度上忽视了嵌入式和面向移动设备的AI目标检测应用。在本文中&#xff0c;我们基于FLOP关注于高…

马斯克的Memphis AI超级计算中心:全球最强AI训练集群的诞生

引言 近期&#xff0c;马斯克宣布其最新的Memphis AI超级计算中心正式启动&#xff0c;这一新闻引发了科技界的广泛关注。该中心配备了10万块液冷H100 GPU&#xff0c;成为全球最强大的AI训练集群。本文将深入探讨Memphis AI超级计算中心的建设过程、技术细节、以及其对未来人…

Unity多客户端位置同步信息

书接上文&#xff0c;有了一个基本的网络同步消息的服务器&#xff0c;客户端这边其实要做的工作就简单许多。 如果对位置信息的保密程度没那么高的话&#xff0c;可以放在客户端处理这部分的逻辑。 即一个客户端移动的时候&#xff0c;另一个客户端跟着移动&#xff0c;基本…

在Java中利用GeoHash实现高效的‘附近xxx‘功能

GeoHash的介绍 GeoHash是一种高效的地理编码系统&#xff0c;它通过将地球表面划分为网格并用字母数字组合的字符串来表示每个区域。 这种编码方法将二维的经纬度坐标转换为一维的字符串&#xff0c;使得地理位置的存储和检索变得更加简单。GeoHash的核心原理是将经纬度坐标转…

后端开发工程师vue2初识的学习

博客主页&#xff1a;音符犹如代码系列专栏&#xff1a;JavaWeb关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 什么是Vue&#xff1f; Vue &#xff08;通常指 Vue.js&#xff09;是一个用…

权限管理的概述以及vue开发前端的路由、菜单、按钮权限控制实现方案

1. 权限管理概念 1.1 权限定义 权限管理是确保用户只能访问被授权资源的机制。在计算机系统中&#xff0c;权限通常指对特定数据或功能的访问权。权限的设置和控制对于保护数据安全和系统安全至关重要。 1.2 前端权限控制重要性 前端权限控制是用户与应用交互的第一道防线。…

超级好用的免费在线流程图软件

超级好用的免费在线流程图软件 Draw io 是一款免费开源的流程图绘制工具&#xff0c;可在浏览器中使用或下载安装。它提供了简单易用的界面和丰富的图形元素&#xff0c;支持创建各种类型的流程图、组织结构图、网络图等。Draw io 支持导入和导出多种格式&#xff0c;包括 PDF…

从零开始,快速打造API:揭秘 Python 库toapi的神奇力量

在开发过程中&#xff0c;我们常常需要从不同的网站获取数据&#xff0c;有时候还需要将这些数据转化成API接口提供给前端使用。传统的方法可能需要大量的时间和精力去编写代码。但今天我要介绍一个神奇的Python库——toapi&#xff0c;它可以让你在几分钟内创建API接口&#x…

数据库练习——处理表

新建数据库 mysql> create database mydb15_indexstu; Query OK, 1 row affected (0.00 sec)mysql> use mydb15_indexstu; Database changed 新建表 建立student表 mysql> create table student(Sno int primary key auto_increment,-> Sname varchar(30) not …