【C++ 11多线程加速计算实操教程】

news2024/9/24 5:32:48

【C++ 11多线程加速计算实操教程】

  • 1. 了解线程的基本概念
  • 2. 创建线程
    • 2.1 启动线程的基本示例:
    • 2.2 运行结果
  • 3. 线程加速计算
    • 3.1 演示如何使用多个线程计算数组的和:
    • 3.2 运行结果
    • 3.3 结果分析
    • 3.4 拓展学习
  • 4. 互斥量(Mutex)
    • 4.1 演示如何使用互斥量来保护共享变量:
    • 4.2 运行结果
    • 4.3 结果分析
  • 5. 传递参数
    • 5.1 线程参数传递
    • 5.2 运行结果
  • 6. 线程池
    • 6.1 线程池的基本功能
    • 6.2. 线程池的简单实现
    • 6.3 运行结果
    • 6.4 输出结果分析
  • 6. 总结

学习 C++ 多线程编程是一个非常重要和实用的领域,尤其是在现代软件开发中。以下是有关如何从基础逐步学习 C++ 线程的详细步骤和建议。从创建线程,线程加速计算,互斥量,参数传递和线程池管理循序渐进,接下来开始第一步了解线程。
环境搭建参考:

  1. 【Qt安装与简易串口控制Arduino开发板小灯教程】
  2. 【VS2019安装+QT配置】

此教程采用Fitten code插件交互生成的,大家都快来试试吧!😘😘😘https://codewebchat.fittenlab.cn/?share=2024923_47cpt62du

1. 了解线程的基本概念

在开始多线程编程之前,了解以下基本概念是必要的:

  • 线程是什么:线程是系统能够独立运行的最小单位,一个进程可以包含多个线程。线程共享进程的资源,如内存和文件句柄。
  • 优点:多线程可以提高程序的执行效率,特别是在多核处理器上,可以并行处理多个任务。
  • 缺点:线程间的共享资源可能导致数据竞争和死锁等问题,编程复杂性增加。

2. 创建线程

在 C++11 及以后的版本中,可以使用标准库中的 std::thread 类来创建和管理线程。以下是创建和

2.1 启动线程的基本示例:

#include <iostream>
#include <thread>

void threadFunction() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    // 创建线程
    std::thread t(threadFunction);
    
    // 等待线程完成
    t.join();
    
    std::cout << "Thread has finished execution." << std::endl;
    return 0;
}

2.2 运行结果

在这里插入图片描述
在这个示例中,函数 threadFunction 被作为线程执行的任务,然后在主线程中调用 join(),用于等待线程执行完毕。

3. 线程加速计算

线程可以用来加速计算,例如,将一个大的计算任务分解成多个子任务,分别在不同的线程中执行。下面是一个简单的示例,

3.1 演示如何使用多个线程计算数组的和:

#include <iostream>
#include <vector>
#include <thread>
#include <chrono> // 用于计时

// 使用多线程计算和的函数
void sum(const std::vector<int>& numbers, unsigned long& result, size_t start, size_t end) {
    long long localResult = 0; // 使用局部变量来减少主线程中的地址频繁变化
    for (size_t i = start; i < end; ++i) {
        localResult += numbers[i];
    }
    result = localResult; // 将局部结果赋值给结果引用
}

// 不使用线程计算和的函数
unsigned long sumSerial(const std::vector<int>& numbers) {
    unsigned long result = 0;
    for (size_t i = 0; i < numbers.size(); ++i) {
        result += numbers[i];
    }
    return result;
}

int main() {
    const size_t arraySize = 40000000; // 扩大数组大小
    const int numThreads = 4; // 线程数量
    std::vector<int> numbers(arraySize, 9); // 初始化一个包含40000000个9的数组
    std::vector<std::thread> threads(numThreads);
    std::vector<unsigned long> results(numThreads, 0); // 存储每个线程的结果

    // 使用多线程计算
    auto startTime = std::chrono::high_resolution_clock::now(); // 记录开始时间

    // 批量生成线程
    for (int i = 0; i < numThreads; ++i) {
        size_t start = i * (arraySize / numThreads);
        size_t end = (i + 1) * (arraySize / numThreads);
        threads[i] = std::thread(sum, std::ref(numbers), std::ref(results[i]), start, end);
    }

    // 等待所有线程完成
    for (int i = 0; i < numThreads; ++i) {
        threads[i].join();
    }

    auto endTime = std::chrono::high_resolution_clock::now(); // 记录结束时间
    std::chrono::duration<double> elapsedSeconds = endTime - startTime; // 计算花费的时间

    unsigned long totalParallel = 0;
    for (const auto& result : results) {
        totalParallel += result; // 合并所有线程的结果
    }
    
    std::cout << "Total sum (multithreading): " << totalParallel << std::endl;
    std::cout << "Time consumed (multithreading): " << elapsedSeconds.count() << " seconds" << std::endl;

    // 不使用线程计算
    startTime = std::chrono::high_resolution_clock::now(); // 记录开始时间
    unsigned long totalSerial = sumSerial(numbers);
    endTime = std::chrono::high_resolution_clock::now(); // 记录结束时间
    elapsedSeconds = endTime - startTime; // 计算花费的时间
    
    std::cout << "Total sum (single-thread): " << totalSerial << std::endl;
    std::cout << "Time consumed (single-thread): " << elapsedSeconds.count() << " seconds" << std::endl;

    return 0;
}

代码说明

  • sumSerial 函数:这个函数用于不使用多线程的情况下计算数组元素的和。它简单地遍历整个数组,依次相加。
  • 计时功能:在计算和的地方,使用 std::chrono 库来记录开始和结束时间,以便测量每种方法的执行时间。
  • 主函数:首先进行多线程计算,并记录执行时间。然后进行单线程计算,并同样记录执行时间。最后输出两次计算的结果和所花费的时间。

3.2 运行结果

在这里插入图片描述
运行效果
执行这段代码后,您将能看到多线程计算与单线程计算的总和以及各自所花费的时间。这有助于直观地比较性能,观察多线程带来的加速效果。加速后比加速前快了4倍

3.3 结果分析

  • 亮点设计:通过局部变量localResult代替全局变量result频繁变化。

  • 批量生成线程:使用一个循环来创建多个线程,每个线程负责计算数组的一部分,计算的起始与结束位置由 start 和 end 变量确定。

  • 结果存储:在一个 results 向量中存储每个线程的计算结果,便于最后进行总和操作。

  • 清晰简洁:通过这种方式,若需更改线程数量,只需更改 numThreads 常量的值,代码就会自动适应。

3.4 拓展学习

运行这个程序后,您将能够生成一个大的随机数组,并通过多线程与单线程的方法分别求和,输出结果和时间消耗。

#include <iostream>
#include <vector>
#include <thread>
#include <cstdlib>
#include <ctime>

// 定义随机数组的行和列
#define ROW   5
#define COL 10000000

// 获得随机数组
int** getRandom() {
    std::cout << "生成" << ROW << "X" << COL << "的随机数组" << std::endl;
    int** r;
    r = (int**)malloc(ROW * sizeof(int*));
    for (int i = 0; i < ROW; i++) {
        r[i] = (int*)malloc(COL * sizeof(int));
    }
    srand(static_cast<unsigned>(time(NULL)));
    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; ++j) {
            r[i][j] = rand();
        }
    }
    std::cout << "随机数组生成完毕\n\n";
    return r;
}

// 定义传入线程的结构体
struct TagValue {
    int* arr;
    long long sum;
};

// 子线程调用求和方法
void threadSum(TagValue* v1) {
    // 子线程的求和方法
    TagValue *v = (TagValue*)v1;
    int* arr = v->arr;
    long long sum = 0;
    for (int i = 0; i < COL; i++) {
        sum += *(arr + i);
    }
    v->sum = sum;
}

// 多线程主方法
void testThreadTime(int** r) {
    // 计算多线程运行时间
    std::vector<TagValue> res(ROW);
    std::vector<std::thread> ths(ROW);
    
    auto start = std::chrono::high_resolution_clock::now();
    
    for (int i = 0; i < ROW; i++) {
        res[i].arr = r[i];
        res[i].sum = 0;

        // 开启子线程
        ths[i] = std::thread(threadSum, &res[i]);
        std::cout << "子线程" << i << "创建成功" << std::endl;
    }

    // 等待所有线程完成
    for (auto& th : ths) {
        th.join();
    }

    // 打印返回值
    std::cout << "多线程的执行结果是:" << std::endl;
    for (int i = 0; i < ROW; i++) {
        std::cout << res[i].sum << std::endl;
    }
    std::cout << "主线程执行完毕" << std::endl;

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed = end - start;
    std::cout << "Time Used: " << elapsed.count() << " seconds" << std::endl;
}

// 非多线程对照方法
void testNoThreadTime(int** r) {
    auto start = std::chrono::high_resolution_clock::now();
    long long res[ROW] = {0};

    for (int i = 0; i < ROW; i++) {
        for (int j = 0; j < COL; j++) {
            res[i] += r[i][j];
        }
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "对照组执行结果是:" << std::endl;
    for (int i = 0; i < ROW; i++) {
        std::cout << res[i] << std::endl;
    }
    std::chrono::duration<double> elapsed = end - start;
    std::cout << "Time Used: " << elapsed.count() << " seconds" << std::endl;
}

int main() {
    int** r = getRandom();
    testThreadTime(r);
    testNoThreadTime(r);
    getchar();
    
    // 释放动态分配的内存
    for (int i = 0; i < ROW; i++) {
        free(r[i]);
    }
    free(r);
    
    return 0;
}

  • 使用 std::thread:在这个修改后的版本中,生成线程的部分使用 std::thread 来创建线程并启动,threadSum 函数,每个线程的参数通过指向 TagValue 结构体的指针传递。
  • 时钟的使用:使用 std::chrono::high_resolution_clock 来测量执行时间,使用 std::chrono::duration 计算时间差。
  • 内存管理:在 main 函数末尾,释放通过 malloc 动态分配的内存,以避免内存泄漏。
  • 输出信息:在控制台输出生成随机数组和各个线程的执行结果,提供清晰的输出格式。

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

在C/C++的多线程使用过程中,一定要注意在子线程中对传入地址的写操作。频繁的跨线程写操作,会带来效率的大幅降低。

4. 互斥量(Mutex)

互斥量(std::mutex)是 C++ 中用于保护共享资源的一种同步机制,确保在任何时刻只有一个线程可以访问这些共享资源。这可以帮助避免数据竞争问题。

示例:使用互斥量防止数据竞争
以下是一个示例,

4.1 演示如何使用互斥量来保护共享变量:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex> // 包含互斥量库
#include <chrono>

unsigned long sharedCounterNoMutex = 0; // 不使用互斥量的共享变量
unsigned long sharedCounterMutex = 0; // 使用互斥量的共享变量
std::mutex mtx; // 创建一个互斥量

// 使用互斥量的计数函数
void incrementCounterWithMutex(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 锁定互斥量
        ++sharedCounterMutex; // 访问共享变量
    }
}

// 不使用互斥量的计数函数
void incrementCounterNoMutex(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        ++sharedCounterNoMutex; // 直接访问共享变量
    }
}

int main() {
    const int numThreads = 4; // 线程数量
    const int iterations = 100000; // 每个线程要执行的迭代次数
    std::vector<std::thread> threads;

    // 使用互斥量的实验
    auto startTimeMutex = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(incrementCounterWithMutex, iterations);
    }
    for (auto& thread : threads) {
        thread.join(); // 等待所有线程完成
    }
    auto endTimeMutex = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsedSecondsMutex = endTimeMutex - startTimeMutex;

    std::cout << "Final counter value with mutex: " << sharedCounterMutex << std::endl;
    std::cout << "Time consumed with mutex: " << elapsedSecondsMutex.count() << " seconds" << std::endl;

    // 清空线程向量
    threads.clear();

    // 不使用互斥量的实验
    auto startTimeNoMutex = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(incrementCounterNoMutex, iterations);
    }
    for (auto& thread : threads) {
        thread.join(); // 等待所有线程完成
    }
    auto endTimeNoMutex = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsedSecondsNoMutex = endTimeNoMutex - startTimeNoMutex;

    std::cout << "Final counter value without mutex: " << sharedCounterNoMutex << std::endl;
    std::cout << "Time consumed without mutex: " << elapsedSecondsNoMutex.count() << " seconds" << std::endl;

    return 0;
}

互斥量示例:

  • std::mutex:创建一个互斥量 mtx,用于保护共享变量 sharedCounter。
  • std::lock_guard:用于自动管理互斥量的锁,离开作用域时自动释放锁,避免手动解锁的风险。

4.2 运行结果

在这里插入图片描述

使用互斥量可以保护共享资源,防止数据竞争,而信号量则可以控制对资源的并发访问。二者都是实现线程安全的重要工具。

4.3 结果分析

结果概述:

  • 使用互斥量的计数器值:
    Final counter value with mutex: 400000
    不使用互斥量的计数器值:
    Final counter value without mutex: 149482
  • 时间消耗:
    Time consumed with mutex: 0.0350588 seconds
    Time consumed without mutex: 0.0029207 seconds
  • 互斥量的影响:使用互斥量的计数器值为 400,000,代表了每个线程在执行过程中正确地增加了计数值。互斥量的作用是确保在同一时刻只有一个线程能够修改 sharedCounterMutex,所以最终结果是稳定和准确的。不使用互斥量的计数器值为 149482,明显低于预期的 400,000(4个线程各自增加 100,000)。这表明在多个线程同时访问 sharedCounterNoMutex 时,发生了数据竞争,导致一些增量操作被覆盖或丢失了,最终结果是不准确的。
  • 时间消耗分析:使用互斥量的时间为 0.0350588 秒,尽管由于互斥量的锁定与解锁操作,存在一定的开销,但它确保了程序的线程安全。不使用互斥量的时间为 0.0029207 秒,显著低于使用互斥量的时间。没有锁信息的处理成本使得操作更快速,但缺乏安全性。
  • 准确性 vs. 性能:
    使用互斥量确保了数据的一致性和准确性,避免了线程间的数据竞争问题。尽管这带来了更高的处理时间,但结果是可靠的。不使用互斥量可以提高性能和效率,但会严重影响结果的准确性,尤其是在多线程操作共享数据时。
  • 应用场景:
    当操作共享数据时,如果希望确保结果的准确性和一致性,应该使用互斥量。若对性能要求较高且对数据不一致的容忍度较大,可以考虑不使用互斥量,但需评估数据丢失的风险。这次实验清晰地展示了在多线程编程中,使用互斥量的必要性与对比效果。

5. 传递参数

在 C++ 中,将参数传递给线程任务是一个常见且重要的操作,尤其是在多线程编程中。通过适当的参数传递,可以使线程执行特定的任务。

  • 值传递:将参数按值传递给线程。这意味着线程将获得参数的副本,线程内对参数的修改不会影响主线程中的变量。
  • 引用传递:通过引用传递参数。这样可以让线程访问主线程的变量,任何对这些变量的修改都将反映在主线程中。
  • 指针传递:将参数按指针类型传递。这与引用类似,允许线程访问和修改主线程中的变量。
    下面是一个简单的示例,展示如何将参数通过值、引用和指针传递给线程任务。

下面是一个简单的示例,展示如何将参数通过值、引用和指针传递给线程任务。

5.1 线程参数传递

#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

// 通过值传递参数
void taskByValue(int value) {
    value += 10; // 修改副本
    std::cout << "Value in task (by value): " << value << std::endl;
}

// 通过引用传递参数
void taskByReference(int& value) {
    value += 10; // 修改原变量
    std::cout << "Value in task (by reference): " << value << std::endl;
}

// 通过指针传递参数
void taskByPointer(int* value) {
    if (value) { // 防止空指针
        *value += 10; // 修改原变量
        std::cout << "Value in task (by pointer): " << *value << std::endl;
    }
}

int main() {
    int num = 5;

    // 创建线程,传递参数
    std::cout << "Original num: " << num << std::endl;

    // 通过值传递
    std::thread t1(taskByValue, num);
    t1.join();

    // 通过引用传递
    std::thread t2(taskByReference, std::ref(num));
    t2.join();
    
    // 通过指针传递
    std::thread t3(taskByPointer, &num);
    t3.join();

    std::cout << "Final num after all tasks: " << num << std::endl;

    return 0;
}

代码说明

  • 值传递(taskByValue):taskByValue 函数接收一个整数参数的副本。在函数内部对该副本的修改不会影响主线程中的num 变量。
  • 引用传递(taskByReference):taskByReference函数接收一个整数的引用。这意味着对它的修改直接反映在主线程中的 num 变量上。
  • 指针传递(taskByPointer):taskByPointer 函数接收一个整数的指针,允许直接修改主线程中的变量。

5.2 运行结果

在这里插入图片描述
运行代码后,您将观察到:

  • 通过值传递,输出的值不会影响原变量。
  • 通过引用和指针传递,输出的值可以修改原变量。

这个示例清晰地展示了如何将参数传递给线程任务,不同的传递方式适用于不同的情况和需求。通过了解这些基础,您可以方便地设计多线程程序。

线程池是现代多线程编程中的一个重要概念,用于有效管理线程的生命周期以及重复利用线程,以避免频繁创建和销毁线程带来的开销。在实际应用中,线程池可以提高性能并优化资源利用。

6. 线程池

线程池是一种设计模式,它维护一定数量的线程,准备好去执行特定的任务。任务可以被添加到任务队列中,线程池中的线程会从队列中取出任务并执行。这种方式可以大幅降低线程的创建和销毁时间,同时也能有效控制并发数量。

6.1 线程池的基本功能

任务提交:提供一个接口供用户提交任务。
线程管理:管理线程的生命周期,保持一定数量的线程在空闲状态,等待任务执行。
任务队列:存储待执行的任务,确保任务的有序执行。

6.2. 线程池的简单实现

下面是一个简单的线程池实现示例,使用 C++11 标准库中的线程和互斥量。

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();
    
    // 提交任务
    void enqueue(std::function<void()> task);

private:
    std::vector<std::thread> workers;         // 工作线程
    std::queue<std::function<void()>> tasks;  // 任务队列
    std::mutex queueMutex;                     // 任务队列的互斥量
    std::condition_variable condition;          // 条件变量
    std::atomic<bool> stop;                    // 停止标记

    void worker(); // 工作线程的执行函数
};

// 构造函数
ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
    for (size_t i = 0; i < numThreads; ++i) {
        workers.emplace_back([this] { worker(); }); // 创建工作线程
    }
}

// 析构函数
ThreadPool::~ThreadPool() {
    stop = true;
    condition.notify_all(); // 唤醒所有线程
    for (std::thread &worker : workers) {
        worker.join(); // 等待所有线程完成
    }
}

// 工作线程的执行函数
void ThreadPool::worker() {
    while (true) {
        std::function<void()> task;

        {
            std::unique_lock<std::mutex> lock(queueMutex);
            condition.wait(lock, [this] { return stop || !tasks.empty(); });
            if (stop && tasks.empty()) return; // 如果停止且任务队列为空,退出线程

            task = std::move(tasks.front());
            tasks.pop();
        }

        task(); // 执行任务
    }
}

// 提交任务
void ThreadPool::enqueue(std::function<void()> task) {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        tasks.emplace(std::move(task)); // 将任务添加到队列
    }
    condition.notify_one(); // 唤醒一个线程
}

// 示例任务
void exampleTask(int id) {
    std::cout << "Task " << id << " is being processed by thread " << std::this_thread::get_id() << std::endl;
}

int main() {
    ThreadPool pool(4); // 创建一个线程池,包含4个工作线程

    // 提交任务
    for (int i = 1; i <= 10; ++i) {
        pool.enqueue([i] { exampleTask(i); });
    }

    // 等待一段时间以确保所有任务完成
    std::this_thread::sleep_for(std::chrono::seconds(1));

    return 0;
}

代码说明

  • ThreadPool 类: 使用 std::vectorstd::thread 存储工作线程。 使用 std::queue<std::function<void()>> 存储待执行任务。 使用 std::mutex 和
    std::condition_variable 来管理任务队列的线程安全和任务执行。
  • 构造函数: 创建指定数量的工作线程,调用 worker 函数。
  • 析构函数: 设置停止标记,将所有线程唤醒并等待它们完成。
  • worker 函数: 无限循环,等待获取任务并执行,直到收到停止信号并且任务队列为空。
  • enqueue 函数: 允许用户提交任务到任务队列。
  • 示例任务: exampleTask 函数是一个简单的示例任务,打印其 ID 和执行线程的 ID。

6.3 运行结果

在这里插入图片描述

6.4 输出结果分析

  • 多线程并发:输出中有多个任务并行执行,这表明线程池成功地利用了多个工作线程来处理任务。不同的任务在不同的线程中被处理,这是线程池设计的目标。
  • 线程共享:观察到任务由同一个线程多次处理(如 Task 5 和 Task 6 都由线程 5240 处理)。这说明线程在执行完任务后并没有闲置,而是继续从任务队列中取出新的任务,表明线程池在高效运行。
  • 任务调度:由于任务是按照提交的顺序放入队列并由工作线程并行处理,因此可以看到任务的顺序输出并不一致。某些较早提交的任务可能在线程上完成的时间较晚(如 Task 9 和 Task 10),这可能与任务的执行时间、线程的调度方式以及任务队列的管理有关。
  • 线程标识:每个输出中的 thread [ID] 表示处理该任务的具体线程 ID。根据输出,可以看到任务分配不是均匀的,某些线程处理了更多的任务。这是因为工作线程在完成当前任务后会立刻获取下一个任务进行处理,而某些线程可能在处理特定任务时执行速度更快。

6. 总结

🥳🥳🥳现在,我们在本教程中,您学习了从创建线程,线程加速计算,互斥量,参数传递和线程池管理教程。🛹🛹🛹从而实现对外部世界进行感知,充分认识这个有机与无机的环境🥳🥳🥳科学地合理地进行创作和发挥效益,然后为人类社会发展贡献一点微薄之力。🤣🤣🤣

如果你有任何问题,可以通过下面的二维码加入鹏鹏小分队,期待与你思维的碰撞😘😘😘

参考文献:

  1. 【Qt安装与简易串口控制Arduino开发板小灯教程】
  2. 【VS2019安装+QT配置】
  3. 记录一个使C/C++多线程无法加速计算的问题

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

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

相关文章

Qt中多语言的操作(以QtCreator为例)

1、首先&#xff0c;我们在代码中与文本相关的且需要支持多语言的地方&#xff0c;用tr来包含多语言key&#xff08;多语言key是我们自己定义的&#xff09;&#xff0c;如下 //举例 QPushButton* btnnew QPushButton(this); btn->move(20,20); btn->resize(100,50); //…

vue.js 展示一个树形结构的数据视图,并禁用其中默认选中的节点

功能描述 展示树形结构&#xff1a; 使用 Element UI 的 <el-tree> 组件展示树形结构数据。数据由 content 数组提供&#xff0c;树形结构包含了嵌套的节点及其子节点。 默认选中节点&#xff1a; 使用 defaultCheckedKeys 属性指定默认选中的节点。这些节点在树形结构渲…

自动换行且带下划线的居中长标题的论文封面一种绘图实现

自动换行且带下划线的居中长标题的论文封面一种绘图实现 引言 在一些学位论文的封面上要求标题带有下划线&#xff0c;但长标题的情况下标题自动换行后下划线就会面临一些问题。 因此&#xff0c;往往需要一些特殊的处理。 在《如何制作自动换行且有定长下划线的论文封面模板…

决策树+随机森林模型实现足球大小球让球预测软件

文章目录 前言一、决策树是什么&#xff1f;二、数据收集与整理1.数据收集2.数据清洗3.特征选择 三、决策树构建3.1绘制训练数据图像3.2 训练决策树模型3.3 依据模型绘制决策树的决策边界3.4 树模型可视化 四、模型预测五、随机森林模型总结 前言 之前搞足球数据分析的时候&…

删除topic提示admin token

这个admin token不是admin的密码&#xff0c;而是如下配置文件中的值&#xff1a; 否则报错&#xff1a; 检查&#xff1a; [rootk1 conf]# pwd /opt/kafka-web/efak-web-3.0.1/conf [rootk1 conf]# grep token system-config.properties # delete kafka topic token efak.t…

教师管理系统小程序+ssm论文源码调试讲解

第二章 开发工具及关键技术介绍 2.1 JAVA技术 Java主要采用CORBA技术和安全模型&#xff0c;可以在互联网应用的数据保护。它还提供了对EJB&#xff08;Enterrise JavaBeans&#xff09;的全面支持&#xff0c;java servlet AI&#xff0c;JS&#xff08;java server ages&…

TCL25届校招测评笔试TAS人才测评题库:高分攻略真题分析

&#x1f31f; 职场新人必看&#xff1a;TCL校招测评全解析 &#x1f31f; 亲爱的小伙伴们&#xff0c;你是否正准备踏入职场&#xff0c;或是对即将到来的校招感到既兴奋又紧张&#xff1f;今天&#xff0c;我将带你深入了解TCL校招中的TAS人才测评&#xff0c;让你在面试前做…

Flutter鸿蒙化环境配置(windows)

Flutter鸿蒙化环境配置&#xff08;windows&#xff09; 参考资料Window配置Flutter的鸿蒙化环境下载配置环境变量HarmonyOS的环境变量配置配置Flutter的环境变量Flutter doctor -v 检测的问题flutter_flutter仓库地址的警告问题Fliutter doctor –v 报错[!] Android Studio (v…

构建数据分析模型,及时回传各系统监控监测数据进行分析反馈响应的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。增…

20 基于STM32的温度、电流、电压检测proteus仿真系统(OLED、DHT11、继电器、电机)

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STM32F103C8T6 采用DHT11读取温度、滑动变阻器模拟读取电流、电压。 通过OLED屏幕显示&#xff0c;设置电流阈值为80&#xff0c;电流小阈值为50&#xff0c;电压阈值为60&#xff0c;温度阈值…

虚幻引擎的射线检测/射线追踪

射线检测在 FPS/TPS 游戏中被广泛应用 什么是射线检测? 两个点行成一条线 , 射线检测是从一个起始点发出一条到终点的射线 , 如果射线命中一个游戏对象&#xff0c;就可以获取到对象命中时的 位置、距离、角度、是否命中、骨骼 等非常多的信息 , 这些信息在射击游戏中至关重…

价格便宜又好用的云电脑挑选:ToDesk云电脑 vs 青椒云

云计算技术的成熟使得云电脑因其便捷、灵活和高效而成为日常工作、学习和娱乐的首选工具。而在众多云电脑品牌之中&#xff0c;ToDesk云电脑与青椒云电脑 较为热门 。在此&#xff0c;笔者将围绕价格、性能、用户体验等关键指标对 比 这两款云电脑&#xff0c; 帮助 你们 找到最…

信号分解降噪 | Matlab实现基于TVFEMD-IMF能量熵增量的数据降噪方法

信号分解降噪 | Matlab实现基于TVFEMD-IMF能量熵增量的数据降噪方法 目录 信号分解降噪 | Matlab实现基于TVFEMD-IMF能量熵增量的数据降噪方法效果一览基本介绍程序设计参考资料 效果一览 基本介绍 信号分解降噪 | Matlab实现基于TVFEMD-IMF能量熵增量的数据降噪方法。该方法引…

地平线静态目标检测 MapTR 参考算法-V1.0

简介 高清地图是自动驾驶系统的重要组件&#xff0c;提供精确的驾驶环境信息和道路语义信息。传统离线地图构建方法成本高&#xff0c;维护复杂&#xff0c;使得依赖车载传感器的实时感知建图成为新趋势。早期实时建图方法存在局限性&#xff0c;如处理复杂地图元素的能力不足…

【python】requests 库 源码解读、参数解读

文章目录 一、基础知识二、Requests库详解2.1 requests 库源码简要解读2.2 参数解读2.3 处理响应2.4 错误处理 一、基础知识 以前写过2篇文章&#xff1a; 计算机网络基础&#xff1a; 【socket】从计算机网络基础到socket编程——Windows && Linux C语言 Python实现…

OpenHarmony(鸿蒙南向开发)——小型系统内核(LiteOS-A)【LMS调测】

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 基本概念 LMS全称为Lite Memory Sanitizer&#xff0c;是一种实时…

新版本大疆上云API指令飞行(drc)模式通讯搭建思路

一、大疆上云API mqtt官方通讯指导 1.1drc链路 1.2mqtt交互时序图 二、自行搭建mqtt说明 2.1工具&#xff1a;用emqx搭建mqtt服务器&#xff0c;mqttx作为客户端测试工具 2.2端口说明&#xff1a;1883&#xff0c;普通mqtt消息端口&#xff0c;8083&#xff0c;ws通信协议端…

商业银行应用安全架构设计实践

传统的信息安全工作通常偏向于事中或事后检测漏洞,随着敏捷开发工作的逐步推进,商业银行认识到安全架构设计在实现IT降本增效方面的独特优势。近几年,商业银行逐步构建了安全架构设计工作体系,在组织人员、安全技术与管控流程方面,与企业IT架构密切协同,着力建设安全公共…

课程表-LeetCode100

现在你总共有 numCourses 门课需要选&#xff0c;记为 0 到 numCourses - 1。给你一个数组 prerequisites &#xff0c;其中 prerequisites[i] [ai, bi] &#xff0c;表示在选修课程 ai 前 必须 先选修 bi 。 例如&#xff0c;想要学习课程 0 &#xff0c;你需要先完成课程 1…

Python | Leetcode Python题解之第432题全O(1)的数据结构

题目&#xff1a; 题解&#xff1a; class Node:def __init__(self, key"", count0):self.prev Noneself.next Noneself.keys {key}self.count countdef insert(self, node: Node) -> Node: # 在 self 后插入 nodenode.prev selfnode.next self.nextnode.…