C++ 线程

C++11 提供了强大的多线程支持,涵盖了线程的创建、同步、共享数据管理等,极大简化了多线程编程的复杂性。以下是 C++11 中所有基础线程知识、常用函数、应用场景和示例:

1. 基础概念

  • 线程:一个程序执行流的最小单位。每个线程都有自己的程序计数器、栈、寄存器等。
  • 多线程:程序可以同时执行多个任务,提高了程序的并发能力。
  • 并行与并发:并行是多个线程在多个处理器上同时运行;并发是多个线程在同一处理器上通过时间片轮转方式运行。

2. 常用库:<thread>

C++11 引入了 std::thread 类用于创建和管理线程,以及一系列相关的同步和互斥机制。

3. 线程的创建与管理

示例 1:基本线程创建与启动

#include <iostream>
#include <thread>

void threadFunction(int n) {
    for (int i = 0; i < n; ++i) {
        std::cout << "Thread executing\n";
    }
}

int main() {
    std::thread t1(threadFunction, 5); // 创建线程并传递参数
    t1.join();  // 等待线程完成
    return 0;
}

  • std::thread t1:创建线程并启动 threadFunction
  • join():阻塞主线程,直到子线程执行完毕。
  • 注意:如果不调用 join()detach(),程序会抛出异常。
注意事项:
  • 线程必须 join 或 detach:线程结束前必须调用 joindetach,否则会导致程序崩溃。
  • detach():分离线程,使其独立于主线程执行,不再等待它完成。
示例 2:分离线程 detach()

std::thread t2([] { std::cout << "Detached thread running\n"; });
t2.detach();  // 分离线程,主线程不会等待

4. 线程同步与数据共享

在多线程环境下访问共享数据时,可能会导致数据竞争问题,C++11 提供了多种同步机制来防止这些问题。

4.1 互斥锁 (std::mutex)

用于保护共享数据,保证同一时间只有一个线程能访问资源。

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

std::mutex mtx;  // 互斥量

void printMessage(const std::string& msg) {
    std::lock_guard<std::mutex> lock(mtx); // 加锁,自动释放
    std::cout << msg << std::endl;
}

int main() {
    std::thread t1(printMessage, "Hello from thread 1");
    std::thread t2(printMessage, "Hello from thread 2");

    t1.join();
    t2.join();
    return 0;
}

  • std::lock_guard<std::mutex>:在构造时自动加锁,析构时自动解锁,防止死锁。
4.2 条件变量 (std::condition_variable)

用于线程间的等待和通知机制。

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

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

void printMessage() {
    std::unique_lock<std::mutex> lock(mtx); // 加锁
    cv.wait(lock, []{ return ready; });     // 等待直到 ready 为 true
    std::cout << "Thread executing\n";
}

int main() {
    std::thread t(printMessage);

    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();  // 唤醒等待的线程

    t.join();
    return 0;
}

  • wait():等待条件变量满足。
  • notify_one():通知一个等待的线程。
  • notify_all():通知所有等待的线程。
4.3 std::futurestd::promise

在C++中,std::futurestd::promise用于实现线程间的异步通信。它们允许一个线程等待另一个线程的结果。std::future代表一个异步操作的结果,std::promise用于设置异步操作的结果并将其传递给std::future

std::async 示例

std::async可以启动一个异步任务并返回一个std::future对象,主线程可以通过future.get()来等待结果。

#include <iostream>
#include <future>

// 异步计算函数
int calculate(int x) {
    return x * x;
}

int main() {
    // 异步启动计算任务
    std::future<int> result = std::async(calculate, 10);

    // 主线程可以继续执行其他任务

    // 获取异步任务结果
    std::cout << "Result: " << result.get() << std::endl; // 阻塞等待,直到结果可用
    return 0;
}
 

要点解释:

  • std::async:启动一个异步任务,返回std::future对象。任务可以并发执行,结果会被异步计算。
  • future.get():阻塞主线程,直到任务完成并返回结果。
std::promise 示例

std::promise提供了一种主动设置值并将结果传递给std::future的方式。std::promise允许一个线程设置异步任务的结果,另一个线程通过std::future获取结果。

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

// 线程函数,使用 promise 设置结果
void calculate(std::promise<int>& prom, int x) {
    prom.set_value(x * x);  // 设置结果
}

int main() {
    // 创建一个 promise 对象
    std::promise<int> prom;

    // 获取与 promise 关联的 future 对象
    std::future<int> result = prom.get_future();

    // 创建一个线程,传入 promise 对象
    std::thread t(calculate, std::ref(prom), 10);

    // 获取线程结果
    std::cout << "Result: " << result.get() << std::endl;

    // 等待线程结束
    t.join();
    return 0;
}

要点解释:

  • std::promise<int>:用于设置结果的类,允许从一个线程将值传递给另一个线程。
  • std::future<int>:从std::promise获取,通过future.get()来获取结果。
  • promise.set_value():在执行线程中设置计算结果。
std::asyncstd::promise 对比
  • std::async:用于启动异步任务,并返回std::future,任务结果由系统管理,简单方便。
  • std::promise:提供了更细粒度的控制,用户可以主动设置结果值,并通过future获取,可以在复杂的线程间通信中使用。
常见使用场景
  • std::async:适用于简单的异步任务,自动管理任务的启动和结果获取。
  • std::promise:适用于复杂的线程通信场景,例如需要手动控制任务结果的设置,或者在不同线程之间传递异常信息。
注意事项
  • 异常处理:如果任务抛出异常,std::future.get()会将异常重新抛出。使用std::promise时,使用set_exception()可以在future.get()中获取异常。
  • 共享状态std::futurestd::promise之间共享状态,在future.get()之前必须保证promise.set_value()已被调用,否则会阻塞。

5.多线程、线程池

1. 多线程原理
1.1 并发与并行
  • 并发(Concurrency):在单核或多核处理器上同时管理多个任务的执行。虽然同一时间可能只运行一个任务,但通过快速切换看似多个任务一起进行。
  • 并行(Parallelism):在多核处理器上同时执行多个任务,每个核心运行一个任务,因此真正意义上同时执行多个任务。
1.2 多线程的优势
  • 提高CPU利用率:在多核处理器中,多线程可以有效利用多个CPU核心,提升并发处理能力。
  • 响应性:UI应用可以将繁重的任务放在后台线程中,从而保持界面的响应。
  • 任务分割:将复杂的任务分割为多个小任务,并行执行,以提高执行效率。
2. 线程池的设计

线程池(Thread Pool)是一种预先创建一定数量的线程,并重复使用这些线程来执行任务的机制。它的主要优点是避免频繁创建和销毁线程的开销。

2.1 线程池的基本原理

线程池中有以下几个主要元素:

  • 任务队列:用来存储需要执行的任务。任务可以是函数、类或者lambda表达式。
  • 工作线程:线程池中的线程循环从任务队列中获取任务并执行。
  • 任务分配:当有空闲线程时,将任务分配给该线程执行。
  • 任务同步:保证多个线程同时访问共享资源时不会产生冲突。
2.2 线程池的Demo

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

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

    // 添加任务到线程池
    template<class F, class... Args>
    void enqueue(F &&f, Args&&... args);

private:
    std::vector<std::thread> workers;   // 工作线程
    std::queue<std::function<void()>> tasks;  // 任务队列

    std::mutex queue_mutex;  // 同步任务队列
    std::condition_variable condition;
    bool stop;  // 控制线程池是否停止

    // 工作线程执行的函数
    void worker();
};

ThreadPool::ThreadPool(size_t threads) : stop(false) {
    for (size_t i = 0; i < threads; ++i) {
        workers.emplace_back(&ThreadPool::worker, this);  // 创建工作线程
    }
}

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();  // 通知所有线程结束
    for (std::thread &worker : workers) {
        worker.join();  // 等待所有线程结束
    }
}

void ThreadPool::worker() {
    while (true) {
        std::function<void()> task;
        {
            std::unique_lock<std::mutex> lock(this->queue_mutex);
            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();  // 执行任务
    }
}

template<class F, class... Args>
void ThreadPool::enqueue(F &&f, Args&&... args) {
    auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        tasks.emplace([task]() { task(); });
    }
    condition.notify_one();  // 通知一个线程处理任务
}

// 测试线程池
void printMessage(int id) {
    std::cout << "Thread " << id << " is working." << std::endl;
}

int main() {
    ThreadPool pool(4);  // 创建一个有4个线程的线程池
    for (int i = 0; i < 10; ++i) {
        pool.enqueue(printMessage, i);  // 添加任务
    }
    return 0;
}
 

2.3 线程池设计要点
  1. 任务队列同步:需要使用std::mutex来保护任务队列的同步访问,避免多个线程同时操作任务队列时发生竞争条件。
  2. 条件变量:使用std::condition_variable来通知线程有任务可以执行,避免线程忙等。
  3. 线程停止:在析构函数中需要等待所有线程完成后才能销毁线程池,确保程序的正常退出。

3. 并发控制

在多线程环境中,必须处理多个线程对共享资源的访问问题,常见的控制方式包括互斥锁、条件变量和读写锁等。

4. 线程安全的设计模式
4.1 生产者-消费者模式

生产者-消费者模式是多线程编程中非常常见的模式,用于多个线程生产和消费共享资源。

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

std::queue<int> dataQueue;
std::mutex mtx;
std::condition_variable cv;

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        dataQueue.push(i);
        cv.notify_one();  // 通知消费者有数据
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !dataQueue.empty(); });  // 等待数据
        int data = dataQueue.front();
        dataQueue.pop();
        std::cout << "Consumed: " << data << std::endl;
        if (data == 9) break;  // 结束消费
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons(consumer);

    prod.join();
    cons.join();

    return 0;
}
 

6. 注意事项与常见问题

  1. 数据竞争(Data Race):多个线程同时读写同一块内存时可能产生不可预测的结果,必须通过锁机制避免。
  2. 死锁(Deadlock):多个线程同时等待对方持有的锁时会发生死锁,避免死锁可以通过一致的锁顺序或者使用std::lock函数。
  3. 忙等待(Busy Waiting):线程不停地检查某个条件是否满足,这样会浪费CPU资源,应该使用std::condition_variable来实现等待。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值