C++多线层join/detach记录

多线程是并发编程的核心技术,它允许程序在同一进程内创建多个执行流(线程),这些线程共享进程的资源(如内存空间、文件描述符等),但拥有独立的执行路径。在C++中,自C++11起,标准库通过<thread>头文件提供了跨平台的多线程支持,极大简化了多线程编程。

一、多线程基础:线程的创建与管理

1. 线程的本质与优势
  • 线程(Thread):进程内的一个执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,线程间共享进程的地址空间和资源。
  • 优势
    • 提高CPU利用率(尤其多核环境下,线程可并行执行)。
    • 提升程序响应性(如UI线程与后台计算线程分离)。
    • 简化复杂任务的拆分(如并行处理数据)。
2. C++中创建线程:std::thread

std::thread是C++标准库的线程类,通过它可创建新线程。线程创建后,需通过join()detach()管理其生命周期。

核心操作

  • 构造函数:传入可调用对象(函数、lambda、仿函数等)作为线程入口。
  • join():阻塞当前线程,等待目标线程执行完毕后再继续。
  • detach():将线程与std::thread对象分离,线程后台运行,由系统回收资源。
  • joinable():判断线程是否可joindetach(已处理过的线程返回false)。

我来详细理解一下join和detach的区别和定义哈:join就是从线程创建执行位置,从主线中断去执行该子线程中的内容,而join的目的就是用来阻塞主线程的执行,join之后的命令,必须要该join对应的线程结束后,才会执行。

而detach,emm,感觉更难理解了一些, 我目前的理解是,detach执行后,子线程会后台执行,会和主线程并行执行,但我不理解emmm,他应该放的位置,但我知道,尽量放在主程序终止前,且建议尽早调用,确保子线程访问的资源比子线程生命周期长。

示例1:基本线程创建与join()

#include <iostream>
#include <thread>  // 多线程核心头文件

// 线程执行的函数
void thread_func(int id) {
    std::cout << "Thread " << id << " is running\n";
}

int main() {
    // 创建线程:传入函数和参数
    std::thread t1(thread_func, 1);  // t1 对应线程1
    std::thread t2(thread_func, 2);  // t2 对应线程2

    // 等待线程执行完毕(必须调用join或detach,否则程序异常终止)
    t1.join();  // 主线程等待t1结束
    t2.join();  // 主线程等待t2结束

    std::cout << "Main thread ends\n";
    return 0;
}

输出(顺序可能因调度不同而变化)

Thread 1 is running
Thread 2 is running
Main thread ends

示例2:detach()的使用(后台线程)

#include <iostream>
#include <thread>
#include <chrono>  // 用于时间相关操作

void background_task() {
    for (int i = 0; i < 3; ++i) {
        std::cout << "Background working... " << i << "\n";
        std::this_thread::sleep_for(std::chrono::seconds(1));  // 线程休眠1秒
    }
}

int main() {
    std::thread t(background_task);
    t.detach();  // 线程分离,后台运行

    // 主线程休眠2秒,确保能看到后台线程输出
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Main thread exits\n";
    return 0;
}

说明
detach()后,线程独立运行,std::thread对象不再管理它。若主线程提前结束,后台线程可能被强制终止(如上例中后台线程的第3次输出可能无法打印)。

注意

  • std::thread对象销毁时未调用join()detach(),其析构函数会调用std::terminate()终止程序。
  • 线程入口函数的参数会被拷贝到线程内部,若需传递引用,需用std::ref()包装(但需确保引用对象生命周期长于线程)。

二、共享资源与数据竞争

多线程的核心问题是共享资源访问。当多个线程同时读写同一资源(如全局变量、堆内存)时,可能出现数据竞争(Data Race),导致结果不可预测。

1. 数据竞争示例
#include <iostream>
#include <thread>

int shared_count = 0;  // 共享资源

// 线程函数:对共享变量自增10000次
void increment() {
    for (int i = 0; i < 10000; ++i) {
        shared_count++;  // 危险:非原子操作,可能引发数据竞争
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();

    // 预期结果:20000,但实际可能小于20000(因数据竞争)
    std::cout << "Final count: " << shared_count << "\n";
    return 0;
}

问题原因
shared_count++看似简单,实际包含三步操作(读取→修改→写入)。若两个线程同时读取到相同值,修改后写入会覆盖彼此的结果(如都读取到100,都修改为101,最终结果少加1)。

2. 解决数据竞争:同步机制

同步机制确保临界区(Critical Section,即访问共享资源的代码段) 互斥执行。C++提供多种同步工具,核心是原子操作

(1)互斥锁(std::mutex

std::mutex保证同一时间只有一个线程进入临界区:

#include <iostream>
#include <thread>
#include <mutex>  // 互斥锁头文件

std::mutex mtx;  // 全局互斥锁
int shared_count = 0;

void safe_increment() {
    for (int i = 0; i < 10000; ++i) {
        mtx.lock();         // 获取锁(若被占用则阻塞)
        shared_count++;     // 临界区:安全修改共享资源
        mtx.unlock();       // 释放锁
    }
}

int main() {
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);
    t1.join();
    t2.join();

    std::cout << "Final count: " << shared_count << "\n";  // 必然是20000
    return 0;
}

优化:用std::lock_guard(RAII)自动管理锁的释放,避免遗漏unlock()

void safer_increment() {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 构造时锁,析构时自动解锁
        shared_count++;
    }
}
(2)原子操作(std::atomic

对于简单的读写操作(如计数器),std::atomic提供硬件级别的原子性,无需锁,效率更高:

#include <iostream>
#include <thread>
#include <atomic>  // 原子操作头文件

std::atomic<int> atomic_count(0);  // 原子变量(默认初始化为0)

void atomic_increment() {
    for (int i = 0; i < 10000; ++i) {
        atomic_count++;  // 原子操作,无数据竞争
    }
}

int main() {
    std::thread t1(atomic_increment);
    std::thread t2(atomic_increment);
    t1.join();
    t2.join();

    std::cout << "Atomic count: " << atomic_count << "\n";  // 必然是20000
    return 0;
}

说明
std::atomic支持基本数据类型(intlong、指针等),其操作由CPU指令直接保证原子性,无需上下文切换,性能优于锁。

三、线程间通信:条件变量

多线程除了共享资源,还需协作完成任务(如"生产者-消费者"模型)。std::condition_variable是线程间通信的核心工具,用于线程等待某个条件成立。

生产者-消费者模型示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>

const int MAX_QUEUE_SIZE = 5;  // 队列最大容量
std::queue<int> data_queue;    // 共享队列(生产者放入,消费者取出)
std::mutex mtx;                // 保护队列的锁
std::condition_variable cv;    // 条件变量

// 生产者:生成数据并放入队列
void producer() {
    for (int i = 1; i <= 10; ++i) {  // 生成10个数据
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待队列有空闲位置(若队列满则阻塞)
        cv.wait(lock, []{ return data_queue.size() < MAX_QUEUE_SIZE; });
        
        // 放入数据
        data_queue.push(i);
        std::cout << "Produced: " << i << " (Queue size: " << data_queue.size() << ")\n";
        
        lock.unlock();
        cv.notify_one();  // 通知消费者有新数据
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟生产耗时
    }
}

// 消费者:从队列取出数据并处理
void consumer() {
    for (int i = 1; i <= 10; ++i) {  // 消费10个数据
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待队列非空(若队列为空则阻塞)
        cv.wait(lock, []{ return !data_queue.empty(); });
        
        // 取出数据
        int data = data_queue.front();
        data_queue.pop();
        std::cout << "Consumed: " << data << " (Queue size: " << data_queue.size() << ")\n";
        
        lock.unlock();
        cv.notify_one();  // 通知生产者有空闲位置
        std::this_thread::sleep_for(std::chrono::milliseconds(150));  // 模拟消费耗时
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons(consumer);
    prod.join();
    cons.join();
    return 0;
}

核心逻辑

  • 生产者满队列时阻塞,等待消费者取走数据后唤醒。
  • 消费者空队列时阻塞,等待生产者放入数据后唤醒。
  • cv.wait(lock, predicate):原子地释放锁并阻塞,被唤醒后重新获取锁并检查条件(防止虚假唤醒)。

四、线程局部存储(thread_local

多线程中,若每个线程需要独立的变量副本(不共享),可使用thread_local关键字,其生命周期与线程一致。

示例:线程私有变量
#include <iostream>
#include <thread>

// 线程局部变量:每个线程有独立副本
thread_local int thread_id = 0;

void print_id(int id) {
    thread_id = id;  // 为当前线程的副本赋值
    for (int i = 0; i < 3; ++i) {
        std::cout << "Thread " << thread_id << " iteration " << i << "\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    std::thread t1(print_id, 1);
    std::thread t2(print_id, 2);
    t1.join();
    t2.join();
    return 0;
}

输出(片段)

Thread 1 iteration 0
Thread 2 iteration 0
Thread 1 iteration 1
Thread 2 iteration 1
...

说明
thread_id在两个线程中是独立的,修改一个线程的副本不影响另一个。

五、多线程常见问题与解决方案

1. 死锁(Deadlock)

定义:两个或多个线程互相等待对方释放锁,导致永久阻塞。

示例

std::mutex mtx1, mtx2;

// 线程1:先锁mtx1,再等mtx2
void thread1() {
    mtx1.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 给线程2抢锁机会
    mtx2.lock();  // 等待mtx2,此时线程2已持有mtx2并等待mtx1,死锁!
    // ...
    mtx2.unlock();
    mtx1.unlock();
}

// 线程2:先锁mtx2,再等mtx1
void thread2() {
    mtx2.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    mtx1.lock();  // 等待mtx1,死锁!
    // ...
    mtx1.unlock();
    mtx2.unlock();
}

解决方法

  • 按固定顺序加锁(如所有线程先锁地址小的锁)。
  • std::lock同时获取多个锁(原子操作,避免部分加锁):
    void safe_thread() {
        std::lock(mtx1, mtx2);  // 原子获取两个锁,无死锁风险
        std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);  // 接管已获取的锁
        std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
        // ...
    }
    
2. 活锁(Livelock)

定义:线程不断响应对方的动作,却无法推进工作(如两个线程互相释放锁并立即重试,导致无限循环)。

解决方法

  • 引入随机延迟(减少冲突概率)。
  • 优先级机制(让一个线程优先获取资源)。
3. 饥饿(Starvation)

定义:某些线程长期无法获取资源(如低优先级线程被高优先级线程持续抢占)。

解决方法

  • 避免长时间持有锁。
  • 合理设置线程优先级(需谨慎,可能引入新问题)。

六、线程池(Thread Pool)

频繁创建/销毁线程会带来性能开销(线程上下文切换、资源分配)。线程池预先创建一批线程,重复利用它们执行任务,降低开销。

简单线程池实现
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <atomic>

class ThreadPool {
private:
    std::vector<std::thread> workers;  // 工作线程
    std::queue<std::function<void()>> tasks;  // 任务队列
    std::mutex mtx;                    // 保护任务队列
    std::condition_variable cv;        // 通知线程有任务
    std::atomic<bool> stop;            // 线程池停止标志

public:
    // 构造函数:创建n个工作线程
    ThreadPool(size_t n) : stop(false) {
        for (size_t i = 0; i < n; ++i) {
            workers.emplace_back([this] {
                while (!stop) {  // 循环等待任务
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(mtx);
                        // 等待任务或停止信号
                        cv.wait(lock, [this] { return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) return;  // 停止且无任务,退出线程
                        task = std::move(tasks.front());    // 取出任务
                        tasks.pop();
                    }
                    task();  // 执行任务
                }
            });
        }
    }

    // 析构函数:停止所有线程
    ~ThreadPool() {
        stop = true;
        cv.notify_all();  // 唤醒所有等待的线程
        for (auto& worker : workers) {
            worker.join();  // 等待线程结束
        }
    }

    // 添加任务到队列
    template<class F>
    void enqueue(F&& f) {
        {
            std::unique_lock<std::mutex> lock(mtx);
            tasks.emplace(std::forward<F>(f));  // 放入任务
        }
        cv.notify_one();  // 通知一个线程执行任务
    }
};

// 使用示例
int main() {
    ThreadPool pool(3);  // 创建3个线程的线程池

    // 添加5个任务
    for (int i = 0; i < 5; ++i) {
        pool.enqueue([i] {
            std::cout << "Task " << i << " executed by thread " 
                      << std::this_thread::get_id() << "\n";
            std::this_thread::sleep_for(std::chrono::seconds(1));
        });
    }

    return 0;  // 析构函数自动停止线程池
}

说明

  • 线程池创建时初始化工作线程,线程循环等待任务队列。
  • 任务通过enqueue添加到队列,线程池自动调度空闲线程执行。
  • 析构时停止线程池,确保所有线程安全退出。

七、多线程最佳实践

  1. 最小化共享状态:减少线程间共享资源,降低同步复杂度(如用消息传递替代共享内存)。
  2. 优先使用原子操作:简单操作(计数、标志位)用std::atomic,避免锁的开销。
  3. 用RAII管理锁lock_guard/unique_lock确保锁正确释放,避免死锁。
  4. 避免长时间持有锁:仅在临界区保留锁,完成后立即释放。
  5. 测试与调试:多线程bug难以复现,可使用工具(如Valgrind的Helgrind)检测数据竞争。

总结

C++多线程编程通过std::threadstd::mutexstd::condition_variable等工具,提供了完整的并发解决方案。核心挑战是共享资源同步线程协作,需理解锁、原子操作、条件变量等机制,并警惕死锁、数据竞争等问题。合理使用线程池可提升性能,而最小化共享状态是编写可靠多线程代码的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值