1.3、移交线程归属权/统计运行线程数量
今天,我将用最通俗易懂的语言,带你深入了解C++线程管理的几个关键环节:如何优雅地移交线程归属权、科学地确定线程数量、精准地识别线程身份。掌握这些技能,将让你的多线程程序,游刃有余。
移交线程归属权:线程的"过户"手续
在C++的世界里,每个创建的线程都有一个"主人"--即拥有其归属权的std::thread
对象。理解线程归属权的转移,就像理解房产证的过户一样重要。
为什么需要移交线程归属权?
你创建了一个线程来执行复杂计算,但随后发现需要将这个线程交给另一个管理器对象来监控。这时,线程归属权的转移就显得尤为重要。
#include <iostream>
#include <thread>
#include <vector>
class ThreadOwner {
private:
std::thread worker;
public:
// 从外部获取线程的归属权
ThreadOwner(std::thread&& t) : worker(std::move(t)) {
std::cout << "线程归属权已转移到ThreadOwner" << std::endl;
}
// 转移线程归属权到另一个对象
std::thread releaseThread() {
std::cout << "线程归属权即将离开ThreadOwner" << std::endl;
return std::move(worker);
}
~ThreadOwner() {
// 确保线程已完成或已分离,避免程序终止
if (worker.joinable()) {
worker.join();
}
}
};
void longTask() {
std::cout << "执行耗时任务..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "耗时任务完成!" << std::endl;
}
int main() {
// 创建线程
std::thread t(longTask);
// 第一次转移归属权:从main函数转移到owner1
ThreadOwner owner1(std::move(t));
// t不再拥有线程的归属权
std::cout << "t是否关联线程:" << (t.joinable() ? "是" : "否") << std::endl;
// 第二次转移归属权:从owner1转移到新线程t2
std::thread t2 = owner1.releaseThread();
// 等待线程完成
if (t2.joinable()) {
t2.join();
}
return 0;
}
注意事项与实用技巧:
- • 归属权移交后,原
std::thread
对象不再与任何线程关联,其joinable()
返回false
。 - • 如果一个拥有线程归属权的
std::thread
对象在析构时,线程仍在运行(joinable
为true
),程序会调用std::terminate()
终止。 - • 移交归属权使用移动语义(
std::move
),而不是复制,因为线程对象不允许复制。 - • 线程归属权的转移可用于实现线程池、工作窃取算法等高级并发模式。
在运行时选择线程数量
选择合适的线程数量就像是餐厅决定雇佣多少服务员--太少会让客人等待,太多则会导致员工之间相互干扰。在高性能计算中,找到这个"黄金数字"至关重要。
如何科学地选择线程数量?
#include <iostream>
#include <thread>
#include <vector>
#include <algorithm>
#include <numeric>
#include <chrono>
#include <functional>
// 一个简单的计算密集型任务:计算大量数字的平方和
double computeIntensiveTask(const std::vector<double>& data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; ++i) {
sum += data[i] * data[i];
// 模拟一些额外计算工作
for (int j = 0; j < 100; ++j) {
sum = sum + (sum * 0.00000001);
}
}
return sum;
}
// 使用多线程计算结果
double parallelCompute(const std::vector<double>& data, int numThreads) {
auto startTime = std::chrono::high_resolution_clock::now();
std::vector<std::thread> threads;
std::vector<double> results(numThreads, 0.0);
size_t chunkSize = data.size() / numThreads;
for (int i = 0; i < numThreads; ++i) {
size_t start = i * chunkSize;
size_t end = (i == numThreads - 1) ? data.size() : (i + 1) * chunkSize;
threads.emplace_back([&data, start, end, &results, i] {
results[i] = computeIntensiveTask(data, start, end);
});
}
for (auto& t : threads) {
t.join();
}
double totalSum = std::accumulate(results.begin(), results.end(), 0.0);
auto endTime = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
std::cout << "使用 " << numThreads << " 个线程计算,耗时: "
<< duration.count() << " ms" << std::endl;
return totalSum;
}
int main() {
// 生成测试数据
const size_t dataSize = 10'000'000;
std::vector<double> testData(dataSize);
for (size_t i = 0; i < dataSize; ++i) {
testData[i] = static_cast<double>(i % 1000) / 1000.0;
}
// 获取CPU核心数
unsigned int numCores = std::thread::hardware_concurrency();
std::cout << "系统检测到 " << numCores << " 个硬件线程" << std::endl;
// 测试不同线程数量的性能
std::vector<int> threadCounts = {1, 2, 4, numCores, numCores*2};
for (int count : threadCounts) {
double result = parallelCompute(testData, count);
std::cout << "计算结果: " << result << std::endl << std::endl;
}
return 0;
}
线程数量选择的核心原则:
- • 硬件感知:使用
std::thread::hardware_concurrency()
获取系统支持的并发线程数(通常等于CPU核心数或硬件线程数)。 - • 任务类型分析:
- • CPU密集型任务:线程数通常等于或略少于核心数
- • IO密集型任务:线程数可以比核心数多,因为大部分时间在等待IO
- • 线程亲和性:在某些性能关键场景,将线程绑定到特定CPU核心可提高缓存命中率
- • 动态调整:根据系统负载和任务队列长度动态调整线程数
在我的实践中,一个经验法则是:对于CPU密集型任务,最佳线程数=CPU核心数+1;对于IO密集型任务,可以使用更多线程,但监控系统性能,避免过度订阅。
识别线程:给线程戴上"身份证"
在复杂的多线程应用中,能够准确识别每个线程是调试和性能分析的关键。想象一下,如果你管理着一个有100名员工的团队,但却不知道谁是谁,那将是一场噩梦。
#include <iostream>
#include <thread>
#include <mutex>
#include <sstream>
#include <iomanip>
#include <map>
#include <chrono>
// 线程安全的日志类
class ThreadSafeLogger {
private:
std::mutex logMutex;
std::map<std::thread::id, std::string> threadNames;
public:
// 为线程设置一个友好的名称
void setThreadName(const std::string& name) {
std::lock_guard<std::mutex> lock(logMutex);
threadNames[std::this_thread::get_id()] = name;
}
// 获取当前线程的名称或ID
std::string getThreadIdentifier() {
std::thread::id id = std::this_thread::get_id();
std::lock_guard<std::mutex> lock(logMutex);
if (threadNames.find(id) != threadNames.end()) {
return threadNames[id];
} else {
std::stringstream ss;
ss << "Thread-" << id;
return ss.str();
}
}
// 记录日志
void log(const std::string& message) {
std::lock_guard<std::mutex> lock(logMutex);
// 获取当前时间
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::tm tm = *std::localtime(&time);
std::cout << "["
<< std::put_time(&tm, "%H:%M:%S") << "]["
<< getThreadIdentifier() << "] "
<< message << std::endl;
}
};
// 全局日志器
ThreadSafeLogger logger;
// 工作线程函数
void workerFunction(int workerId) {
// 设置线程名称
std::string threadName = "Worker-" + std::to_string(workerId);
logger.setThreadName(threadName);
logger.log("线程启动,准备工作");
// 模拟一些工作
for (int i = 0; i < 3; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(workerId * 100));
logger.log("正在处理任务 " + std::to_string(i+1) + "/3");
}
logger.log("工作完成,线程退出");
}
int main() {
logger.setThreadName("MainThread");
logger.log("程序开始,创建工作线程");
// 创建多个工作线程
std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
workers.emplace_back(workerFunction, i+1);
}
// 等待所有线程完成
for (auto& worker : workers) {
worker.join();
}
logger.log("所有工作线程已完成,程序退出");
return 0;
}
线程识别的实用技巧:
- • 使用
std::thread::id
作为线程的唯一标识符 - • 为线程分配有意义的名称,便于调试和日志分析
- • 在多线程日志系统中自动包含线程标识信息
- • 针对特定线程设置监控和性能计数器
- • 使用线程本地存储(Thread Local Storage, TLS)保存线程特有数据
在大型项目中,我常设计一个"线程注册表",让每个线程在启动时注册自己的目的和关键信息,便于监控和分析性能瓶颈。
总结
记住,多线程编程的最终目标不是创建尽可能多的线程,而是让有限的线程发挥最大的效能。在这个越来越需要并行计算的时代,掌握这门艺术将成为C++开发者的重要竞争力。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)
阅读剩余
版权声明:
作者:讳疾忌医-note
链接:https://www.1217zy.vip/archives/1127
文章版权归作者所有,未经允许请勿转载。
THE END