1.2、线程使用

线程基本概念与C++程序的多线程本质

线程是操作系统分配处理器时间的基本单位,简单说就是程序执行流程的最小序列。许多人不知道,每个C++程序从启动那一刻起,就已经在使用多线程了。

当你运行一个最简单的程序:

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

这里面至少已经包含了一个线程--运行main()函数的主线程。C++运行时系统会自动创建这个线程,它是我们程序执行的起点。

案例实战:观察线程ID

让我们编写一个简单程序来观察多线程执行:

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

void background_task() {
    std::cout << "子线程ID: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "子线程工作完成" << std::endl;
}

int main() {
    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
    
    std::thread worker(background_task);  // 创建并启动一个新线程
    
    std::cout << "主线程继续执行其他工作" << std::endl;
    
    worker.join();  // 等待子线程完成
    
    std::cout << "所有工作已完成" << std::endl;
    return 0;
}

运行这段代码,你会发现主线程和子线程有不同的ID,它们并发执行--这正是多线程程序的基本特性。

线程的创建与启动方式

C++提供了多种方式来指定线程要执行的任务,灵活性极高。

使用函数指针

最直观的方式是用普通函数作为线程入口:

void task() {
    // 线程执行的代码
}

std::thread t(task);  // 创建并启动线程

使用函数对象(仿函数)

函数对象提供了更多灵活性,可以携带状态:

class TaskWithState {
private:
    int value;
public:
    TaskWithState(int v) : value(v) {}
    
    void operator()() {
        std::cout << "执行任务,值为:" << value << std::endl;
    }
};

TaskWithState task(42);
std::thread t(task);  // 使用函数对象创建线程

注意陷阱:使用临时函数对象时,必须避免C++最令人困惑的语法解析问题:

// 错误写法:可能被解释为函数声明而非创建线程
std::thread t(TaskWithState());  

// 正确写法1:使用额外括号
std::thread t((TaskWithState()));  

// 正确写法2:使用统一初始化语法
std::thread t{TaskWithState()};  

使用Lambda表达式

Lambda表达式是创建线程最简洁的方式之一:

std::thread t([](){
    std::cout << "使用Lambda表达式创建的线程" << std::endl;
});

实战案例:不同创建方式的性能对比

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

// 计时辅助函数
template<typename Func>
long long timeIt(Func f) {
    auto start = std::chrono::high_resolution_clock::now();
    f();
    auto end = std::chrono::high_resolution_clock::now();
    return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}

// 普通函数
void normalFunction() {
    volatile int sum = 0;
    for (int i = 0; i < 1000000; ++i) sum += i;
}

// 函数对象
class FunctionObject {
public:
    void operator()() {
        volatile int sum = 0;
        for (int i = 0; i < 1000000; ++i) sum += i;
    }
};

int main() {
    const int ITERATIONS = 1000;
    
    // 测试普通函数
    long long normalTime = timeIt([&](){
        for (int i = 0; i < ITERATIONS; ++i) {
            std::thread t(normalFunction);
            t.join();
        }
    });
    
    // 测试函数对象
    long long objectTime = timeIt([&](){
        for (int i = 0; i < ITERATIONS; ++i) {
            FunctionObject fo;
            std::thread t(fo);
            t.join();
        }
    });
    
    // 测试Lambda表达式
    long long lambdaTime = timeIt([&](){
        for (int i = 0; i < ITERATIONS; ++i) {
            std::thread t([](){
                volatile int sum = 0;
                for (int j = 0; j < 1000000; ++j) sum += j;
            });
            t.join();
        }
    });
    
    std::cout << "普通函数: " << normalTime << " 微秒" << std::endl;
    std::cout << "函数对象: " << objectTime << " 微秒" << std::endl;
    std::cout << "Lambda表达式: " << lambdaTime << " 微秒" << std::endl;
    
    return 0;
}

在我的测试中,这三种方式性能差异不大,选择哪种主要取决于代码清晰度和具体需求。

线程的生命周期管理:join与detach

核心规则:一旦线程启动,必须在std::thread对象销毁前调用join()detach()方法,否则程序会调用std::terminate()终止运行。

join:等待线程完成

std::thread t(task);
// ...
t.join();  // 阻塞当前线程,直到t完成执行

join()的重要特性:

  • • 只能调用一次,重复调用会导致程序崩溃
  • • 调用后,线程不再可汇合(joinable()返回false
  • • 会阻塞当前线程直到目标线程完成

detach:分离线程

当线程需要在后台独立运行时,可以使用detach()

std::thread t(background_task);
t.detach();  // 线程在后台独立运行

分离后的线程被称为"守护线程",程序无法直接与它通信或等待它完成。线程的资源管理由C++运行时系统负责。

警告:分离线程访问主线程栈上的变量是极其危险的!

实战案例:异常安全的线程等待

考虑以下场景,如果在t.join()执行前抛出异常,程序将终止:

void unsafe_thread_management() {
    std::thread t([]{ /* 任务代码 */ });
    
    process_data();  // 如果这里抛出异常...
    
    t.join();  // ...这行永远不会执行
}  // t析构时,如果未join或detach,程序终止

专业解决方案:使用RAII(资源获取即初始化)技术

class thread_guard {
private:
    std::thread& t;
public:
    explicit thread_guard(std::thread& t_) : t(t_) {}
    
    ~thread_guard() {
        if(t.joinable()) {
            t.join();
        }
    }
    
    // 禁止复制
    thread_guard(const thread_guard&) = delete;
    thread_guard& operator=(const thread_guard&) = delete;
};

void safe_thread_management() {
    std::thread t([]{ /* 任务代码 */ });
    thread_guard g(t);  // 守卫对象,确保t在函数退出时被join
    
    process_data();  // 即使这里抛出异常,t也会被正确join
    
    // 函数结束时,g的析构函数确保t被join
}

实战案例:文档编辑器中的后台任务

class DocumentEditor {
public:
    void saveDocument(const std::string& filename, const std::string& content) {
        // 在后台线程中保存文档,不阻塞UI
        std::thread t(&DocumentEditor::saveDocumentImpl, this, filename, content);
        t.detach();  // 分离线程,让保存过程在后台进行
        
        // 立即向用户显示保存中消息
        displayStatusMessage("文档保存中...");
    }

private:
    void saveDocumentImpl(std::string filename, std::string content) {
        // 注意参数是传值的,创建了副本
        try {
            // 模拟耗时的IO操作
            std::this_thread::sleep_for(std::chrono::seconds(2));
            
            // 实际保存文件
            std::ofstream file(filename);
            file << content;
            
            // 通知UI线程保存完成(使用某种线程安全的通信机制)
            notifyUiThreadSafely("文档已保存: " + filename);
        }
        catch(const std::exception& e) {
            // 处理异常,通知UI线程
            notifyUiThreadSafely("保存失败: " + std::string(e.what()));
        }
    };
    
    void displayStatusMessage(const std::string& msg) {
        // 显示状态消息
        std::cout << msg << std::endl;
    }
    
    void notifyUiThreadSafely(const std::string& msg) {
        // 实际应用中,这里会使用事件队列或其他线程安全机制
        std::cout << msg << std::endl;
    }
};

线程参数传递

向线程函数传递参数看似简单,但隐藏着许多容易导致灾难性错误的陷阱。

参数传递的基本机制

void func(int i, double d, const std::string& s) {
    std::cout << "线程收到参数: " << i << ", " << d << ", " << s << std::endl;
}

int main() {
    int num = 42;
    double pi = 3.14159;
    std::string msg = "Hello";
    
    std::thread t(func, num, pi, msg);
    t.join();
    
    return 0;
}

关键要点:

  • • 参数默认是拷贝到线程内部存储
  • • 拷贝的参数以右值方式传递给线程函数
  • • 参数拷贝发生在std::thread构造时,而非线程实际开始执行时

致命陷阱:线程访问已销毁的栈变量

考虑以下看似无害的代码:

void process_data(const std::string* data) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << *data << std::endl;  // 可能访问已释放内存!
}

void dangerous_function() {
    std::string local_data = "局部变量数据";
    std::thread t(process_data, &local_data);
    t.detach();  // 危险!线程可能在local_data销毁后仍尝试访问
}  // local_data在函数结束时销毁

这段代码可能导致程序崩溃或不确定行为,因为线程可能在local_data销毁后仍试图访问它。

安全解决方案:确保数据生命周期长于线程,或在线程内创建数据副本

void process_data(std::string data) {  // 注意:传值而非指针
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << data << std::endl;  // 安全:使用线程私有的数据副本
}

void safe_function() {
    std::string local_data = "局部变量数据";
    std::thread t(process_data, local_data);  // 数据被复制
    t.detach();  // 安全:线程使用的是数据副本
}

引用参数传递需要std::ref

如果线程函数需要引用参数,必须显式使用std::ref

void increment(int& value) {
    ++value;
}

int main() {
    int number = 0;
    
    // 错误:number会被拷贝,而非引用传递
    // std::thread t1(increment, number);
    
    // 正确:使用std::ref确保引用传递
    std::thread t2(increment, std::ref(number));
    t2.join();
    
    std::cout << "Number: " << number << std::endl;  // 输出1
    
    return 0;
}

传递成员函数与this指针

调用类的成员函数作为线程任务需要特殊处理:

class DataProcessor {
public:
    void process(const std::string& data) {
        std::cout << "处理数据: " << data << std::endl;
        processed_count++;
    }
    
    int get_processed_count() const {
        return processed_count;
    }
    
private:
    int processed_count = 0;
};

int main() {
    DataProcessor processor;
    std::string data = "重要数据";
    
    // 传递成员函数指针、对象指针和参数
    std::thread t(&DataProcessor::process, &processor, data);
    t.join();
    
    std::cout << "已处理项目数: " << processor.get_processed_count() << std::endl;
    
    return 0;
}

移动语义与不可复制对象

对于只能移动不能复制的对象(如std::unique_ptr),必须使用std::move

#include <iostream>
#include <thread>
#include <memory>

void process_unique_data(std::unique_ptr<int> data) {
    std::cout << "处理数据: " << *data << std::endl;
    *data = 100;  // 修改数据
}

int main() {
    std::unique_ptr<int> data = std::make_unique<int>(42);
    
    // 错误:unique_ptr不能被复制
    // std::thread t(process_unique_data, data);
    
    // 正确:转移所有权
    std::thread t(process_unique_data, std::move(data));
    
    t.join();
    
    // 此时data为nullptr,所有权已转移
    if (!data) {
        std::cout << "数据所有权已转移到线程" << std::endl;
    }
    
    return 0;
}

线程所有权的转移:移动而非复制

std::thread对象自身也是只能移动不能复制的,这允许我们在不同对象间转移线程所有权:

std::thread worker([]{ std::cout << "工作线程" << std::endl; });

// 转移所有权到t2,worker不再关联任何线程
std::thread manager = std::move(worker);

// 此时worker.joinable()为false

实战案例:动态创建并管理多个线程

#include <iostream>
#include <thread>
#include <vector>
#include <numeric>

// 计算一个区间内所有数字的和
void sum_range(size_t start, size_t end, size_t& result) {
    result = 0;
    for (size_t i = start; i < end; ++i) {
        result += i;
    }
}

// 执行并行求和
size_t parallel_sum(size_t n, size_t num_threads) {
    std::vector<std::thread> threads;
    std::vector<size_tresults(num_threads);
    
    size_t chunk_size = n / num_threads;
    
    for (size_t i = 0; i < num_threads; ++i) {
        size_t start = i * chunk_size;
        size_t end = (i == num_threads - 1) ? n : (i + 1) * chunk_size;
        
        threads.push_back(
            std::thread(sum_range, start, end, std::ref(results[i]))
        );
    }
    
    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }
    
    // 合并结果
    return std::accumulate(results.begin(), results.end(), 0ull);
}

int main() {
    size_t total = parallel_sum(10000008);
    std::cout << "Sum: " << total << std::endl;
    return 0;
}

线程所有权转移的实用场景

功能一:返回创建的线程

std::thread create_worker(const std::string& name) {
    return std::thread([name]{
        std::cout << "工作线程 " << name << " 启动" << std::endl;
        // 执行实际工作...
    });
}

int main() {
    // 创建并获取线程所有权
    std::thread worker = create_worker("数据处理器");
    worker.join();
    return 0;
}

功能二:将线程移入容器

class ThreadPool {
private:
    std::vector<std::thread> workers;
    
public:
    template<typename Func, typename... Args>
    void add_task(Func&& f, Args&&... args) {
        workers.emplace_back(
            std::forward<Func>(f), 
            std::forward<Args>(args)...
        );
    }
    
    ~ThreadPool() {
        for (auto& worker : workers) {
            if (worker.joinable()) {
                worker.join();
            }
        }
    }
};

线程异常处理的高级技巧

C++的异常机制与多线程结合时需要特别小心。关键法则:线程函数内抛出的异常必须在该线程内处理,否则程序将调用std::terminate()终止!

安全的线程异常处理模式:

void thread_task() {
    try {
        // 可能抛出异常的代码
        throw std::runtime_error("模拟错误");
    }
    catch(const std::exception& e) {
        // 记录错误
        std::cerr << "线程异常: " << e.what() << std::endl;
        
        // 可能的恢复操作
        // ...
    }
    catch(...) {
        std::cerr << "线程中发生未知异常" << std::endl;
    }
}

如果需要在主线程中获取工作线程的异常信息,可以使用std::promisestd::future

#include <iostream>
#include <thread>
#include <future>
#include <stdexcept>

void thread_task(std::promise<void> result_promise) {
    try {
        // 模拟一个会失败的任务
        throw std::runtime_error("任务执行失败");
        
        // 如果成功,设置值
        result_promise.set_value();
    }
    catch(...) {
        // 捕获所有异常,并通过promise传递给等待的线程
        result_promise.set_exception(std::current_exception());
    }
}

int main() {
    std::promise<void> result_promise;
    std::future<void> result_future = result_promise.get_future();
    
    std::thread t(thread_task, std::move(result_promise));
    
    try {
        // 等待线程完成或抛出异常
        result_future.get();
        std::cout << "任务成功完成" << std::endl;
    }
    catch(const std::exception& e) {
        std::cout << "任务失败,异常: " << e.what() << std::endl;
    }
    
    t.join();
    return 0;
}

总结与心得

C++11的std::thread库为我们提供了强大而灵活的多线程编程能力,但这种能力需要谨慎使用。我发现,真正掌握多线程编程不仅要学会API使用,更要深刻理解底层原理和可能的陷阱。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)

 

阅读剩余
THE END