线程的创建.销毁

发布于:2025-09-12 ⋅ 阅读:(21) ⋅ 点赞:(0)

线程

线程的创建

在 C++ 中,线程的创建核心是通过std::thread类实现的,其构造函数需要传入一个可调用对象(Callable Object)作为线程入口。可调用对象包括普通函数、lambda 表达式、函数对象(functor)、类的成员函数等。下面详细介绍几种常见的线程创建方式:

一、使用普通函数创建线程

最基础的方式是将普通函数作为线程入口,可同时传递参数给函数。

#include <iostream>
#include <thread>

// 普通函数:线程入口
void print_info(int thread_id, const std::string& message) {
    std::cout << "线程 " << thread_id << ": " << message << std::endl;
}

int main() {
    // 创建线程:传入函数名和参数(参数按顺序传递)
    std::thread t1(print_info, 1, "Hello from thread 1");
    std::thread t2(print_info, 2, "Hello from thread 2");

    // 等待线程完成
    t1.join();
    t2.join();

    return 0;
}
    

说明

  • std::thread构造时,第一个参数是函数名,后续参数会被传递给该函数。
  • 若函数需要多个参数,直接在构造函数中按顺序添加即可。

二、使用 lambda 表达式创建线程

lambda 表达式适合编写简短的线程逻辑,尤其当需要捕获外部变量时非常方便。

#include <iostream>
#include <thread>

int main() {
    int base = 100;  // 外部变量

    // 用lambda表达式创建线程(捕获外部变量base)
    std::thread t([&base](int offset) {
        // 线程逻辑:使用捕获的base和传入的offset
        std::cout << "线程内计算:" << base + offset << std::endl;
    }, 50);  // 传递给lambda的参数(offset=50)

    t.join();  // 等待线程完成
    return 0;
}
    

说明

  • lambda 的捕获列表([&base])用于访问外部变量,&表示按引用捕获(可修改外部变量),=表示按值捕获(只读)。
  • lambda 后的参数(如50)会作为 lambda 的输入参数。

三、使用函数对象(Functor)创建线程

函数对象是重载了operator()的类 / 结构体,适合需要携带状态(成员变量)的线程逻辑。

#include <iostream>
#include <thread>

// 函数对象:重载operator()
struct Counter {
    int count;  // 携带的状态

    // 构造函数初始化状态
    Counter(int init) : count(init) {}

    // 线程入口:operator()
    void operator()(int step) {
        for (int i = 0; i < 5; ++i) {
            count += step;
            std::cout << "当前计数:" << count << std::endl;
        }
    }
};

int main() {
    // 创建函数对象(初始状态count=0)
    Counter counter(0);

    // 用函数对象创建线程,传递参数step=2
    std::thread t(std::ref(counter), 2);  // 注意用std::ref传递引用

    t.join();

    // 线程执行后,counter的状态已被修改
    std::cout << "最终计数:" << counter.count << std::endl;
    return 0;
}
    

说明

  • 函数对象的成员变量(如count)可用于存储线程的状态,避免使用全局变量。
  • 若需在线程中修改原对象(而非副本),需用std::ref传递引用(否则std::thread会复制对象)。

四、使用类的成员函数创建线程

当线程逻辑需要访问类的成员变量时,可将类的成员函数作为线程入口,需同时指定对象指针。

#include <iostream>
#include <thread>
#include <string>

class Worker {
private:
    std::string name;  // 成员变量

public:
    Worker(const std::string& n) : name(n) {}

    // 成员函数:线程入口
    void work(int task_id) {
        std::cout << "工人 " << name << " 正在执行任务 " << task_id << std::endl;
    }
};

int main() {
    Worker worker("Alice");  // 创建对象

    // 用成员函数创建线程:参数为(对象指针,成员函数地址,函数参数)
    std::thread t(&Worker::work, &worker, 1001);  // &worker是对象指针

    t.join();
    return 0;
}
    

说明

  • std::thread构造时,第一个参数是成员函数地址(&Worker::work),第二个参数是对象指针(&worker),后续参数是成员函数的参数。
  • 若对象是动态分配的(new Worker(...)),则传递堆对象的指针即可。

关键注意事项

  1. 线程必须被 join 或 detach
    std::thread对象销毁前,必须调用join()(等待线程结束)或detach()(分离线程,使其独立运行),否则会触发std::terminate()终止程序。

  2. 参数传递的拷贝问题
    线程构造时传递的参数会被拷贝到线程内部,若需传递引用,需用std::refstd::cref(常量引用),但需确保引用的对象生命周期长于线程。

  3. 线程入口的生命周期
    若线程入口是临时对象(如 lambda 或函数对象),需确保其生命周期覆盖线程执行期,避免悬空引用。

总结

C++ 线程创建的核心是通过std::thread绑定可调用对象,不同方式的适用场景:

  • 普通函数:适合简单、无状态的线程逻辑。
  • lambda 表达式:适合简短逻辑或需要捕获外部变量的场景。
  • 函数对象:适合需要携带状态的复杂逻辑。
  • 成员函数:适合面向对象编程中,线程逻辑需访问类成员的场景。

线程的销毁

我们使用std::thread创建的线程对象是进程中的子线程,一般进程中还有主线程,在程序中就是main线程,那么当我们创建线程后至少是有两个线程的,那么两个线程谁先执行完毕谁后执行完毕,这是随机的,但是当进程执行结束之后,主线程与子线程都会执行完毕,进程会回收线程拥有的资源。并且,主线程main执行完毕,其实整个进程也就执行完毕了。一般我们有两种方式让子线程结束,一种是主线程等待子线程执行完毕,我们使用join函数,让主线程回收子线程的资源;另外一种是子线程与主线程分离,我们使用detach函数,此时子线程驻留在后台运行,这个子线程就相当于被C++运行时库接管,子线程执行完毕后,由运行时库负责清理该线程相关的资源。使用detach之后,表明就失去了对子线程的控制。

void func()
{
    cout << "void func()" << endl;
    cout << "I'm child thread" << endl;
}

void test()
{
    cout << "I'm main thread" << endl;
    thread th1(func);
    th1.join();//主线程等待子线程
}

线程的状态

线程类中有一成员函数joinable,可以用来检查线程的状态。如果该函数为true,表示可以使用join()或者detach()函数来管理线程生命周期。

void test()
{
    thread t([]{
        cout << "Hello, world!" << endl;
    });
 
    if (t.joinable()) {
        t.detach();
    }
}

void test2()
{
    thread th1([]{
        cout << "Hello, world!" << endl;
    });
 
    if (t.joinable()) {
        t.join();
    }
}

线程id

为了唯一标识每个线程,可以给每个线程一个id,类型为std::thread::id,可以使用成员函数get_id()进行获取。

void test()
{
    thread th1([](){
        cout << "子线程ID:" << std::this_thread::get_id() << endl;
    });
    th1.join();
}

互斥锁mutex

互斥锁是一种同步原语,用于协调多个线程对共享资源的访问。互斥锁的作用是保证同一时刻只有一个线程可以访问共享资源,其他线程需要等待互斥锁释放后才能访问。在多线程编程中,多个线程可能同时访问同一个共享资源,如果没有互斥锁的保护,就可能出现数据竞争等问题。

然而,互斥锁的概念并不陌生,在Linux下,POSIX标准中也有互斥锁的概念,这里我们说的互斥锁是C++11语法层面提出来的概念,是C++语言自身的互斥锁std::mutex,互斥锁只有两种状态:上锁与解锁。

2、头文件

#include <mutex>
class mutex;

3、常用函数接口

3.1、构造函数
constexpr mutex() noexcept;
mutex( const mutex& ) = delete;
3.2、上锁
void lock();
3.3、尝试上锁
bool try_lock();
3.4、解锁
void unlock();
3.5、使用示例
int gCount = 0;
mutex mtx;//初始化互斥锁
​
void threadFunc()
{
    for(int idx = 0; idx < 1000000; ++idx)
    {
        mtx.lock();//上锁
        ++gCount;
        mtx.unlock();//解锁
    }
}
​
int main(int argc, char *argv[])
{
    thread th1(threadFunc);
    thread th2(threadFunc);
​
    th1.join();
    th2.join();
    cout << "gCount = " << gCount << endl;
    
    return 0;
}

三、lock_guard与unique_lock

在 C++ 多线程编程中,std::lock_guard 和 std::unique_lock 都是用于管理互斥锁(std::mutex)的RAII 风格工具类,核心作用是自动加锁和解锁,避免手动操作锁导致的死锁(如忘记解锁、异常时未释放锁等问题)。但它们的灵活性和适用场景有显著区别。

一、核心共同点

  • 都遵循RAII 原则:构造时获取锁,析构时自动释放锁(无论正常退出还是异常退出)。
  • 都用于保护临界区,防止多线程并发访问共享资源导致的数据竞争。

二、关键区别与适用场景

特性 std::lock_guard std::unique_lock
灵活性 简单,功能有限 灵活,支持更多操作
手动解锁 不支持(只能通过析构函数自动解锁) 支持(通过 unlock() 手动解锁)
延迟锁定 不支持(构造时必须锁定) 支持(通过 std::defer_lock 延迟锁定)
尝试锁定 不支持 支持(通过 std::try_to_lock 尝试锁定)
所有权转移 不支持(不可复制、不可移动) 支持(可移动,不可复制)
性能开销 更低(轻量级) 略高(因灵活性带来的额外状态管理)
适用场景 简单临界区(全程需要锁定) 复杂场景(如条件变量、中途解锁、延迟锁定等)

三、详细说明与示例

1. std::lock_guard:简单场景的首选

lock_guard 是轻量级锁管理工具,设计用于最简单的场景:进入临界区时加锁,离开时解锁,全程不需要手动干预。

特点

  • 构造函数必须锁定互斥量(要么直接锁定,要么接受一个已锁定的互斥量,通过 std::adopt_lock 标记)。
  • 没有 unlock() 方法,只能在析构时自动解锁(通常是离开作用域时)。
  • 不可复制、不可移动,所有权无法转移。

示例

#include <mutex>
#include <iostream>

std::mutex mtx;
int shared_data = 0;

void increment() {
    // 构造时自动锁定mtx,离开作用域(函数结束)时析构,自动解锁
    std::lock_guard<std::mutex> lock(mtx);
    
    // 临界区:安全访问共享资源
    shared_data++;
    std::cout << "当前值: " << shared_data << std::endl;
    
    // 无需手动解锁,lock析构时自动处理
}

适用场景

  • 临界区逻辑简单,从进入到退出全程需要锁定。
  • 不需要中途解锁、延迟锁定等复杂操作。
  • 追求最小性能开销。
2. std::unique_lock:复杂场景的灵活选择

unique_lock 是功能更全面的锁管理工具,支持手动解锁、延迟锁定、尝试锁定等操作,适合需要灵活控制锁状态的场景。

特点

  • 支持延迟锁定:通过 std::defer_lock 标记,构造时不锁定互斥量,后续可通过 lock() 手动锁定。
  • 支持手动解锁:通过 unlock() 方法中途释放锁,之后可再次通过 lock() 重新锁定。
  • 支持尝试锁定:通过 std::try_to_lock 标记,尝试锁定互斥量(成功返回 true,失败不阻塞)。
  • 支持所有权转移:可通过移动语义(std::move)转移锁的所有权(不可复制)。
  • 是条件变量(std::condition_variable)的必需参数:条件变量的 wait() 方法需要 unique_lock 作为参数,因为 wait() 会在等待时释放锁,被唤醒时重新获取锁(这要求锁可以手动解锁和锁定)。

示例 1:延迟锁定与手动解锁

#include <mutex>
#include <iostream>

std::mutex mtx;

void complex_operation() {
    // 延迟锁定:构造时不锁定,仅关联互斥量
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    
    // 做一些不需要锁定的操作
    std::cout << "准备锁定..." << std::endl;
    
    // 手动锁定
    lock.lock();
    std::cout << "已锁定,执行临界区操作..." << std::endl;
    
    // 中途手动解锁(释放锁,允许其他线程访问)
    lock.unlock();
    std::cout << "临时解锁,执行其他操作..." << std::endl;
    
    // 再次锁定
    lock.lock();
    std::cout << "再次锁定,完成剩余操作..." << std::endl;
    
    // 析构时自动解锁(若当前处于锁定状态)
}

示例 2:与条件变量配合

#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    
    // 等待条件满足:会释放锁并阻塞,被唤醒时重新获取锁
    cv.wait(lock, []{ return data_ready; });
    
    // 条件满足,执行消费操作
    std::cout << "数据已准备好,开始处理..." << std::endl;
}

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        data_ready = true; // 生产数据
    } // 离开作用域,自动解锁
    
    cv.notify_one(); // 通知消费者
}

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

适用场景

  • 需要中途解锁(如临界区中间有耗时操作但无需锁定)。
  • 需要延迟锁定(如先做准备工作,再根据条件决定是否锁定)。
  • 需要与条件变量配合(wait() 必须使用 unique_lock)。
  • 需要转移锁的所有权(如将锁传递给其他函数)。

四、总结

  • 优先使用 std::lock_guard:当场景简单,临界区全程需要锁定时,它更轻量、更高效。
  • 使用 std::unique_lock:当需要灵活性(手动解锁、延迟锁定、配合条件变量等)时,牺牲少量性能换取功能。

两者的核心目标都是安全管理锁的生命周期,避免手动操作锁导致的错误,选择时主要依据场景的复杂度和灵活性需求。

条件变量condition_variable

在 C++ 多线程编程中,std::condition_variable(条件变量)是用于线程间同步的核心机制,它允许线程在满足特定条件前阻塞等待,当条件满足时被其他线程唤醒,从而实现高效的协作(避免无效轮询)。

一、核心作用

条件变量解决的核心问题:让线程在 “条件不满足” 时进入休眠状态,在 “条件满足” 时被唤醒继续执行,避免线程通过 “轮询”(反复检查条件)浪费 CPU 资源。

例如:

  • 消费者线程等待生产者线程生成数据(“数据就绪” 是条件)。
  • 主线程等待子线程完成初始化(“初始化完成” 是条件)。

二、核心 API 与工作机制

std::condition_variable 定义在 <condition_variable> 头文件中,核心方法如下:

方法 作用
wait(lock, pred) 阻塞当前线程,释放锁并等待被唤醒;被唤醒后重新获取锁,检查pred是否为true,若为true则继续执行,否则重新阻塞。
notify_one() 唤醒一个正在等待该条件变量的线程(若有)。
notify_all() 唤醒所有正在等待该条件变量的线程。
关键细节:
  1. 必须配合互斥锁:条件变量的操作必须与互斥锁(std::mutex)结合,且必须使用 std::unique_lock(而非 std::lock_guard),因为 wait() 过程需要先释放锁、被唤醒后重新获取锁unique_lock 支持手动解锁 / 加锁,lock_guard 不支持)。

  2. 处理 “虚假唤醒”:操作系统可能在无明确通知时唤醒线程(虚假唤醒),因此 wait() 必须配合条件谓词(pred) 使用,确保只有当条件真正满足时才继续执行。

三、工作流程(以生产者 - 消费者为例)

  1. 消费者线程

    • 锁定互斥锁,检查条件(如 “数据是否就绪”)。
    • 若条件不满足,调用 wait():释放锁并阻塞等待。
    • 被唤醒后,重新获取锁,再次检查条件(避免虚假唤醒)。
    • 条件满足时,执行操作(如消费数据)。
  2. 生产者线程

    • 锁定互斥锁,修改共享资源(如生成数据)。
    • 调用 notify_one() 或 notify_all() 唤醒等待的消费者。
    • 释放锁(由 unique_lock 或 lock_guard 自动完成)。

四、完整示例:生产者 - 消费者模型

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>

// 共享队列(缓冲区)
std::queue<int> buffer;
const int MAX_SIZE = 5;  // 缓冲区最大容量

// 同步工具
std::mutex mtx;
std::condition_variable cv_producer;  // 生产者等待的条件变量(缓冲区不满)
std::condition_variable cv_consumer;  // 消费者等待的条件变量(缓冲区非空)

// 生产者:向缓冲区添加数据
void producer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待缓冲区不满(若满则阻塞)
        cv_producer.wait(lock, []{ return buffer.size() < MAX_SIZE; });
        
        // 生产数据
        int data = id * 100 + i;
        buffer.push(data);
        std::cout << "生产者 " << id << " 生产: " << data 
                  << ",缓冲区大小: " << buffer.size() << std::endl;
        
        // 通知消费者:缓冲区非空
        cv_consumer.notify_one();
        
        // 模拟生产耗时
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

// 消费者:从缓冲区取出数据
void consumer(int id) {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待缓冲区非空(若空则阻塞)
        cv_consumer.wait(lock, []{ return !buffer.empty(); });
        
        // 消费数据
        int data = buffer.front();
        buffer.pop();
        std::cout << "消费者 " << id << " 消费: " << data 
                  << ",缓冲区大小: " << buffer.size() << std::endl;
        
        // 通知生产者:缓冲区不满
        cv_producer.notify_one();
        
        // 模拟消费耗时
        std::this_thread::sleep_for(std::chrono::milliseconds(150));
    }
}

int main() {
    // 创建2个生产者和2个消费者
    std::thread p1(producer, 1);
    std::thread p2(producer, 2);
    std::thread c1(consumer, 1);
    std::thread c2(consumer, 2);
    
    // 等待所有线程完成
    p1.join();
    p2.join();
    c1.join();
    c2.join();
    
    return 0;
}

五、关键注意事项

  1. 必须使用 unique_lockwait() 方法的第一个参数必须是 std::unique_lock<std::mutex>,因为 wait() 内部会执行 “解锁→阻塞→被唤醒后重新加锁” 的操作,unique_lock 支持这种灵活的锁状态管理(lock_guard 不支持手动解锁,无法配合 wait())。

  2. 条件谓词不可省略:即使你认为 “不会有虚假唤醒”,也必须在 wait() 中传入条件谓词(第二个参数)。例如:

    // 错误:未处理虚假唤醒
    cv.wait(lock); 
    
    // 正确:确保条件满足才继续
    cv.wait(lock, []{ return condition; }); 
    
  3. notify_one() 与 notify_all() 的选择

    • notify_one():唤醒一个等待线程,适用于 “只有一个线程能处理” 的场景(如缓冲区只有一个数据)。
    • notify_all():唤醒所有等待线程,适用于 “多个线程都能处理” 的场景(如广播一个全局事件)。过度使用 notify_all() 可能导致线程唤醒后竞争锁,浪费资源。
  4. 避免持有锁时长时间操作:唤醒线程后,应尽快释放锁(完成临界区操作),避免其他线程被唤醒后因无法获取锁而阻塞。

  5. 生命周期管理:确保条件变量在所有等待线程退出前保持有效,避免访问已销毁的条件变量。

六、总结

std::condition_variable 是多线程协作的高效工具,通过 “等待 - 通知” 机制替代轮询,减少 CPU 浪费。其核心是:线程在条件不满足时阻塞,条件满足时被唤醒,配合互斥锁和条件谓词确保同步安全。典型应用包括生产者 - 消费者模型、线程池任务调度、事件驱动同步等。


网站公告

今日签到

点亮在社区的每一天
去签到