【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++ 线程的详细步骤和建议。从创建线程,线程加速计算,互斥量,参数传递和线程池管理循序渐进,接下来开始第一步了解线程。
环境搭建参考:
- 【Qt安装与简易串口控制Arduino开发板小灯教程】
- 【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. 总结
🥳🥳🥳现在,我们在本教程中,您学习了从创建线程,线程加速计算,互斥量,参数传递和线程池管理教程。🛹🛹🛹从而实现对外部世界进行感知,充分认识这个有机与无机的环境🥳🥳🥳科学地合理地进行创作和发挥效益,然后为人类社会发展贡献一点微薄之力。🤣🤣🤣
如果你有任何问题,可以通过下面的二维码加入鹏鹏小分队,期待与你思维的碰撞😘😘😘
参考文献:
- 【Qt安装与简易串口控制Arduino开发板小灯教程】
- 【VS2019安装+QT配置】
- 记录一个使C/C++多线程无法加速计算的问题