[Linux] 最基础简单的线程池 及其 单例模式的实现

news2024/11/15 1:31:14

|cover


本篇文章主要用到线程相关内容, 下面是博主关于线程相关内容的文章:

[Linux] 线程同步分析:什么是条件变量?生产者消费者模型是什么?POSIX信号量怎么用?阻塞队列和环形队列模拟生产者消费者模型

[Linux] 线程互斥分析: 多线程的问题、互斥锁、C++封装使用互斥锁、线程安全分析、死锁分析…

[Linux] 如何理解线程ID?什么是线程局部存储?

[Linux] 多线程控制分析:获取线程ID、线程退出分析、自动回收线程、线程分离…

[Linux] 多线程概念相关分析: 什么是线程、再次理解进程、线程的创建与查看、线程异常、线程与进程的对比…


线程池

什么是线程池?

线程池一种线程使用模式. 我们知道, 线程的创建、调度、销毁都是需要消耗资源的. 也就是说 线程过多会带来调度开销, 进而影响缓存局部性和整体性能.

而线程池维护着多个线程, 这些线程等待着被分配可并发执行的任务. 这避免了在处理短时间任务时创建与销毁线程的代价.

说简单点, 就是 线程池维护着多个线程, 这些线程都可以随时被调度、随时被派发任务, 不用在任务需要派发时再创建线程, 而是在需要派发任务时 可以直接调度线程池内的线程, 执行任务

线程池的使用场景, 一般是 任务量巨大, 但是任务内容小的、任务时间短 的时候. 这样可以避免发生过多线程的创建与销毁. 或者 需要快速响应的任务, 因为不用再创建线程.

简单的固定线程数线程池

下面, 封装一个 简单的 拥有固定线程数量的线程池.

线程池维护着多个线程, 并不是说创建几个线程就可以了. 线程池还要管理这些线程的调度和执行, 整个实现类似一个变化的生产者消费者模型.

所以我们可以通过阻塞队列, 来实现对接收任务和同步调度线程.

threadPool.hpp:

#pragma once

#include <iostream>
#include <queue>
#include <cassert>
#include <pthread.h>
#include <unistd.h>

#define THREADNUM 5

template <class T>
class threadPool {
public:
    threadPool(size_t threadNum = THREADNUM)
        : _threadNum(threadNum)
        , _isStart(false) {
        assert(_threadNum > 0);

        pthread_mutex_init(&_mutex, nullptr); // 初始化 锁
        pthread_cond_init(&_cond, nullptr);   // 初始化 条件变量
    }
    
    // 线程回调函数
    // static 修饰, 是因为需要让函数参数 取消this指针, 只留一个void*
    // 但是由于 需要访问类内成员, 所以 传参需要传入this指针
    static void* threadRoutine(void* args) {
        // 线程执行回调函数
        // 先分离, 自动回收
        pthread_detach(pthread_self());

        // 获取this指针
        threadPool<T>* tP = static_cast<threadPool<T>*>(args);
        while (true) {
            // 即将通过任务队列给线程分配任务, 即 多线程访问临界资源, 需要上锁
            tP->lockQueue();
            while (!tP->haveTask()) {
                // 任务队列中没有任务, 就让线程通过条件变量等待
                tP->waitForTask();
            }
            // 走到这里 说明条件队列中有任务
            // 线程已经可以获取到任务
            T task = tP->popTask();
            // 获取到任务之后 临界资源的访问就结束了, 可以释放锁了.
            // 尽量避免拿着锁 执行任务
            tP->unlockQueue();

            // 为任务类提供一个运行的接口, 这样获取到任务之后 直接 task.run();
            task.run();
        }
    }

    // 开启线程池
    void start() {
        try {
            // _isStart 为true 则说明线程池已经开启
            if (_isStart)
                throw "Error: thread pool already exists";
        }
        catch (const char* e) {
            std::cout << e << std::endl;
            return;
        }

        for (int i = 0; i < _threadNum; i++) {
            pthread_t temp;
            pthread_create(&temp, nullptr, threadRoutine, this); // 回调函数的参数传入this指针, 用于类访问内成员
        }
        // 开启线程池之后, 要把 _isStart 属性设置为 true
        _isStart = true;
    }

    // 给任务队列添加任务 并分配任务
    void pushTask(const T& in) {
        // 上锁
        lockQueue();
        _taskQueue.push(in);
        // 任务队列中已经存在任务, 线程就不用再等待了, 就可以唤醒线程
        choiceThreadForHandler();
        // 释放锁
        unlockQueue();
    }

    ~threadPool() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

private:
    // 线程调度 即为从任务队列中给各线程分配任务
    // 所以 任务队列是临界资源需要上锁
    void lockQueue() {
        pthread_mutex_lock(&_mutex);
    }
    void unlockQueue() {
        pthread_mutex_unlock(&_mutex);
    }

    // 条件变量 使用条件, 判断是否任务队列是否存在任务
    bool haveTask() {
        return !_taskQueue.empty();
    }
    // 线程通过条件变量等待任务
    void waitForTask() {
        pthread_cond_wait(&_cond, &_mutex);
    }

    // 从任务队列中获取任务, 并返回
    T popTask() {
        T task = _taskQueue.front();
        _taskQueue.pop();

        return task;
    }

    // 唤醒在条件变量前等待的线程
    // 由于唤醒之后就是线程调度的过程
    // 所以函数名 是线程调度相关
    void choiceThreadForHandler() {
        pthread_cond_signal(&_cond);
    }

private:
    size_t _threadNum;        	// 线程池内线程数量
    bool _isStart;            	// 判断线程池是否已经开启
    std::queue<T> _taskQueue;	// 任务队列
    pthread_mutex_t _mutex;		// 锁 给临界资源使用 即任务队列 保证线程调度互斥
    pthread_cond_t _cond; 		// 条件变量 保证线程调度同步
};

这部分代码就是一个再简单不过的线程池.

这个最简单的线程池的功能就包括:

  1. 单次开启线程池, 创建多线程, 并让线程等待调度
  2. 可以获取任务并存储任务
  3. 由于通过多线程访问临界资源分配任务, 所以 要做到同步互斥地给线程分配任务
  4. 得到任务之后, 线程执行任务, 执行完成继续等待调度

所以, 成员变量至少要用到的成员变量有:

  1. size_t _threadNum, 用来设置线程池中线程的数量
  2. bool _isStart, 用来设置线程池开启状态
  3. std::queue<T> _taskQueue, 任务队列, 临界资源. 用来接收主线程发来的任务. 存储任务, 向线程分配
  4. pthread_mutex_t _mutex, 锁, 为保证多线程访问任务队列互斥, 且实现同步向线程分配任务
  5. pthread_cond_t _cond, 条件变量, 为实现无任务时 线程等待调度, 且实现同步向线程分配任务

整个线程池中, 最重要的就是多线程所执行的回调函数的实现.

此函数中, 包括线程等待, 分配任务, 执行任务的功能, 并且参数的传递也很重要:

// 线程回调函数
// static 修饰, 是因为需要让函数参数 取消this指针, 只留一个void*
// 但是由于 需要访问类内成员, 所以 传参需要传入this指针
static void* threadRoutine(void* args) {
    // 线程执行回调函数
    // 先分离, 自动回收
    pthread_detach(pthread_self());

    // 获取this指针
    threadPool<T>* tP = static_cast<threadPool<T>*>(args);
    while (true) {
        // 即将通过任务队列给线程分配任务, 即 多线程访问临界资源, 需要上锁
        tP->lockQueue();
        while (!tP->haveTask()) {
            // 任务队列中没有任务, 就让线程通过条件变量等待
            tP->waitForTask();
        }
        // 走到这里 说明条件队列中有任务, 线程已经可以获取任务
        T task = tP->popTask();
        // 获取到任务之后 临界资源的访问就结束了, 可以释放锁了.
        // 尽量避免拿着锁 执行任务
        tP->unlockQueue();

        // 为任务类提供一个运行的接口, 这样获取到任务之后 直接 task.run();
        task.run();
    }
}

// 类内创建线程时的操作
pthread_create(&temp, nullptr, threadRoutine, this);

我们知道, 线程需要执行的回调函数格式是这样的void* 函数名(void*)

但是, 类内的所有成员函数第一个参数是this指针. 所以我们需要将此函数用static修饰. 然而修饰之后, 此函数就不属于类内成员函数了, 所以无法直接调用访问类内成员. 所以, 参数需要传入类的this指针, 通过this指针访问对象成员.

所以, 此函数的首要的功能 除了分离线程之外, 就是要通过参数获取到调用对象的this指针

然后就要实现线程主要需要执行的功能:

首先, 线程需要 在没有任务时, 通过条件变量陷入等待. 而且, 线程在 执行完任务 时, 需要 重新在没有任务时, 通过条件变量陷入等待. 所以, 函数的主体功能是在 一个循环 内的.

|inline

进入循环后, 就应该 从任务队列中获取任务, 但是 如果任务队列中 没有任务, 线程就需要等待.

并且, 线程 无论是获取任务的过程 还是 判断是否有任务的过程, 访问的都是临界资源, 而 临界资源需要保证线程安全, 所以 在进入循环之后的 第一件事, 应该是 对临界资源上锁, 即 多线程争夺锁. 争夺到锁之后, 才能访问临界资源:

|inline

获取到任务之后, 就表示线程访问本次临界资源已经结束, 就可以释放锁, 然后执行任务了.

而, 除了线程执行的功能函数之外, 还有一个需要将任务放入任务队列的函数:

|inline

至此, 有关线程池的主要功能函数就是先完毕了.

我们可以再结合下面这几个代码文件 测试一下:

logMessage.hpp:

#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <cstdlib>

// 宏定义 四个日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char* log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};

// 实现一个 可以输出: 日志等级、日志时间、用户、以及相关日志内容的 日志消息打印接口
void logMessage(int level, const char* format, ...) {
    // 通过可变参数实现, 传入日志等级, 日志内容格式, 日志内容相关参数

    // 确保日志等级正确
    assert(level >= DEBUG);
    assert(level <= FATAL);

    // 获取当前用户名
    char* name = getenv("USER");

    // 简单的定义log缓冲区
    char logInfo[1024];

    // 定义一个指向可变参数列表的指针
    va_list ap;
    // 将 ap 指向可变参数列表中的第一个参数, 即 format 之后的第一个参数
    va_start(ap, format);

    // 此函数 会通过 ap 遍历可变参数列表, 然后根据 format 字符串指定的格式, 将ap当前指向的参数以字符串的形式 写入到logInfo缓冲区中
    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

    // ap 使用完之后, 再将 ap置空
    va_end(ap); // ap = NULL

    // 通过判断日志等级, 来选择是标准输出流还是标准错误流
    FILE* out = (level == FATAL) ? stderr : stdout;

    // 获取本地时间
    time_t tm = time(nullptr);
    struct tm* localTm = localtime(&tm);
    char* localTmStr = asctime(localTm);
    char* nC = strstr(localTmStr, "\n");
    if(nC) {
        *nC = '\0';
    }
    fprintf( out, "%s | %s | %s | %s\n", 
            log_level[level],
            localTmStr,
            name == nullptr ? "unknow" : name, 
            logInfo );
}

intArithmeticTask.hpp:

// 任务类
#pragma once

#include <iostream>
#include <map>
#include <string>
#include <functional>
#include <pthread.h>
#include "logMessage.hpp"

std::map<char, std::function<int(int, int)>> opFunctions{
    {'+', [](int elemOne, int elemTwo) { return elemOne + elemTwo; }},
    {'-', [](int elemOne, int elemTwo) { return elemOne - elemTwo; }},
    {'*', [](int elemOne, int elemTwo) { return elemOne * elemTwo; }},
    {'/', [](int elemOne, int elemTwo) {
         if (elemTwo == 0) {
             std::cout << "div zero, abort" << std::endl;
             return -1;
         }
         return elemOne / elemTwo;
     }},
    {'%', [](int elemOne, int elemTwo) {
         if (elemTwo == 0) {
             std::cout << "div zero, abort" << std::endl;
             return -1;
         }
         return elemOne % elemTwo;
     }}
};

class Task {
public:
    Task(int one = 0, int two = 0, char op = '0')
        : _elemOne(one)
        , _elemTwo(two)
        , _operator(op) {}

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

    void run() {
        int result = 0;
        if (opFunctions.find(_operator) != opFunctions.end()) {
            result = opFunctions[_operator](_elemOne, _elemTwo);
            if ((_elemTwo == 0 && _operator == '/') ||(_elemTwo == 0 && _operator == '%')) 
                return;
            logMessage(NOTICE, "新线程[%lu] 完成算术任务: %d %c %d = %d", pthread_self(), _elemOne, _operator, _elemTwo, result);
        }
        else {
            std::cout << "非法操作: " << _operator << std::endl;
        }
    }

    void get(int* e1, int* e2, char* op) {
        *e1 = _elemOne;
        *e2 = _elemTwo;
        *op = _operator;
    }

private:
    int _elemOne;
    int _elemTwo;
    char _operator;
};

threadPool.cc:

// 开启线程池, 任务派发主函数
#include <iostream>
#include <memory>
#include <ctime>
#include <cstdlib>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include "logMessage.hpp"
#include "threadPool.hpp"
#include "intArithmeticTask.hpp"

const std::string operators = {"+-*/\%"};

int main() {
    std::unique_ptr<threadPool<Task>> tP(new threadPool<Task>);
    // 开启线程池
    tP->start();

    srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self());
    while (true) {
        int elemOne = rand()%20;
        int elemTwo = rand()%10;
        char oper = operators[rand()%operators.size()];

        logMessage(NOTICE, "主线程[%lu] 派发算术任务: %d %c %d = ?", pthread_self(), elemOne, oper, elemTwo);
        Task taskTmp(elemOne, elemTwo, oper);
        tP->pushTask(taskTmp);

        // 设置为1s添加 分配一个任务
        sleep(1);
    }
    
    return 0;
}

然后, 编译运行:

可以看到运行的结果就是 我们期望的结果, 主线程每秒添加并分配一个, 5个线程同步获取到任务并执行.

我们还可以将任务的处理速度设置慢一些, 任务的添加分配速度快一些, 更明显的看到多线程的并发

将 处理速度设置为1s, 添加分配速度设置为0.1s:

请添加图片描述

当派发速度变快 处理速度变慢, 之间超过5倍差的时候:

|inline

懒汉单例模式线程池

单例模式, 是指 只能创建一个实例对象的类

懒汉式的单例模式, 是指 在使用时才实例化单例对象的单例模式.

我们可以将 这个线程池 修改为单例模式:

lock.hpp:

// 一个 RAII思想实现的锁
#pragma once

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

class Mutex {
public:
    Mutex() {
        pthread_mutex_init(&_lock, nullptr);
    }
    void lock() {
        pthread_mutex_lock(&_lock);
    }
    void unlock() {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex() {
        pthread_mutex_destroy(&_lock);
    }

private:
    pthread_mutex_t _lock;
};

class LockGuard {
public:
    LockGuard(Mutex* mutex)
        : _mutex(mutex) {
        _mutex->lock();
        std::cout << "加锁成功..." << std::endl;
    }

    ~LockGuard() {
        _mutex->unlock();
        std::cout << "解锁成功...." << std::endl;
    }

private:
    Mutex* _mutex;
};

threadPool.hpp:

// 单例模式的线程池
#pragma once

#include <cstddef>
#include <iostream>
#include <ostream>
#include <queue>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include "lock.hpp"

#define THREADNUM 5

template <class T>
class threadPool {
public:
    static threadPool<T>* getInstance() {
        // RAII锁
        static Mutex mutex;
        if (_instance == nullptr) {
            LockGuard lockG(&mutex);
            if (_instance == nullptr) {
                _instance = new threadPool<T>();
            }
        }

        return _instance;
    }
    // 线程回调函数
    // static 修饰, 是因为需要让函数参数 取消this指针, 只留一个void*
    // 但是由于 需要访问类内成员, 所以 传参需要传入this指针
    static void* threadRoutine(void* args) {
        // 线程执行回调函数
        // 先分离, 自动回收
        pthread_detach(pthread_self());

        // 获取this指针
        threadPool<T>* tP = static_cast<threadPool<T>*>(args);
        while (true) {
            // 即将通过任务队列给线程分配任务, 即 多线程访问临界资源, 需要上锁
            tP->lockQueue();
            while (!tP->haveTask()) {
                // 任务队列中没有任务, 就让线程通过条件变量等待
                tP->waitForTask();
            }
            // 走到这里 说明条件队列中有任务
            // 线程已经可以获取到任务
            T task = tP->popTask();
            // 获取到任务之后 临界资源的访问就结束了, 可以释放锁了.
            // 尽量避免拿着锁 执行任务
            tP->unlockQueue();

            // 为任务类提供一个运行的接口, 这样获取到任务之后 直接 task.run();
            task.run();
        }
    }

    // 开启线程池
    void start() {
        try {
            // _isStart 为true 则说明线程池已经开启
            if (_isStart)
                throw "Error: thread pool already exists";
        }
        catch (const char* e) {
            std::cout << e << std::endl;
            return;
        }

        for (int i = 0; i < _threadNum; i++) {
            pthread_t temp;
            pthread_create(
                &temp, nullptr, threadRoutine,
                this); // 回调函数的参数传入this指针, 用于类访问内成员
        }
        // 开启线程池之后, 要把 _isStart 属性设置为 true
        _isStart = true;
    }

    // 给任务队列添加任务 并分配任务
    void pushTask(const T& in) {
        // 上锁
        lockQueue();
        _taskQueue.push(in);
        // 任务队列中已经存在任务, 线程就不用再等待了, 就可以唤醒线程
        choiceThreadForHandler();
        // 释放锁
        unlockQueue();
    }

    ~threadPool() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

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

private:
    threadPool(size_t threadNum = THREADNUM)
        : _threadNum(threadNum)
        , _isStart(false) {
        assert(_threadNum > 0);

        pthread_mutex_init(&_mutex, nullptr); // 初始化 锁
        pthread_cond_init(&_cond, nullptr);   // 初始化 条件变量
    }
    // 线程调度 即为从任务队列中给各线程分配任务
    // 所以 任务队列是临界资源需要上锁
    void lockQueue() {
        pthread_mutex_lock(&_mutex);
    }
    void unlockQueue() {
        pthread_mutex_unlock(&_mutex);
    }

    // 条件变量 使用条件, 判断是否任务队列是否存在任务
    bool haveTask() {
        return !_taskQueue.empty();
    }
    // 线程通过条件变量等待任务
    void waitForTask() {
        pthread_cond_wait(&_cond, &_mutex);
    }

    // 从任务队列中获取任务, 并返回
    T popTask() {
        T task = _taskQueue.front();
        _taskQueue.pop();

        return task;
    }

    // 唤醒在条件变量前等待的线程
    // 由于唤醒之后就是线程调度的过程
    // 所以函数名 是线程调度相关
    void choiceThreadForHandler() {
        pthread_cond_signal(&_cond);
    }

private:
    size_t _threadNum;        // 线程池内线程数量
    bool _isStart;            // 判断线程池是否已经开启
    std::queue<T> _taskQueue; // 任务队列
    pthread_mutex_t _mutex; // 锁 给临界资源使用 即任务队列 保证线程调度互斥
    pthread_cond_t _cond; // 条件变量 保证线程调度同步

    static threadPool<T>* _instance;
};

template <class T>
threadPool<T>* threadPool<T>::_instance = nullptr;

运行结果:

请添加图片描述

执行效果是没有区别的.


感谢阅读~

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

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

相关文章

Python接口自动化测试--requests高级进阶

Cookies与会话对象 如果某个响应中包含一些Cookie&#xff0c;你可以快速访问它们&#xff1a; import requests r requests.get(http://www.google.com.hk/) print(r.cookies[NID]) print(tuple(r.cookies)) 要想发送你的cookies到服务器&#xff0c;可以使用 cookies 参…

xShell中使用vim编辑时,无法粘贴外来文本

鼠标右键弹出菜单时&#xff0c;vim直接变成了视图模式了&#xff0c;不能粘贴了。。 好&#xff0c;执行命令 vim ~/.vimrc输入: set mousec即可。 此时便可以粘贴了。

「观察者(Observer)」设计模式 Swift实现

这里写目录标题 介绍设计模式介绍举例 iOS 中已有的 观察者设计模式实现Notification什么是通知机制或者说如何实现通知机制&#xff1f; KVOKVO底层实现如何实现手动KVO&#xff1f; 介绍 设计模式介绍 观察者设计模式&#xff08;Observer Pattern&#xff09;是一种行为型…

win10电脑出现网络问题时,如何解决?

我们的Windows可能会出现各种网络连接问题&#xff1a; 尝试连接Wi-Fi网络时出现错误&#xff1a;Windows无法连接到此网络&#xff1b;您可以通过Wifi访问互联网&#xff0c;但通过电缆访问以太网却无法正常工作&#xff1b;尝试通过电缆连接互联网时出现错误&#xff1a; Wi…

图数据库:neo4j学习笔记

参考资料&#xff1a;neo4j 教程_w3cschool Springboot集成Neo4j_喝醉的咕咕鸟的博客-CSDN博客 SpringBoot 整合 Neo4j_springboot neo4j_$懒小猿$的博客-CSDN博客 图数据库Neo4j实战&#xff08;全网最详细教程&#xff09;_neo4j使用教程_星川皆无恙的博客-CSDN博客 代码片段…

Flink DataStream之输出数据到File中

新建类 package test01;import org.apache.flink.api.common.serialization.SimpleStringEncoder; import org.apache.flink.configuration.Configuration; import org.apache.flink.configuration.MemorySize; import org.apache.flink.connector.file.sink.FileSink; import…

Model, ViewModel, EnvironmentObject 的使用

1. Model 数据模型的定义与使用 1.1 案例 struct UserModel: Identifiable{let id: String UUID().uuidStringlet dispalyName: Stringlet userName: Stringlet followerCount: Intlet isVerified: Bool }/// 数据模型 struct ModelBootcamp: View {State var users:[Use…

web 禁用 OPTIONS方法启用【原理扫描】

Web服务器上启用了HTTP OPTIONS方法。 OPTIONS方法提供了Web服务器支持的方法列表&#xff0c;它表示对有关由Request-URI标识的请求/响应链上可用的通信选项的信息的请求。 直接在IIS上进行关闭即可&#xff1a;

osg osgDB::readImageFile 返回空指针 解决中

在 osg功能开发中,需要用到 纹理 加载图片&#xff0c;最神奇的之前 好好的。 现在 把osg 编译成了 osg 342vs2013x86 环境 就出现幺蛾子了&#xff0c;之前是使用的 osg364vs2013x86。结果 命令行运行 加载图片 直接 有 warning 提示。还在处理中&#xff01; 提示 找不到文…

Nginx 的Nacos配置

进入nginx 配置目录 cd /usr/local/nginx/conf 2. 编辑nginx配置文件 vi nginx.conf 3. 增加对Nacos 的代理 upstream nacosServerList {server 192.168.172.102:8848;server 192.168.172.103:8848;server 192.168.172.104:8848; } # Nacos地址服务器寻址配置 server {#监听端…

微信小程序第六节——个体账号如何实现用户自定义内容

&#x1f4cc; 微信小程序第一节 ——自定义顶部、底部导航栏及获取胶囊位置信息。 &#x1f4cc; 微信小程序第二节 —— 微信小程序第二节 —— 自定义组件。 &#x1f4cc; 微信小程序第三节 —— 页面跳转的那些事儿。 &#x1f4cc; 微信小程序第四节 —— 网络请求那些事…

matlab GUI入门

matlab GUI入门 两种方法 法一&#xff1a;使用guide 法二&#xff1a;使用appdesigner&#xff08;推荐&#xff0c;更直观&#xff09; winopen(cd) 打开当前路径。 ctrlI 代码自动对齐 matlab 导入数据文件 导入图片数据 用imread&#xff08;&#xff09;函数导入…

第 3 章 Spark 通讯架构

3.1 Spark 通信架构概述 3.1 Spark 通信架构概述 Spark 中通信框架的发展&#xff1a; ➢ Spark 早期版本中采用 Akka 作为内部通信部件。 ➢ Spark1.3 中引入 Netty 通信框架&#xff0c;为了解决 Shuffle 的大数据传输问题使用 ➢ Spark1.6 中 Akka 和 Netty 可以配置使用。…

bug的合规描述

bug的合格描述&#xff1a; 发现问题的版本bug的合格描述&#xff1a; 开发人员需要知道出现问题的版本&#xff0c;才能够获取对应版本的代码来重现故障问题出现的环境 环境分为硬件环境和软件环境&#xff0c;详细的环境描述有利于故障的重现(如果是web项目&#xff0c;需…

到手价的监测要求和方法

品牌在做电商价格监测时&#xff0c;为什么要对到手价进行监测&#xff0c;这其中的原因还是很显现的&#xff0c;各平台的促销信息众多&#xff0c;如果只监测页面价的低价行为&#xff0c;那将有非常多的低价链接不会被发现&#xff0c;而这也会导致品牌做渠道管控时失去公平…

如何随机切换代理IP以避免被封禁?

在网络爬虫和数据抓取的领域&#xff0c;使用代理IP技术是非常常见的做法。使用代理IP可以有效地绕过网站的访问限制&#xff0c;提高访问速度和稳定性。然而&#xff0c;如果我们在访问网站时只使用一个代理IP&#xff0c;那么可能会被网站封禁&#xff0c;从而导致访问失败。…

开源预训练框架 MMPRETRAIN官方文档(高级指南)

1、准备数据集 1、自定义数据集&#xff08;下面都是分类数据的自定义数据集准备&#xff09; CustomDataset是一个通用数据集类&#xff0c;供您使用自己的数据集。要使用CustomDataset&#xff0c;您需要根据以下两种格式组织数据集文件&#xff1a; 1、子文件夹格式 在这…

正则表达式概念以及语法的使用

目录 1.概念 2. 为什么使用正则表达式&#xff1f; 3. 语法 1.普通字符 非打印字符 2. 特殊字符 3. 限定符 4. 定位符 5. 运算优先级 3.匹配规则 1. 基本模式匹配 2. 字符簇 3. 确定重复出现 1.概念 正则表达式(Regular Expression)是一种文本模式&#xff0c;包…

MAYA挖掘机绑定

打组 少选一个 放中心点 把它放组里 放中心点 创建骨骼 放骨骼 旋转不会带动上面骨骼 中心点的位置 骨骼和组做约束 活塞运行 放中心点 相互目标 管子短&#xff0c;需要加长 又短了 设置中心点 创建IK 制作控制器 让控制器带动模型动 手柄 IK 少一个控制器 删除 不用的…

途乐证券杠杆开户-A股首份半年报出炉 康缘药业净利同比增30.6%

中药职业迎成绩兑现期&#xff0c;多家公司上半年盈利估计倍增 7月12日晚&#xff0c;沪深两市首份半年报出炉。康缘药业半年报显示&#xff0c;公司上半年完成营收25.53亿元&#xff0c;同比添加21.74%&#xff1b;完成归母净利润2.76亿元&#xff0c;同比添加30.6%。 康缘药…