【Linux】认识线程池 AND 手撕线程池(正常版)

news2024/11/25 22:52:42

文章目录

  • 0.回顾进程池
  • 1.计算机层面的池化技术
  • 2.线程池预备知识
    • 2.1介绍线程池
    • 2.2设计线程池的意义是什么?
    • 2.3其他知识
  • 3.回顾C++类与对象
    • 3.1cpp什么情况下成员函数必须是静态的?
    • 3.1可变参数列表
    • 3.2格式化输出函数
    • 3.3预定义符号
  • 4.图解线程池运作原理
    • 4.0完整代码
      • Makefile
      • log.hpp
      • lockGuard.hpp
      • Task.hpp
      • thread.hpp
      • stdThreadPool.hpp
      • stdTestMain.cc
    • 4.1详细图解
    • 4.2运行结果
  • 5.指针版的线程池

0.回顾进程池

在这里插入图片描述

模拟进程池

1.计算机层面的池化技术

在计算机层面,池化技术是一种常见且重要的编程和设计技巧。其核心思想在于提前准备和保存大量的资源,以备不时之需,同时实现资源的重复使用,提高资源使用效率。这些资源可以是内存、线程、数据库连接等,它们通常被组织在一个特定的“池子”中,方便进行统一管理和复用。

池化技术有多种应用形式,如内存池、线程池、连接池等。例如,在数据库连接池中,系统会预先创建一定数量的数据库连接,并存放在连接池中。当需要访问数据库时,程序可以直接从连接池中获取一个已存在的连接,而不是每次都重新创建新的连接。这样,可以显著降低系统频繁建连的资源开销,提高应用性能。

池化技术的优点:

提高性能。通过重用资源,减少了创建和销毁资源的时间,从而提高了资源的使用效率
降低系统开销, 避免了频繁地向操作系统申请和释放资源的开销
简化代码。通过封装资源管理逻辑,使得应用程序代码更简洁易懂

此外,在人工智能与机器学习领域,池化技术也有重要的应用。

在卷积神经网络(CNN)中,池化层用于对卷积层的输出进行下采样,以减少参数数量和计算量,同时保留模型的表达能力。这种池化技术对于图像处理、自然语言处理、计算机视觉等任务至关重要。

总的来说,池化技术通过提前创建和重复利用资源,提高了系统的性能和资源使用效率,是计算机领域中一种非常重要的技术。

2.线程池预备知识

2.1介绍线程池

在Linux背景下,线程池是一种优化线程管理的技术,旨在减少线程创建和销毁的开销,提高系统的响应能力和稳定性。线程池预先创建并维护一组线程,这些线程在应用程序需要执行并发任务时可以被复用。

线程池的核心思想是将任务队列与线程集合分离。当有新任务到达时,线程池会将其放入任务队列中,而不是立即创建新线程。线程池中的线程会不断从任务队列中取出任务并执行,直到队列为空或线程池被关闭。

什么是线程池

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

线程池的设计考虑到了以下几点:

线程数量控制:线程池通过设定最大线程数来限制并发执行的任务数量,避免过多线程导致系统资源耗尽。

任务队列管理:线程池使用队列来存储待执行的任务,这允许任务以先入先出(FIFO)的顺序被处理,同时保证线程可以无锁地获取任务,提高并发性能。

线程复用:线程池中的线程在完成任务后不会立即销毁,而是继续等待新的任务,从而减少了线程的创建和销毁开销。

可扩展性和可配置性:线程池通常提供配置选项,允许开发者根据应用程序的需求调整线程数量、任务队列大小等参数。

在Linux环境下,线程池的实现可以依赖于底层的线程库(如pthread库)或更高级的并发框架。这些实现通常提供了线程池的创建、任务提交、线程管理等功能,使开发者能够更方便地利用线程池来优化应用程序的并发性能。

通过使用线程池,Linux应用程序可以更好地管理线程资源,提高系统的响应速度、吞吐量和稳定性,特别是在处理大量并发任务时表现出色。

2.2设计线程池的意义是什么?

在Linux下,线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的ThreadFactory创建一个新线程。

设计线程池的主要意义有以下几点:

资源复用:线程是一种宝贵的系统资源,频繁地创建和销毁线程会消耗大量的系统时间和资源。线程池通过预先创建一定数量的线程并保存在内存中,实现了线程的复用,避免了线程的频繁创建和销毁,从而提高了系统的性能。
控制最大并发数:线程池可以限制线程的数量,防止因为创建过多的线程而耗尽系统资源。通过线程池,我们可以设定一个最大并发数,确保系统的稳定性和可靠性。
提高响应速度:当任务到达时,如果线程池中有空闲线程,那么任务可以立即被处理,无需等待新线程的创建。这可以大大提高系统的响应速度。
便于管理:线程池提供了一种统一的方式来管理线程,包括线程的创建、销毁、调度等。这使得我们可以更方便地对线程进行监控和管理。
总的来说,线程池通过复用线程、控制最大并发数、提高响应速度和便于管理等方式,有效地提高了系统的性能和稳定性。在Linux下,我们可以利用一些库(如pthread库)或者框架(如C++11的std::thread)来方便地实现线程池。

2.3其他知识

  1. 申请内存要调用系统调用:嵌入内核/更改CPU状态/切换页表/内存管理算法(刷新缓冲区/进行IO/腾出空间)/整理内存碎片/杀掉不常用应用节省空间。这一系列操作要耗费资源(时间/空间)
  2. 创建线程时:创建/初始化各种数据结构 申请内存 维护各种关系

为什么要用线程池?

主要是为了以空间换时间,预先申请一批线程,新任务到来时,直接指派线程而非实时创建。什么是线程池?一次预先申请一批线程,如果有任务就处理,没任务等待。

线程池的目的:

减少系统调用的次数,提高使用内存的效率。

何为线程池?

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

线程池的应用场景

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

线程池示例:

  1. 创建固定数量线程池,循环从任务队列中获取任务对象
  2. 获取到任务对象后,执行任务对象中的任务接口

3.回顾C++类与对象

3.1cpp什么情况下成员函数必须是静态的?

在C++项目中,成员函数被声明为静态的情况主要有以下几种:

  1. 无需访问对象状态:当成员函数不需要访问或修改类的非静态成员变量时,它可以被声明为静态。静态成员函数只能访问静态成员变量和其他静态成员函数。这是因为静态成员函数与任何特定的对象实例无关,它们属于类本身,而不是类的任何特定实例。
  2. 作为工具函数:静态成员函数经常作为工具函数使用,这些函数执行与类相关的某些任务,但不涉及任何特定的对象实例。例如,一个类可能有一个静态成员函数来解析或生成特定于该类的某种数据格式。
  3. 作为工厂函数:静态成员函数也经常用作工厂函数,用于创建和返回类的实例。这种方式可以隐藏类的构造函数,并提供更安全的对象创建方式,例如通过确保所有必要的参数都被提供,或者在创建对象之前执行某些初始化步骤。
  4. 作为回调函数:在某些情况下,需要将类的成员函数作为回调函数传递给其他函数或对象。由于非静态成员函数需要一个对象实例来调用,因此不能直接将它们作为回调函数。在这种情况下,可以创建一个静态成员函数作为回调函数的接口,然后在静态成员函数内部调用非静态成员函数。
    请注意,虽然静态成员函数在某些情况下很有用,但它们也有一些限制。例如,静态成员函数不能调用非静态成员函数(除非通过对象实例),也不能直接访问非静态成员变量。因此,在决定使用静态成员函数时,需要仔细考虑这些限制以及它们是否适合你的特定用例。
    在C++中,当需要将成员函数作为回调函数传递给其他函数或对象时,常常会遇到问题。这是因为非静态成员函数在调用时需要一个对象实例(通过this指针),而回调函数通常不会提供这样的实例。因此,直接将非静态成员函数作为回调函数是不可行的。

为了解决这个问题,一种常见的方法是使用静态成员函数作为回调函数的接口。静态成员函数不依赖于任何对象实例,因此可以直接作为回调函数传递。在静态成员函数内部,可以通过其他方式(如全局变量、单例模式、参数传递等)来访问或操作类的实例。

下面是一个简单的示例来说明这个概念:

cpp
#include <iostream>  
#include <functional>  
  
class MyClass {  
public:  
    // 静态成员函数作为回调函数  
    static void StaticCallback(const std::function<void()>& func) {  
        // 在这里调用传递进来的函数对象  
        func();  
    }  
  
    // 非静态成员函数,我们想要在某个时候以回调的形式调用它  
    void NonStaticMemberFunction() {  
        std::cout << "NonStaticMemberFunction called!" << std::endl;  
    }  
  
    // 一个用于绑定非静态成员函数的辅助函数  
    std::function<void()> BindNonStaticMemberFunction() {  
        MyClass* instance = this; // 保存当前对象实例的指针  
        return [instance]() { // 使用lambda捕获实例指针  
            instance->NonStaticMemberFunction(); // 调用非静态成员函数  
        };  
    }  
};  
  
int main() {  
    MyClass obj;  
    // 将非静态成员函数的绑定作为参数传递给静态回调函数  
    MyClass::StaticCallback(obj.BindNonStaticMemberFunction());  
    return 0;  
}

在上面的代码中,MyClass有一个静态成员函数StaticCallback,它接受一个std::function<void()>类型的参数。这个std::function可以包含任何可调用对象,包括lambda表达式、函数指针或绑定到对象的成员函数。

NonStaticMemberFunction是MyClass的一个非静态成员函数,我们想要在某个时候以回调的形式调用它。

BindNonStaticMemberFunction是一个辅助成员函数,它返回一个std::function<void()>对象,该对象内部是一个lambda表达式,捕获了MyClass的实例指针,并在调用时通过这个指针调用NonStaticMemberFunction。

在main函数中,我们创建了一个MyClass的实例obj,并通过调用obj.BindNonStaticMemberFunction()将非静态成员函数的绑定作为参数传递给静态回调函数MyClass::StaticCallback。这样,当StaticCallback被调用时,它实际上会调用我们绑定的非静态成员函数。

这种方法允许我们绕过非静态成员函数需要对象实例的限制,使得它们能够以回调的形式被使用。然而,这种方法需要小心处理对象的生命周期,确保在回调函数被调用时对象仍然有效。

3.1可变参数列表

在这里插入图片描述

  1. va_start函数
    功能:用于初始化可变参数列表的访问。它设置了一个va_list类型的变量,使其指向可变参数列表的起始位置。

工作原理:在函数内部,参数是以栈的形式存储的,从右向左依次压入栈中。va_start通过获取最后一个固定参数的地址,然后计算出可变参数列表的起始地址,并将这个地址赋值给va_list类型的变量。这样,后续就可以通过这个变量来访问可变参数列表了。

  1. va_arg宏
    功能:用于获取可变参数列表中的下一个参数,并将其转换为指定的类型。

工作原理:va_arg通过指针偏移的方式来访问可变参数列表中的数据。在调用va_arg时,它会根据当前va_list变量所指向的地址,以及所请求的类型的大小,计算出下一个参数的地址,并将va_list变量的值更新为这个新地址。然后,返回这个地址所指向的值,并将其转换为指定的类型。

  1. va_end函数
    功能:用于结束可变参数的获取过程,并清理为va_list变量分配的内部数据。

工作原理:在遍历完可变参数列表后,需要调用va_end来释放与va_list变量相关的资源。这通常涉及到恢复栈的状态,确保在函数返回后,栈能够正确地返回到调用前的状态。如果未正确使用va_end,可能会导致程序崩溃或产生不可预测的行为。

  1. va_copy函数
    功能:用于复制一个va_list变量的状态到另一个va_list变量,这样两个变量都可以用来遍历相同的可变参数列表。

工作原理:va_copy简单地将源va_list变量的值(即指向可变参数列表的指针)复制到目标va_list变量中。这样,两个变量就指向了相同的可变参数列表,可以独立地进行遍历操作。需要注意的是,在使用完复制的va_list变量后,也需要调用va_end来进行清理。

在使用这些函数和宏时,通常遵循以下步骤:首先使用va_start初始化va_list变量,然后使用va_arg逐个获取可变参数,最后使用va_end进行清理。如果需要同时遍历相同的可变参数列表,可以使用va_copy来复制va_list变量的状态。

下面是一个简单的示例,展示了如何使用这些函数来创建一个接受可变数量整数的函数,并计算它们的总和:

#include <stdio.h>
#include <stdarg.h>
int sum_of_ints(int count, ...)
{
    int sum = 0;

    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; i++)
    {
        sum += va_arg(args, int);
    }
    va_end(args);
    return sum;
}
int main()
{
    printf("Sum: %d\n", sum_of_ints(3, 1, 2, 3)); // 输出: Sum: 6
    return 0;
}

在这个示例中,sum_of_ints 函数接受一个整数 count,表示后面可变参数的数量,然后使用 va_start、va_arg 和 va_end 来遍历并计算这些参数的总和。

3.2格式化输出函数

在这里插入图片描述

#include <stdio.h>

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

#include <stdarg.h>

int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

3.3预定义符号

在这里插入图片描述

4.图解线程池运作原理

4.0完整代码

Makefile

stdThreadPool:stdTestMain.cc
	g++ -o $@ $^ -std=c++11 -lpthread -DDEBUG_COMPILE
.PHONY:clean
clean:
	rm -f stdThreadPool

log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *gLevelMap[] = {
    " DEBUG ",
    " NORMAL",
    "WARNING",
    " ERROR ",
    " FATAL "};

#define LOGFILE "./threadPool.log"

// 日志功能: 日志等级 时间 用户自定义(日志内容/文件名/文件行) 等
void logMsg(int level, const char *format, ...)
{
#ifndef DEBUG_COMPILE // 非调试编译下 不输出DEBUG信息
    if (level == DEBUG)
        return;
#endif

    // 1.标准日志内容
    char stdBuf[1024];
    // 1.1获取时间戳
    time_t timestamp = time(nullptr);
    if (timestamp == std::time_t(-1))
    {
        std::cerr << "获取时间失败" << std::endl;
        exit(1);
    }
    // 1.2获取格式化时间
    struct tm *CLK = std::localtime(&timestamp); // tm *localtime(const time_t *__timer)
    //1.3将日志信息输出到日志文件
    // snprintf(stdBuf, sizeof stdBuf, "[%s] [%ld] ", gLevelMap[level], timestamp);
    snprintf(stdBuf, sizeof stdBuf, "[%s] [%d/%d/%d %d:%d:%d ", gLevelMap[level],
             1900 + CLK->tm_year, 1 + CLK->tm_mon, CLK->tm_mday, CLK->tm_hour, CLK->tm_min, CLK->tm_sec);

    // 2.用户自定义内容
    va_list args;
    va_start(args, format);
    char logBuf[1024];
    // int vsnprintf(char *str, size_t size, const char *format, va_list ap);
    vsnprintf(logBuf, sizeof logBuf, format, args);
    va_end(args);

    FILE *fp = fopen(LOGFILE, "a");
    // fprintf(stdout, "%s%s\n", stdBuf, logBuf);
    fprintf(fp, "%s%s\n", stdBuf, logBuf);
    fclose(fp);
}

lockGuard.hpp

#pragma once

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

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx)
        : _pmtx(mtx)
    {
    }

    void lock()
    {
        //std::cout << "加锁中..." << std::endl;
        pthread_mutex_lock(_pmtx);
    }

    void unlock()
    {
        //std::cout << "解锁中..." << std::endl;
        pthread_mutex_unlock(_pmtx);
    }

    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_pmtx;
};

// RAII风格的加锁方式
class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx)
        : _mtx(mtx)
    {
        _mtx.lock();
    }

    ~lockGuard()
    {
        _mtx.unlock();
    }

private:
    Mutex _mtx;
};

Task.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"

typedef std::function<int(int, int)> func_t;

class Task
{
public:
    Task() {}
    Task(int x, int y, func_t func)
        : _x(x),
          _y(y),
          _startRoutine(func)
    {
    }
    void operator()(const std::string &threadName)
    {
        logMsg(WARNING, "%s 处理任务: %d+%d=%d | %s | %d | %s | %s",
               threadName.c_str(), _x, _y, _startRoutine(_x, _y), __FILE__, __LINE__, __DATE__, __TIME__);
    }

public:
    int _x;
    int _y;
    func_t _startRoutine;
};

thread.hpp

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

// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *);

class ThreadInfo
{
public:
    std::string _threadName;
    void *_ptrThreadPool;
};

class Thread
{
public:
    Thread(int index, fun_t startRoutine, void *ptrTotp)
        : _startRoutine(startRoutine)
    {
        char nameBuf[64];
        snprintf(nameBuf, sizeof nameBuf, "Thread-%d", index);
        _name = nameBuf;

        _tInfo._threadName = _name;
        _tInfo._ptrThreadPool = ptrTotp;
    }

    void start()
    {
        pthread_create(&_tid, nullptr, _startRoutine, (void *)&_tInfo);
    }

    void join()
    {
        pthread_join(_tid, nullptr);
    }
    std::string name()
    {
        return _name;
    }
    ~Thread()
    {
    }

private:
    pthread_t _tid;
    std::string _name;
    fun_t _startRoutine;
    ThreadInfo _tInfo;
};

stdThreadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"

const int g_threadNum = 3;

template <class T>
class stdThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &lock;
    }
    void waitCond()
    {
        pthread_cond_wait(&cond, &lock);
    }
    bool isEmpty()
    {
        return _taskQueue.empty();
    }
    T getTask()
    {
        T task = _taskQueue.front();
        _taskQueue.pop();
        return task;
    }
    static void *startRoutine(void *args)
    {
        ThreadInfo *threadInfo = (ThreadInfo *)args;
        stdThreadPool<T> *ptrTotp = (stdThreadPool<T> *)threadInfo->_ptrThreadPool;
        while (true)
        {
            T task;
            {
                lockGuard lockguard(ptrTotp->getMutex());
                while (ptrTotp->isEmpty())
                    ptrTotp->waitCond();
                task = ptrTotp->getTask(); 
            }
            task(threadInfo->_threadName);
        }
    }
    // 构造函数
    stdThreadPool(int threadNum = g_threadNum)
        : _threadNum(threadNum)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);

        for (int i = 1; i <= _threadNum; i++)
        {
            // 初始化列表区域 对象还未存在 走到函数块{}内 对象已存在 可以使用this指针
            _threads.push_back(new Thread(i, startRoutine, this));
        }
    }
    // 启动多线程
    void run()
    {
        for (auto &iter : _threads)
        {
            iter->start();
            logMsg(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }
    void pushTask(const T &task)
    {
        lockGuard lockguard(&lock);
        _taskQueue.push(task);
        pthread_cond_signal(&cond);
    }
    // 析构函数
    ~stdThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

private:
    int _threadNum;
    std::vector<Thread *> _threads;
    std::queue<T> _taskQueue;

    pthread_mutex_t lock;
    pthread_cond_t cond;
};

stdTestMain.cc

#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>

#include "stdThreadPool.hpp"
#include "Task.hpp"

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());

    stdThreadPool<Task> *tp = new stdThreadPool<Task>();
    tp->run();

    while (true)
    {
        // 生产数据/制作任务 -- 耗费时间
        int x = rand() % 10 + 1;
        usleep(1000);
        int y = rand() % 5 + 1;
        Task t(x, y, [](int x, int y) -> int
               { return x + y; });

        logMsg(DEBUG, "Main-Pro 发送任务: %d+%d=未知", x, y);

        // 推送任务到线程池中
        tp->pushTask(t);
        sleep(1);
    }
    return 0;
}

4.1详细图解

在这里插入图片描述

4.2运行结果

在这里插入图片描述

5.指针版的线程池

  1. 搞两个queue1, queue2
  2. std::queue *p_queue, *c_queue; p_queue->queue1 ;c_queue->queue2
  3. p_queue->生产一批任务之后,swap(p_queue, c_queue), 唤醒所有线程 / 一个线程
  4. 消费者处理完毕,swap(p_queue, c_queue)
  5. 生产和消费用的是不同的队列,进行资源的处理【线程安全问题】的时候,仅仅是指针

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

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

相关文章

并查集----格子游戏

并查集中最重要的是要搞懂&#xff1a; 不明白的可以拿纸自己先演示一番&#xff0c;find函数不仅能找到他们的祖先数&#xff0c;而且同时也能更新路径的子结点都等于祖先&#xff0c;然后以后寻找时会更加的方便&#xff01;

P3817 小A的糖果(贪心)

思路&#xff1a;真绝了&#xff0c;开了longlong从80分到了100分。因为一个特判st值影响我AC&#xff0c;那个单独的特判竟然有问题。我想的是如果有a[i] a[i1]则将状态值st标为true。最后在输出结果之前先看st的值&#xff0c;如果他为false&#xff0c;则说明没有两盒子的和…

网络编程套接字 (一)

本专栏内容为&#xff1a;Linux学习专栏&#xff0c;分为系统和网络两部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握Linux。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;网络 &#x1f69a;代码仓库&#xff1a;小小unicorn的代…

InnoDB支持哪几种行格式?

一、问题解析 数据库表的行格式决定了一行数据是如何进行物理存储的&#xff0c;进而影响查询和DML操作的性能。 在InnoDB中&#xff0c;常见的行格式有4种&#xff1a; 1COMPACT &#xff1a;是MySQL 5.0之前的默认格式&#xff0c;除了保存字段值外&#xff0c;还会利用空值…

沃通国密SSL根证书入根赢达信国密浏览器

近日&#xff0c;沃通CA国密SSL根证书正式入根赢达信国密安全浏览器&#xff0c;携手推动国产密码技术应用、完善国密应用生态体系&#xff0c;也标志着沃通国密SSL证书兼容性再次得到提升&#xff0c;进一步夯实国密应用根基。 密码算法的安全性是信息安全保障的核心&#xff…

时序预测 | Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测

时序预测 | Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测 目录 时序预测 | Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测&#xff08;完整源码和数据…

PyLMKit(8):ChatDB与你的数据库聊天,数据库问答

功能介绍 与你的结构化数据聊天&#xff1a;支持主流数据库、表格型excel等数据&#xff01; ChatDB&#xff1a;支持数据库问答ChatTable&#xff1a;支持txt,excel,csv等pandas dataframe表格的问答 1.下载安装 pip install pylmkit -U pip install pymysql sqlalchemy s…

基于el-table实现行内增删改

实现效果&#xff1a; 核心代码&#xff1a; <el-table :data"items"style"width: 100%;margin-top: 16px"border:key"randomKey"><el-table-column label"计划名称"property"name"><template slot-scope&q…

40.HarmonyOS鸿蒙系统 App(ArkUI)实现页面跳转与返回

1.新建项目&#xff0c;默认创建inext.ets界面。 2.右键page添加第二个页面&#xff0c;设置page2,点击finish 设置按钮触发事件&#xff1a; page2.ets按钮触发事件 index.ets import FaultLogger from ohos.faultLogger import promt2 from ohos.prompt import promt_action…

Linux重点思考(中)--端口/静态内存/负载/日志

这里写目录标题 知道的linux常用命令&#xff1a;查看指定端口进程netstat -pantunetstat -pantu|grep 22 静态运行内存free硬盘物理内存df和du当前负载uptime查看日志awk统计文件每一行单词sed 替换文件单词 知道的linux常用命令&#xff1a;查看指定端口进程 netstat -pantu…

C#简单——多选框控件相关的神奇问题

他们真的很简单&#xff0c;但我怎么总是忘记啊QAQ 还是记下来吧&#xff0c;下次直接copy 点开后出现 有列名 列名有数据&#xff0c;有表 表里有数据但不显示的情况 解决&#xff1a;点击小三角-点击Design View-Columns-需要在这里对下拉列表里的内容进行对应配置。 多选框…

ngAlain下使用nz-select与文件上传框出现灵异bug

bug描述 初始化页面&#xff0c;文件上传框无法出现&#xff1a; 但点击一次选择框以后&#xff0c;就会出现&#xff1a; 真的很神奇。。。 下面逐步排查看看是什么原因。 设想一&#xff1a; 选择框与文件框不可同时存在&#xff0c;删掉选择框看看&#xff1a; 还…

pytest--python的一种测试框架--pycharm创建项目并进行接口请求

前言 学习request的使用&#xff0c;在用之前&#xff0c;用官方文档提供的接口&#xff1a;https://api.github.com/events&#xff1b; ctrl鼠标左键可以进入被调用函数源码&#xff0c;可以看到第一个参数URL是必须参数&#xff0c;params是选填&#xff0c;**kwargs是关键…

基于单片机的自动浇灌系统的设计

本文设计了一款由单片机控制的自动浇灌系统。本设计的硬件电路采用AT89C51单片机作为主控芯片,采用YL-69土壤湿度传感器检测植物的湿度。通过单片机将采集湿度值与设定值分析处理后,控制报警电路和水泵浇灌电路的开启,从而实现植物的自动浇灌。 1 设计目的 随着生活水平的…

哥本哈根Major后steam搬砖该何去何从?

都在问我哥本哈根major比赛过后市场会不会崩盘呢&#xff1f;说实话&#xff0c;我是不喜欢预测市场的&#xff0c;其实是没那个本事而已。若真有这个预测市场走势的本事&#xff0c;我还用坐在这里每天苦哈哈的搬砖吗&#xff1f;我直接干囤卡囤号的倒卖生意岂不早发财了&…

Docker 哲学 - push 本机镜像 到 dockerhub

注意事项&#xff1a; 1、 登录 docker 账号 docker login 2、docker images 查看本地镜像 3、注意的是 push镜像时 镜像的tag 需要与 dockerhub的用户名保持一致 eg&#xff1a;本地镜像 express:1 直接 docker push express:1 无法成功 原因docker不能识别 push到哪里 …

LeetCode题练习与总结:N皇后

一、题目描述 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回所有不同的 n 皇后问题 的解决…

【嵌入式——C语言】VScode编写C程序、交叉编译

【嵌入式——C语言】VScode编写C程序、交叉编译 第一步第二步第三步第四步第五步第六步第七步第八步 第一步 下载Visual Studio Code下载地址 然后直接安装就可以了。 第二步 前提是你的电脑上安装了WSL。。。 打开vscode的扩展&#xff0c;输入WSL进行安装 安装完之后在窗…

SpringBoot 集成分布式任务调度 XXL-JOB【保姆级上手】

文章目录 XXL-JOB 介绍分布式任务调度XXL-JOB 概述 快速入门下载源码初始化调度数据库编译源码调度中心调度中心介绍配置调度中心部署调度中心集群部署调度中心&#xff08;可选&#xff09;Docker 镜像方式搭建调度中心&#xff08;可选&#xff09; 执行器执行器介绍添加依赖…

DOM 节点遍历:掌握遍历 XML文档结构和内容的技巧

遍历是指通过或遍历节点树 遍历节点树 通常&#xff0c;您想要循环一个 XML 文档&#xff0c;例如&#xff1a;当您想要提取每个元素的值时。 这被称为"遍历节点树"。 下面的示例循环遍历所有 <book> 的子节点&#xff0c;并显示它们的名称和值&#xff1a;…