云计算百科
云计算领域专业知识百科平台

Linux线程实战:从等待到C++多线程

Linux 线程深度指南:从等待、分离到 C++ 多线程实战

1. 线程基础概念回顾

在 Linux 系统中,线程是轻量级的执行单元,共享进程的地址空间、文件描述符等资源,但拥有独立的栈、寄存器状态和程序计数器。使用 POSIX 线程库 (pthread) 进行线程操作。

  • 创建线程:pthread_create
  • 获取线程 ID:pthread_self
  • 终止线程:pthread_exit 或从线程函数返回。

2. 线程的状态管理:等待 (Joining) 与 分离 (Detaching)

2.1 线程等待 (pthread_join)

当一个线程创建了另一个线程(通常称为子线程)时,父线程可能需要等待子线程完成执行并获取其退出状态。这通过 pthread_join 实现。

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

  • thread: 要等待的线程 ID。
  • retval: 指向一个指针的指针,用于接收目标线程的退出状态(即 pthread_exit 传入的值或线程函数的返回值)。可为 NULL。
  • 返回值:成功返回 0;错误返回错误码。

作用:

  • 阻塞调用线程,直到指定的 thread 终止。
  • 回收目标线程的资源(主要是栈空间)。
  • 获取目标线程的退出状态。
  • 示例:

    void* thread_function(void* arg) {
    // … 线程工作 …
    return (void*)42; // 或 pthread_exit((void*)42);
    }

    int main() {
    pthread_t tid;
    void *exit_status;

    pthread_create(&tid, NULL, thread_function, NULL);
    // … 主线程其他工作 …
    pthread_join(tid, &exit_status); // 等待子线程结束
    printf("Thread exited with status: %ld\\n", (long)exit_status);
    return 0;
    }

    https://tv.sohu.com/v/dXMvNDQyMDkzMDExLzY5NTk0MDczOC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDQyMDkzMDExLzY5NTk0MDkyMy5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDQyMDkzMDExLzY5NTk0MDc1NC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MDk3Ny5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTIxNC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTA3NC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTIyMi5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTI1Ny5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTI2MS5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTEzNy5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTMyNi5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTQzMi5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTE0NS5zaHRtbA==.html  

    关键点:

    • 一个线程只能被 join 一次。
    • 如果线程已被 join 或标记为 detached,再次 join 会出错。
    • 未 join 的非分离线程终止后,其资源不会被自动回收,成为“僵尸线程”,造成资源泄漏。

    2.2 线程分离 (pthread_detach)

    有时,父线程并不关心子线程何时结束或其退出状态,也不想去显式 join 它。这时可以将线程标记为“分离状态”。

    #include <pthread.h>
    int pthread_detach(pthread_t thread);

    • thread: 要分离的线程 ID。
    • 返回值:成功返回 0;错误返回错误码。

    作用:

  • 将线程 thread 标记为分离状态。
  • 分离状态的线程终止时,其资源会自动被系统回收,无需其他线程调用 pthread_join。
  • 不能对分离状态的线程调用 pthread_join。
  • 何时使用:

    • 当不关心线程的返回结果时。
    • 当线程是“一次性”的后台任务时。
    • 防止忘记 join 导致资源泄漏。

    示例:

    void* worker_thread(void* arg) {
    // … 后台工作,不需要返回结果 …
    return NULL;
    }

    int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, worker_thread, NULL);
    pthread_detach(tid); // 立即分离,不关心其结束
    // … 主线程继续运行,无需等待 tid …
    return 0;
    }

    https://tv.sohu.com/v/dXMvNDQyMDkzMDExLzY5NTk0MDczOC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDQyMDkzMDExLzY5NTk0MDkyMy5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDQyMDkzMDExLzY5NTk0MDc1NC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MDk3Ny5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTIxNC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTA3NC5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTIyMi5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTI1Ny5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTI2MS5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTEzNy5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTMyNi5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTQzMi5zaHRtbA==.html https://tv.sohu.com/v/dXMvNDM4OTIwMjE2LzY5NTk0MTE0NS5zaHRtbA==.html  

    关键点:

    • 可以在线程创建后立即分离(如示例)。
    • 线程也可以在线程函数内部调用 pthread_detach(pthread_self()) 来自我分离。
    • 尝试分离一个已经终止的线程或未创建的线程会出错。
    • 分离操作只能进行一次。

    join vs detach 总结:

    特性pthread_joinpthread_detach
    目的 等待结束、获取状态、回收资源 自动回收资源,不获取状态
    阻塞 阻塞调用线程直到目标线程结束 非阻塞,立即返回
    资源回收 由 join 调用者负责回收 由系统在线程结束时自动回收
    状态获取 可以获取退出状态 无法获取退出状态
    调用次数 只能 join 一次 只能 detach 一次

    3. 线程同步机制 (Synchronization)

    多个线程并发访问共享资源时,需同步以避免数据竞争 (Data Race) 和竞态条件 (Race Condition)。主要机制:

    3.1 互斥锁 (Mutexes, pthread_mutex_t)

    提供对共享资源的独占访问。线程访问临界区前加锁,访问后解锁。

    #include <pthread.h>
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化

    // 动态初始化
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    int pthread_mutex_lock(pthread_mutex_t *mutex); // 阻塞加锁
    int pthread_mutex_trylock(pthread_mutex_t *mutex); // 非阻塞加锁
    int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁

    示例 (银行账户存取款):

    struct account {
    pthread_mutex_t lock;
    int balance;
    };

    void* deposit(void* arg) {
    struct account *acc = (struct account*)arg;
    for (int i = 0; i < 10000; ++i) {
    pthread_mutex_lock(&acc->lock);
    acc->balance += 1;
    pthread_mutex_unlock(&acc->lock);
    }
    return NULL;
    }
    // … 类似地实现 withdraw …

    3.2 条件变量 (Condition Variables, pthread_cond_t)

    用于线程间等待某个条件成立。必须与互斥锁配合使用。

    #include <pthread.h>
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

    int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
    int pthread_cond_destroy(pthread_cond_t *cond);

    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // 等待条件,释放锁,被唤醒后重新获得锁
    int pthread_cond_signal(pthread_cond_t *cond); // 唤醒至少一个等待该条件的线程
    int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待该条件的线程

    使用范式 (生产者-消费者):

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    queue_t queue; // 共享队列

    // 生产者线程
    void* producer(void* arg) {
    while (/* 有数据生产 */) {
    data_t data = produce_data();
    pthread_mutex_lock(&mutex);
    enqueue(&queue, data); // 将数据放入队列
    pthread_cond_signal(&cond); // 通知可能有消费者在等待
    pthread_mutex_unlock(&mutex);
    }
    return NULL;
    }

    // 消费者线程
    void* consumer(void* arg) {
    while (/* 需要消费 */) {
    pthread_mutex_lock(&mutex);
    while (queue_empty(&queue)) { // 必须用 while 循环检查,避免虚假唤醒
    pthread_cond_wait(&cond, &mutex); // 等待条件,释放锁
    }
    data_t data = dequeue(&queue); // 队列不空,取出数据
    pthread_mutex_unlock(&mutex);
    consume_data(data);
    }
    return NULL;
    }

    关键点:

    • pthread_cond_wait 会释放关联的互斥锁,并阻塞线程。
    • 被唤醒时(通过 signal 或 broadcast),它会在返回前重新获得互斥锁。
    • 唤醒后,条件可能不再成立(其他线程抢先消费了),因此必须在循环中检查条件 (while (condition))。

    3.3 其他同步机制

    • 读写锁 (pthread_rwlock_t): 允许多个读线程并发,但写线程独占。
    • 信号量 (sem_t): 更通用的计数器,可用于控制并发访问数量。
    • 屏障 (pthread_barrier_t): 让一组线程在代码中的某一点等待,直到所有成员都到达。

    4. C++ 多线程实战 (std::thread)

    C++11 引入了 <thread> 头文件,提供了更现代、面向对象的线程接口 std::thread,通常比直接使用 pthread API 更简洁安全。

    4.1 创建线程

    #include <thread>
    #include <iostream>

    void hello() {
    std::cout << "Hello Concurrent World from thread!" << std::endl;
    }

    int main() {
    std::thread t(hello); // 创建并启动线程,执行 hello 函数
    t.join(); // 等待线程结束
    // t.detach(); // 或者分离它
    return 0;
    }

    • 线程函数可以是任何可调用对象(函数、函数对象、Lambda 表达式)。
    • 注意:如果线程函数抛出异常且未被捕获,程序将调用 std::terminate。

    4.2 传递参数

    参数按值复制或移动传递到线程内部存储。若需传递引用,需使用 std::ref 或 std::cref 包装。

    void modify(int &x) {
    x = 42;
    }

    int main() {
    int value = 10;
    std::thread t(modify, std::ref(value)); // 传递引用
    t.join();
    std::cout << value << std::endl; // 输出 42
    return 0;
    }

    4.3 等待与分离 (join, detach)

    与 pthread 概念一致:

    std::thread t(background_task);

    if (t.joinable()) { // 检查是否可以 join 或 detach
    t.join(); // 等待 t 结束
    // 或 t.detach(); // 分离 t
    }

    • 必须在 std::thread 对象销毁前决定是 join 还是 detach(或已 join/detach),否则程序终止 (std::terminate)。

    4.4 C++ 同步机制 (<mutex>, <condition_variable>)

    C++11 也提供了标准库的互斥锁和条件变量。

    4.4.1 互斥锁

    #include <mutex>
    std::mutex mtx;

    void safe_increment(int &counter) {
    std::lock_guard<std::mutex> lock(mtx); // RAII: 构造时加锁,析构时解锁
    ++counter;
    // 也可使用 std::unique_lock (更灵活,可延迟加锁、提前解锁,用于条件变量)
    }

    • std::lock_guard: 作用域锁,构造时锁定,析构时解锁。
    • std::unique_lock: 更灵活,支持延迟锁定、手动锁定解锁、转移所有权等。
    4.4.2 条件变量

    #include <mutex>
    #include <condition_variable>
    #include <queue>

    std::mutex mtx;
    std::condition_variable cv;
    std::queue<data_t> data_queue;

    // 生产者
    void producer() {
    while (/* … */) {
    data_t data = produce_data();
    {
    std::lock_guard<std::mutex> lk(mtx);
    data_queue.push(data);
    }
    cv.notify_one(); // 通知一个消费者
    }
    }

    // 消费者
    void consumer() {
    while (/* … */) {
    std::unique_lock<std::mutex> lk(mtx); // 必须用 unique_lock
    cv.wait(lk, []{ return !data_queue.empty(); }); // 等待条件成立,使用 Lambda 谓词检查
    data_t data = data_queue.front();
    data_queue.pop();
    lk.unlock(); // 可以提前解锁,处理数据时不需锁
    consume_data(data);
    }
    }

    • 使用 std::unique_lock 配合条件变量,因为 wait 方法需要能解锁和重新加锁。
    • cv.wait(lk, predicate) 等价于: while (!predicate()) {
      cv.wait(lk);
      }

    4.4.3 std::future 和 std::async

    用于异步执行任务并获取结果。

    #include <future>
    #include <iostream>

    int compute_value() {
    // … 耗时计算 …
    return 42;
    }

    int main() {
    std::future<int> fut = std::async(std::launch::async, compute_value); // 异步执行
    // … 做其他事情 …
    int result = fut.get(); // 获取结果(如果需要,会等待计算完成)
    std::cout << "Result: " << result << std::endl;
    return 0;
    }

    • std::async 可以指定策略 (std::launch::async 立即异步执行, std::launch::deferred 延迟执行直到调用 get/wait)。
    • fut.get() 阻塞直到结果可用,并只能调用一次。fut.wait() 只等待完成不取结果。

    5. 线程池 (Thread Pool)

    频繁创建销毁线程开销大。线程池预先创建一组线程,等待任务到来。任务被放入队列,空闲线程从队列中取出任务执行。

    核心组件:

  • 线程队列:一组工作线程。
  • 任务队列:存放待执行的任务(通常是函数对象)。
  • 同步机制:互斥锁保护任务队列,条件变量通知线程有新任务。
  • 简化版 C++11 线程池示例:

    #include <thread>
    #include <mutex>
    #include <condition_variable>
    #include <queue>
    #include <functional>
    #include <vector>
    #include <stdexcept>

    class ThreadPool {
    public:
    ThreadPool(size_t threads);
    ~ThreadPool();

    template<class F, class… Args>
    auto enqueue(F&& f, Args&&… args)
    -> std::future<typename std::result_of<F(Args…)>::type>;

    private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
    };

    // 构造函数:创建 threads 个工作线程
    ThreadPool::ThreadPool(size_t threads) : stop(false) {
    for (size_t i = 0; i < threads; ++i) {
    workers.emplace_back([this] {
    for (;;) {
    std::function<void()> task;
    {
    std::unique_lock<std::mutex> lock(this->queue_mutex);
    this->condition.wait(lock,
    [this]{ return this->stop || !this->tasks.empty(); });
    if (this->stop && this->tasks.empty())
    return;
    task = std::move(this->tasks.front());
    this->tasks.pop();
    }
    task(); // 执行任务
    }
    });
    }
    }

    // 析构函数:停止所有线程
    ThreadPool::~ThreadPool() {
    {
    std::unique_lock<std::mutex> lock(queue_mutex);
    stop = true;
    }
    condition.notify_all(); // 唤醒所有线程
    for (std::thread &worker : workers) {
    worker.join(); // 等待所有线程结束
    }
    }

    // 添加任务到队列
    template<class F, class… Args>
    auto ThreadPool::enqueue(F&& f, Args&&… args)
    -> std::future<typename std::result_of<F(Args…)>::type> {
    using return_type = typename std::result_of<F(Args…)>::type;

    auto task = std::make_shared<std::packaged_task<return_type()>>(
    std::bind(std::forward<F>(f), std::forward<Args>(args)…)
    );

    std::future<return_type> res = task->get_future();
    {
    std::unique_lock<std::mutex> lock(queue_mutex);
    if (stop)
    throw std::runtime_error("enqueue on stopped ThreadPool");
    tasks.emplace([task](){ (*task)(); }); // 将可调用对象放入任务队列
    }
    condition.notify_one(); // 通知一个工作线程
    return res;
    }

    使用:

    ThreadPool pool(4); // 4个工作线程

    auto result = pool.enqueue([](int answer) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return answer;
    }, 42);

    // … 做其他事 …
    std::cout << "Result: " << result.get() << std::endl; // 等待并获取结果

    6. 线程安全与常见陷阱

    • 数据竞争 (Data Race):多个线程同时读写同一内存位置且无同步。使用互斥锁或原子操作解决。
    • 死锁 (Deadlock):两个或多个线程相互等待对方持有的锁。避免方法:
      • 锁顺序:所有线程按相同顺序获取锁。
      • 避免嵌套锁:尽量避免在一个锁保护区内获取另一个锁。
      • 使用 std::lock 或 std::scoped_lock (C++17):一次锁定多个互斥量,避免死锁。
    • 虚假唤醒 (Spurious Wakeup):条件变量 wait 可能在没有 signal/broadcast 的情况下返回。因此必须在循环中检查条件。
    • 优先级反转 (Priority Inversion):低优先级线程持有高优先级线程需要的锁。可通过优先级继承协议解决 (如 pthread_mutexattr_setprotocol)。

    7. 调试与分析工具

    • Valgrind (Helgrind, DRD):检测线程错误(数据竞争、死锁)。
    • GDB:调试多线程程序 (info threads, thread <id>, break <function> thread <id>).
    • ps -eLf / top -H:查看进程的线程信息。
    • perf:性能分析,查看线程调度、CPU 使用、Cache 命中率等。

    总结

    理解 Linux 线程的核心在于掌握其生命周期管理(创建、等待、分离)和同步机制(互斥锁、条件变量)。C++ 的 std::thread 及相关库提供了更现代便捷的接口。线程池是管理并发任务的常用模式。始终警惕线程安全问题(数据竞争、死锁),并善用调试工具。通过合理的设计和实践,多线程编程能显著提升程序的性能和响应能力。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux线程实战:从等待到C++多线程
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!