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对象在析构时,线程仍在运行(joinabletrue),程序会调用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<doubleresults(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<doubletestData(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 = {124, 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】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)

 

阅读剩余
THE END