<Linux> 线程池

news2025/1/16 14:04:06

一、线程池

1. 池化技术

池化技术是一种在计算机科学中广泛应用的优化技术,它的核心思想是:预先创建并维护一组资源(例如线程、连接、对象),供多个任务共享使用,以减少创建和销毁资源的开销,提高效率和性能。

  • 减少资源创建和销毁的开销: 创建和销毁资源(例如线程、数据库连接等)通常需要较高的开销,池化技术可以预先创建一组资源,避免频繁创建和销毁,从而提高性能
  • 降低内存占用: 池化技术可以减少创建和销毁资源所带来的内存开销,从而降低内存占用。
  • 是以空间换时间的策略

2. 线程池概念

线程池是一种线程使用模式。

线程过多会带来调度开销,进而影响缓存局部和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

3. 线程池的优点

  • 线程池避免了在处理短时间任务时创建与销毁线程的代价。
  • 线程池不仅能够保证内核充分利用,还能防止过分调度。

注意: 线程池中可用线程的数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

4. 线程池的应用场景

线程池常见的应用场景如下:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。 

相关解释:

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

5. 线程池实现 

下面我们实现一个简单的线程池,线程池中提供了一个任务队列,以及若干个线程(多线程),主线程向任务队列push任务,线程池中的线程pop获取任务

  • 线程池中的多个线程负责从任务队列当中拿任务,并将拿到的任务进行处理。
  • 线程池对外提供一个Push接口,用于让外部线程能够将任务Push到任务队列当中。

线程池代码:

#pragma once
#include <vector>
#include <queue>
#include <iostream>
#include <pthread.h>

// 线程信息
struct ThreadInfo
{
    pthread_t tid;    // 线程tid
    std::string name; // 线程名字
};

// 线程池
template <class T>
class ThreadPool
{
    static const int defaultnum = 5;

public:
    void Lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void Unlock()   
    {
        pthread_mutex_unlock(&_mutex);
    }
    void Wakeup()
    {
        // 唤醒线程执行任务
        pthread_cond_signal(&_cond);
    }
    void ThreadSleep()
    {
        // 在条件变量下等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    bool IsQueueEmpty()
    {
        return _tasks.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        // 获取线程name
        for (const auto &e : _threads)
        {
            if (e.tid == tid)
                return e.name;
        }
        return "None";
    }

public:
    ThreadPool(int num = defaultnum)
        : _threads(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    static void *HandlerTask(void *args)
    {
        // 获取this指针
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        // 获取线程名字,方便后续打印观察
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            // 1. 加锁
            tp->Lock();

            // 2. 判断是否等待排队(while 防止伪唤醒)
            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            // 3. 获取任务
            T t = tp->pop();
            // 4. 解锁
            tp->Unlock();

            // 5. 运行任务
            t();
            std::cout << name << " run, result: " << t.GetResult() << std::endl;
        }
        sleep(1);
    }

    // 线程池启动,创建线程,执行任务
    void start()
    {
        int num = _threads.size();
        for (int i = 0; i < num; i++)
        {
            _threads[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(_threads[i].tid), nullptr, HandlerTask, this);
            // 传递this指针
            // 1. 如果HandlerTask是普通成员函数,形参抹油有this指针,那么线程创建时还需要额外传递this指针
            // 2. 将HandlerTask改为静态成员函数,可是函数内部需要使用成员函数和成员变量,所以crete时的参数应传递this指针
            // 3. 所以将Lock等函数访问权限设为public
        }
    }

    // 外部push任务
    void push(const T &t)
    {
        // 1. 上锁
        Lock();
        // 2. push
        _tasks.push(t);
        // 3. 唤醒线程执行任务
        Wakeup();
        // 4. 解锁
        Unlock();
    }

    // 在HandlerTask内部调用它时已经加锁了
    T pop()
    {
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    ~ThreadPool()
    {
        // 销毁锁、条件变量
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<ThreadInfo> _threads; // 多线程容器,记录各个线程信息
    std::queue<T> _tasks;             // 线程从任务队列拿任务

    pthread_mutex_t _mutex; // 互斥锁
    pthread_cond_t _cond;   // 条件变量
};

相关细节:

成员变量的设计

  • 任务队列由STL的queue实现,没有限定大小,但是线程池中的线程数量有限制,构造函数处默认为defaultnum
  • 线程池的构造和析构就是对互斥锁和条件变量的初始化和销毁工作,其他的自定义类型成员变量例如_threads、_tasks会调用各自默认的析构函数
  • 还需要有一个互斥锁和条件变量

为什么线程池中需要有互斥锁和条件变量?

  • 线程池需要有互斥锁和条件变量
  • 互斥锁因为线程池中的任务队列是会被多个执行流并发访问的共享资源,因此我们需要引入互斥锁对任务队列进行保护,同一时刻只能由一个执行流对任务队列进行push或pop操作
  • 条件变量是因为线程池当中的线程要从任务队列里拿任务,前提条件是任务队列中必须要有任务,因此线程池当中的线程在拿任务之前,需要先判断任务队列当中是否有任务,若此时任务队列为空,那么该线程应该进行等待,直到任务队列中有任务时再将其唤醒,因此我们需要引入条件变量。

为什么线程池中的线程执行例程 HandlerTask 需要设置为静态方法?

  • 线程池需要由start函数(线程池启动,创建线程,执行任务),该函数用来创建多个线程,并为创建的线程传入HandlerTask 执行例程,但是需要注意,成员函数默认第一个参数为类类型的this指针,所以实际上HandlerTask 有两个参数:类类型指针、void*参数,而pthread_create只能向HandlerTask 传递一个void*参数,所以我们需要将 HandlerTask 函数改为static静态成员函数,因为静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针,此时Routine函数才真正只有一个参数类型为void*的参数。
  • 将 HandlerTask 改为静态成员函数又会引来一个问题,我们需要在 HandlerTask 使用互斥锁、条件变量、pop函数等成员,但是 HandlerTask 静态成员函数没有this指针,无法访问成员变量和成员函数,所以我们需要在start函数的pthread_create处为HandlerTask传递当前对象的 this 指针!此时我们就能够通过该this指针在HandlerTask 函数内部调用非静态成员函数了。
  • 此外 HandlerTask 内部的逻辑是先对任务队列加锁、判断是否等待排队(while 防止伪唤醒)、获取任务、解锁、运行任务

任务类型的设计

  • 我们将线程池进行了模板化,因此线程池当中存储的任务类型可以是任意的,但无论该任务是什么类型的,在该任务类当中都必须包含一个Run方法,当我们处理该类型的任务时只需调用该Run方法即可。

 

#include <string>

std::string opers = "+-*/%";

enum
{
    DivZero = 1,
    ModZero,
    Unknown
};

template <class T>
class Task
{
public:
    Task()
    {}
    
    Task(int x, int y, char op)
        : _data1(x), _data2(y), _op(op), _result(0), _exitcode(0)
    {}

    // run起来
    void run()
    {
        switch (_op)
        {
        case '+':
            _result = _data1 + _data2;
            break;
        case '-':
            _result = _data1 - _data2;
            break;
        case '*':
            _result = _data1 * _data2;
            break;
        case '/':
            {
                if (_data2 == 0) _exitcode = DivZero;
                else _result = _data1 / _data2;
            } 
            break;
        case '%':
            {
                if (_data2 == 0)
                    _exitcode = ModZero;
                else
                    _result = _data1 % _data2;
            }
            break;
        default:
            _exitcode = Unknown;
            break;
        }
    }

    void operator()()
    {
        run();
    }

    std::string GetResult()
    {
        std::string result = std::to_string(_data1) + _op + std::to_string(_data2) + '=' + std::to_string(_result);
        result += "[code: " + std::to_string(_exitcode) + ']';
        return result;
    }

    std::string GetTask()
    {
        std::string result = std::to_string(_data1) + _op + std::to_string(_data2) + "= ?";
        return result;
    }
    
private:
    T _data1;
    T _data2;
    char _op;

    T _result;
    int _exitcode;
};

主线程逻辑 

主线程就负责不断向任务队列当中Push任务就行了,此后线程池当中的线程会从任务队列当中获取到这些任务并进行处理。

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "ThreadPool.hpp"
#include "Task.hpp"

int main()
{
    srand(time(nullptr));

    ThreadPool<Task<int>>* tp = new ThreadPool<Task<int>>(5);
    tp->start();
    // 因为此时已经创建线程,开始执行HandlerTask,所以五个线程申请锁,然后在条件变量下等待(释放锁),排队顺序为1 2 3 4 5
    // 当push任务后,1被唤醒,执行任务,再次申请锁,因为push很慢,所以又在条件变量下释放锁等待,排队顺序为2 3 4 5 1
    while (true)
    {
        // 1. 构建任务
        int data1 = rand() % 10 + 1;
        usleep(10);
        int data2 = rand() % 5;
        char op = opers[rand() % opers.size()];
        Task<int> t(data1, data2, op);
        
        // 2. 交给线程池处理
        tp->push(t);
        std::cout << "main thread make task: " << t.GetTask() << std::endl;
        sleep(1);
    }
    return 0;
}

 

我们会发现这五个线程在处理时会呈现出一定的顺序性,因为主线程在 tp->start() 调用后此时已经创建线程,各线程开始执行 HandlerTask,所以五个线程申请锁,因为创建时是有顺序的,所以最初线程对锁的申请也具有顺序性,然后因为任务队列没有任务,而都在条件变量下等待(释放锁),排队顺序为1 2 3 4 5。

当push任务后,1被唤醒,执行任务,再次申请锁,因为push很慢,所以又在条件变量下释放锁等待,排队顺序为2 3 4 5 1 ,因此这五个线程在处理任务时会呈现出一定的顺序性。

注意: 此后我们如果想让线程池处理其他不同的任务请求时,我们只需要提供一个任务类,在该任务类当中提供对应的任务处理方法就行了。

完整代码

Makefile

ThreadPool:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -rf mycond

Task.hpp

#include <string>

std::string opers = "+-*/%";

enum
{
    DivZero = 1,
    ModZero,
    Unknown
};

template <class T>
class Task
{
public:
    Task()
    {}
    
    Task(int x, int y, char op)
        : _data1(x), _data2(y), _op(op), _result(0), _exitcode(0)
    {}

    // run起来
    void operator()()
    {
        switch (_op)
        {
        case '+':
            _result = _data1 + _data2;
            break;
        case '-':
            _result = _data1 - _data2;
            break;
        case '*':
            _result = _data1 * _data2;
            break;
        case '/':
            {
                if (_data2 == 0) _exitcode = DivZero;
                else _result = _data1 / _data2;
            } 
            break;
        case '%':
            {
                if (_data2 == 0)
                    _exitcode = ModZero;
                else
                    _result = _data1 % _data2;
            }
            break;
        default:
            _exitcode = Unknown;
            break;
        }
    }

    std::string GetResult()
    {
        std::string result = std::to_string(_data1) + _op + std::to_string(_data2) + '=' + std::to_string(_result);
        result += "[code: " + std::to_string(_exitcode) + ']';
        return result;
    }

    std::string GetTask()
    {
        std::string result = std::to_string(_data1) + _op + std::to_string(_data2) + "= ?";
        return result;
    }
    
private:
    T _data1;
    T _data2;
    char _op;

    T _result;
    int _exitcode;
};

ThreadPool.hpp

#pragma once
#include <vector>
#include <queue>
#include <iostream>
#include <pthread.h>

// 线程信息
struct ThreadInfo
{
    pthread_t tid;    // 线程tid
    std::string name; // 线程名字
};

// 线程池
template <class T>
class ThreadPool
{
    static const int defaultnum = 5;

public:
    void Lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void Unlock()   
    {
        pthread_mutex_unlock(&_mutex);
    }
    void Wakeup()
    {
        // 唤醒线程执行任务
        pthread_cond_signal(&_cond);
    }
    void ThreadSleep()
    {
        // 在条件变量下等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    bool IsQueueEmpty()
    {
        return _tasks.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        // 获取线程name
        for (const auto &e : _threads)
        {
            if (e.tid == tid)
                return e.name;
        }
        return "None";
    }

public:
    ThreadPool(int num = defaultnum)
        : _threads(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    static void *HandlerTask(void *args)
    {
        // 获取this指针
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        // 获取线程名字,方便后续打印观察
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            // 1. 加锁
            tp->Lock();

            // 2. 判断是否等待排队(while 防止伪唤醒)
            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            // 3. 获取任务
            T t = tp->pop();
            // 4. 解锁
            tp->Unlock();

            // 5. 运行任务
            t();
            std::cout << name << " run, result: " << t.GetResult() << std::endl;
        }
        sleep(1);
    }

    // 线程池启动,创建线程,执行任务
    void start()
    {
        int num = _threads.size();
        for (int i = 0; i < num; i++)
        {
            _threads[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(_threads[i].tid), nullptr, HandlerTask, this);
            // 传递this指针
            // 1. 如果HandlerTask是普通成员函数,形参抹油有this指针,那么线程创建时还需要额外传递this指针
            // 2. 将HandlerTask改为静态成员函数,可是函数内部需要使用成员函数和成员变量,所以crete时的参数应传递this指针
            // 3. 所以将Lock等函数访问权限设为public
        }
    }

    // 外部push任务
    void push(const T &t)
    {
        // 1. 上锁
        Lock();
        // 2. push
        _tasks.push(t);
        // 3. 唤醒线程执行任务
        Wakeup();
        // 4. 解锁
        Unlock();
    }

    // 在HandlerTask内部调用它时已经加锁了
    T pop()
    {
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    ~ThreadPool()
    {
        // 销毁锁、条件变量
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<ThreadInfo> _threads; // 多线程容器,记录各个线程信息
    std::queue<T> _tasks;             // 线程从任务队列拿任务

    pthread_mutex_t _mutex; // 互斥锁
    pthread_cond_t _cond;   // 条件变量
};

Main.cc 

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "ThreadPool.hpp"
#include "Task.hpp"

int main()
{
    srand(time(nullptr));

    ThreadPool<Task<int>>* tp = new ThreadPool<Task<int>>(5);
    tp->start();
    // 因为此时已经创建线程,开始执行HandlerTask,所以五个线程申请锁,然后在条件变量下等待(释放锁),排队顺序为1 2 3 4 5
    // 当push任务后,1被唤醒,执行任务,再次申请锁,因为push很慢,所以又在条件变量下释放锁等待,排队顺序为2 3 4 5 1
    while (true)
    {
        // 1. 构建任务
        int data1 = rand() % 10 + 1;
        usleep(10);
        int data2 = rand() % 5;
        char op = opers[rand() % opers.size()];
        Task<int> t(data1, data2, op);
        
        // 2. 交给线程池处理
        tp->push(t);
        std::cout << "main thread make task: " << t.GetTask() << std::endl;
        sleep(1);
    }
    return 0;
}

二、简单封装原生线程库

类似C++的线程库,实际上C++的线程库就是对原生线程库的封装,我们也来简单封装一下原生线程库

类似C++的线程库

#pragma once

#include <iostream>
#include <pthread.h>
#include <ctime>
#include <string>
// #include <functional>
// using fun = std::function<void()>;

typedef void (*callback_t)(int);

static int num = 1;

template<class T>
class Thread
{
public:
    static void* Routine(void* args)
    {
        Thread* td = static_cast<Thread*>(args);
        td->Entery();
        return nullptr;
    } 
public:
    Thread(callback_t cb, int data)
        :_tid(0), _name(""), _start_timestamp(0), _isrunning(false), _cb(cb), _data(data)
    {}

    void Run()
    {
        _name = "thread-" + std::to_string(num++);
        _start_timestamp = time(nullptr);
        _isrunning = true;
        pthread_create(&_tid, nullptr, Routine, this);
    }

    void join()
    {
        pthread_join(_tid, nullptr);
    }

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

    uint64_t StartTimestamp()
    {
        return _start_timestamp;
    }

    bool IsRunning()
    {
        return _isrunning;
    }

    void Entery()
    {
        _cb(_data);
    }

    ~Thread()
    {}
private:
    pthread_t _tid;              // 线程tid
    std::string _name;           // 线程名字
    uint64_t _start_timestamp;   // 时间戳
    bool _isrunning;             // 是否运行
    callback_t _cb;              // 用户传递执行任务
    T _data;                      // 用户传递函数的参数
};

相关细节:

  • 将线程封装,我们首先要对线程进行描述,线程要有tid、线程名、是否在运行、运行多长时间等信息,可以自行添加
  • 类的构造函数只是要初始化列表初始化成员变量,线程的创建在run成员函数内部
  • 类内封装线程的create函数时,与线程池一样,我们需要将Routine线程执行函数改为静态成员函数,并在create函数向Routine传递本对象的this指针
  • 使用过C++的线程库的同学都知道,在使用时外部可以传递函数,那么我们实现时就根据 typedef 的函数指针类型,指定用户传递的函数的类型,如果函数带参,那么 Thread 类要加上模板、成员变量(用来接收用户传递的参数),然后在Entery成员函数内调用回调函数,将成员变量传进去 ,所以在Routine函数内部执行的是用户传递的函数

 在Main.cc中,如果传递的函数带参,那么需要加上模板

Main.cc

一次创建单个线程 

#include <iostream>
#include "Thread.hpp"

void Print(int cnt)
{
    for (int i = 0; i < cnt; i++)
        std::cout << "hello world!" << std::endl;
}

int main()
{
    Thread<int> t(Print, 5);
    t.Run();

    t.join();
    return 0;
}
  • 调用我们封装的库,需要手动调用Run函数、join等待线程 

 一次创建多个线程

#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"

void Print(int cnt)
{
    for (int i = 0; i < cnt; i++)
        std::cout << "hello world!" << std::endl;
    std::cout << "一次结束" << std::endl;
}

int main()
{
    std::vector<Thread<int>> threads;
    for (int i = 0; i < 10; i++)
    {
        threads.push_back(Thread<int>(Print, 5));
    }

    for (auto& e : threads)
    {
        e.Run();
    }
    for (auto& e : threads)
    {
        e.join();
    }

    return 0;
}

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

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

相关文章

贪吃蛇游戏(代码篇)

我们并不是为了满足别人的期待而活着。 前言 这是我自己做的第五个小项目---贪吃蛇游戏&#xff08;代码篇&#xff09;。后期我会继续制作其他小项目并开源至博客上。 上一小项目是贪吃蛇游戏&#xff08;必备知识篇&#xff09;&#xff0c;没看过的同学可以去看看&#xf…

使用Java API访问Apache Kafka

简介 Kafka是由Apache软件基金会开发的一个开源流处理平台,Kafka是一个功能强大且灵活的平台。 基本情况 软件名称:Apache Kafka 软件平台:跨平台 软件语言:Scala、Java 开发商:Apache软件基金会 软件授权:Apache License 2.0 最近更新时间:2024年7月23日 核心概念 -…

Ubuntu:用户不在sudoers文件中

1、问题 执行sudo xxx命令时&#xff0c;显示&#xff1a; user 不在sudoers文件中 需要查看系统版本进入恢复模式修复。 2、重启进入恢复模式 查看系统命令&#xff1a;uname -r 可能显示为&#xff1a;6.8.0-45-generic 重启Ubuntu系统&#xff0c;在开机时按ESC进入模…

oracle归档日志爆满问题处理

最近客户单位的oracle数据库出了问题&#xff0c;经常出现无法连接,报错提示 ORA-00257: archiver error, Connect internal only, until freed.&#xff0c;手动清除归档日志后可以恢复访问&#xff0c;但是过不了几天依旧会爆满&#xff0c;每日生成的归档日志很大。经过详细…

内部排序算法小结

练习目标&#xff1a; 1、实现 直接插入排序、冒泡排序、SHELL排序和快速排序&#xff1b; 2、随机生成100组数据&#xff0c;每组数据1000个元素。 注意&#xff1a;计时的单位是CPU的clock而非时间&#xff01;&#xff01;&#xff01; 【后续】 1、加入选择排序&#xff1b…

读书笔记《PPT演讲力》大树模型

作者把PPT演讲比作一棵大树&#xff0c;树的每一部分对应着PPT演讲的一个技巧。 根据这个大树模型&#xff0c;是否有联想到自己过往的演讲经历&#xff1f;演讲是否都达到了大树模型中说的效果&#xff1f;根据这个思维导图&#xff0c;结合自己的经历&#xff0c;试着总结3句…

云计算第四阶段-----CLOUND二周目 04-06

cloud 04 今日目标&#xff1a; 一、Pod 生命周期 图解&#xff1a; [rootmaster ~]# vim web1.yaml --- kind: Pod apiVersion: v1 metadata:name: web1 spec:initContainers: # 定义初始化任务- name: task1 # 如果初始化任务失败&#…

数字化与数智化,你知道它们的区别吗?

​其实老早就想说这个。中间一直在忙忙忙&#xff0c;还有处理自己的事情&#xff0c;导致拖更了。 最近听说一个物流大佬现在也转行做数字化厂家负责人&#xff0c;顺便给我讲解了这二者的区别&#xff0c;这里我就重新梳理了一下&#xff0c;加上了我自己的一些观点&#xf…

qt creator 开发环境的安装

1.找官网 官网地址&#xff1a;Installation | Qt Creator Documentation 点 Parent Directory 继续点 Parent Directory 点 archive/ 2.下载在线安装器 点 online_ainstallers 选择在线安装器版本 选择对应版本后进入下载列表&#xff0c;根据自己的系统选择下载。 下载后…

常用类(四)---String结构剖析

文章目录 1.String结构剖析2.String创建剖析3.String特性分析4.String方法总结5.StringBuffer和StringBuilder总结5.1stringbuffer对象的创建方法5.2string--stringbuffer之间的相互转换&#xff1a; 这个是我第二次学习string&#xff0c;听的是hsp老师的课程&#xff0c;我认…

IRP读写函数

驱动代码&#xff1a; #include <ntddk.h>#define DEVICE_NAME L"\\device\\MyDricer1" //设备对象名称 #define LINK_NAME L"\\dosdevices\\Goose" //符号链接名称VOID UnDirver(PDRIVER_OBJECT pDriverObj) {UNICODE_STRING uLinkName RTL_CONST…

OpenCV高级图形用户界面(6)获取指定窗口中图像的矩形区域函数getWindowImageRect()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 提供窗口中图像的矩形区域。 该函数 getWindowImageRect 返回图像渲染区域的客户端屏幕坐标、宽度和高度。 函数原型 Rect cv::getWindowImage…

旺店通ERP集成金蝶云星瀚(旺店通主供应链)

源系统成集云目标系统 金蝶云星瀚介绍 金蝶云星瀚是专为大企业设计的新一代数字化管理云服务、大型企业SaaS管理云&#xff0c;旨在凭借千亿级高性能和世界一流企业的实践&#xff0c;帮助大企业实现可信的数字化系统升迁&#xff0c;打造韧性企业&#xff0c;支撑商业创新和管…

C++:list(用法篇+模拟实现)

文章目录 前言一、list 的用法1. list 简介2. 用法代码演示1&#xff09;头/尾 插/删和迭代器遍历2&#xff09;insert与erase3&#xff09;排序sort相关4&#xff09;其他相关 二、list模拟实现1. 结点类模板list_node2. 定义迭代器1&#xff09;为什么要专门封装一个迭代器&a…

【uniapp】设置公共样式,实现公共背景等

目录 1、 全局渐变背景色 2.1 创建common目录 2.2 在common下新建style和images等目录 2.3 在style下新建common-style.scss 2.4 common-style输入全局渐变颜色 2.5 引入样式 2.6 业务页面引入 2.7 展示 2、全局字体颜色 2.1 新建base-style.scss文件 2.2 设置base-…

STM32的GPIO寄存器描述

寄存器&#xff1a; 软件控制硬件(在程序中操作对应控制器)&#xff0c;通过寄存器&#xff0c;就是 寄存器(可以存放数据)&#xff0c;但是其中的数据具有特定的硬件含义(查看芯片手册)&#xff0c;设置寄存器的值&#xff0c;对应的控制器就执行对应的工作。相当于寄存器就是…

IntelliJ IDEA中配置scala

1.IDEA中 配置 maven 左上角 file -> Setting 选择(或直接搜maven) Build, Execution,Deployment -> Build Toos -> Maven Maven home path 选择 maven 安装目录&#xff08;bin的上层目录&#xff09; 示例&#xff1a; D:\maven\apache-maven-3.8.6 User settings…

2024.10月11日--- SpringMVC拦截器

拦截器 1 回顾过滤器&#xff1a; Servlet规范中的三大接口&#xff1a;Servlet接口&#xff0c;Filter接口、Listener接口。 过滤器接口&#xff0c;是Servlet2.3版本以来&#xff0c;定义的一种小型的&#xff0c;可插拔的Web组件&#xff0c;可以用来拦截和处理Servlet容…

基于Springboot+Vue的租房管理系统 (含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统根…