C++ 多线程详解

C++ 多线程详解

一、C++ 多线程详解

多线程允许程序同时执行多个任务,充分利用多核处理器资源,提高程序性能,尤其在处理 I/O 密集型或并行计算任务时效果显著。C++11 标准引入了 <thread> 头文件,提供了对原生线程操作的支持。

1、创建线程

最基本的操作是创建一个线程来执行函数。使用 std::thread 类。

cpp 复制代码
#include <iostream>
#include <thread>

using namespace std;

void hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(hello); // 创建线程并执行 hello 函数
    t.join(); // 等待线程结束
    return 0;
}
  • std::thread t(func, args...):创建一个线程对象 t,它将执行函数 func,并可传递参数 args...
  • .join():主线程阻塞,等待子线程执行完毕。必须调用 join()detach() 之一。
  • .detach():分离线程,使其独立运行,主线程不再等待。分离后不能再 join

2、传递参数

向线程函数传递参数遵循普通函数参数传递规则,但需注意参数是按值复制传递 的。若要传递引用,需使用 std::refstd::cref

cpp 复制代码
#include <iostream>
#include <thread>

using namespace std;

void print_value(int& x) {
    x *= 2;
    std::cout << "Value in thread: " << x << '\n';
}

int main() {
    int num = 5;
    std::thread t(print_value, std::ref(num)); // 传递引用
    t.join();
    std::cout << "Value in main: " << num << '\n'; // 输出 10
    return 0;
}

3、 线程同步

当多个线程访问共享数据时,可能引发数据竞争 (Data Race),导致未定义行为。需要使用同步原语保护共享数据。

(1) 互斥锁 (std::mutex)

最基本的同步机制。通过 lock()unlock() 确保同一时间只有一个线程访问临界区。

cpp 复制代码
#include <mutex>
#include <iostream>
#include <thread>

using namespace std;

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock(); // 加锁
        ++counter;  // 临界区操作
        mtx.unlock(); // 解锁
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << '\n'; // 应为 200000
    return 0;
}

注意 :手动 lock/unlock 容易出错(如忘记解锁)。推荐使用 std::lock_guardstd::unique_lock,它们在构造时加锁,析构时自动解锁(RAII 思想)。

cpp 复制代码
void safe_increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时解锁
        ++counter;
    }
}

(2) 条件变量 (std::condition_variable)

用于线程间的条件等待和通知。常与互斥锁配合使用,实现线程等待特定条件成立。

cpp 复制代码
#include <condition_variable>
#include <queue>
#include <iostream>
#include <thread>

using namespace std;

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool done = false;

void producer() {
    for (int i = 0; i < 5; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::lock_guard<std::mutex> lock(mtx);
        data_queue.push(i);
        cv.notify_one(); // 通知一个等待线程
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        done = true;
    }
    cv.notify_all(); // 通知所有等待线程
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !data_queue.empty() || done; }); // 等待条件满足
        if (done && data_queue.empty()) break;
        int data = data_queue.front();
        data_queue.pop();
        lock.unlock();
        std::cout << "Consumed: " << data << '\n';
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons(consumer);
    prod.join();
    cons.join();
    return 0;
}
  • .wait(lock, predicate):释放锁 lock 并阻塞线程,直到被 notify_* 唤醒且 predicate 条件为 true。唤醒后会自动重新获得锁。
  • .notify_one():随机唤醒一个等待线程。
  • .notify_all():唤醒所有等待线程。

4、异步操作 (std::async, std::future)

<future> 头文件提供了更高级的异步操作接口。

  • std::async:异步执行函数,返回一个 std::future 对象。
  • std::future:存储异步操作的结果。通过 .get() 获取结果(会阻塞直到结果准备好)。
cpp 复制代码
#include <future>
#include <cmath>
#include <iostream>
#include <thread>

using namespace std;

double calculate_sqrt(double x) {
    return std::sqrt(x);
}

int main() {
    std::future<double> result = std::async(calculate_sqrt, 25.0);
    // ... 主线程可以同时做其他事情 ...
    std::cout << "Result: " << result.get() << '\n'; // 输出 5.0
    return 0;
}

5、原子操作 (std::atomic)

对于简单的数据类型(如 int, bool),可以使用原子类型避免锁的开销。原子操作保证该操作在多线程环境下是不可分割的。

cpp 复制代码
#include <atomic>
#include <iostream>
#include <thread>

using namespace std;

std::atomic<int> atomic_counter(0);

void atomic_increment() {
    for (int i = 0; i < 100000; ++i) {
        ++atomic_counter; // 原子递增
    }
}

int main() {
    std::thread t1(atomic_increment);
    std::thread t2(atomic_increment);
    t1.join();
    t2.join();
    std::cout << "Atomic Counter: " << atomic_counter << '\n'; // 应为 200000
    return 0;
}

6、线程局部存储 (thread_local)

使用 thread_local 关键字声明变量,使每个线程拥有该变量的独立副本。

cpp 复制代码
#include <iostream>
#include <thread>

using namespace std;

thread_local int thread_specific_id = 0;

void print_id() {
    std::cout << "Thread ID: " << thread_specific_id << '\n';
    thread_specific_id = std::rand() % 100; // 修改本线程的副本
}

int main() {
    std::thread t1(print_id); // 输出 0
    std::thread t2(print_id); // 输出 0
    t1.join();
    t2.join();
    return 0;
}

7、性能考量与最佳实践

  • 避免过度同步:锁会带来开销。尽量减少临界区范围。
  • 注意死锁:多个线程互相等待对方释放锁。避免嵌套锁或按固定顺序加锁。
  • 警惕虚假共享:多个线程频繁访问同一缓存行的不同变量,导致缓存失效。可以通过填充字节或重新组织数据结构来避免。
  • 优先使用高级抽象 :如 std::async, std::future, std::atomic 等,它们通常更安全高效。
  • 使用线程池:频繁创建销毁线程代价高。线程池可以复用线程。

二、代码示例

C++ 代码实现

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <chrono>

// 共享数据:缓冲区和同步工具
std::mutex mtx; // 互斥锁,保护共享缓冲区
std::condition_variable cv_producer; // 条件变量,用于生产者等待
std::condition_variable cv_consumer; // 条件变量,用于消费者等待
std::queue<int> buffer; // 缓冲区队列
const int BUFFER_SIZE = 5; // 缓冲区最大容量
const int NUM_ITEMS = 10; // 每个生产者生产的总项目数

// 生产者线程函数
void producer(int id) {
    for (int i = 0; i < NUM_ITEMS; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产延迟
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待缓冲区不满(条件变量检查)
        cv_producer.wait(lock, []{ return buffer.size() < BUFFER_SIZE; });
        
        int item = i * 100 + id; // 生成数据项(例如:生产者ID和项目索引组合)
        buffer.push(item);
        std::cout << "Producer " << id << " produced item: " << item << " (Buffer size: " << buffer.size() << ")" << std::endl;
        
        lock.unlock();
        cv_consumer.notify_one(); // 通知一个消费者
    }
}

// 消费者线程函数
void consumer(int id) {
    for (int i = 0; i < NUM_ITEMS; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待缓冲区不空(条件变量检查)
        cv_consumer.wait(lock, []{ return !buffer.empty(); });
        
        int item = buffer.front();
        buffer.pop();
        std::cout << "Consumer " << id << " consumed item: " << item << " (Buffer size: " << buffer.size() << ")" << std::endl;
        
        lock.unlock();
        cv_producer.notify_one(); // 通知一个生产者
        std::this_thread::sleep_for(std::chrono::milliseconds(150)); // 模拟消费延迟
    }
}

int main() {
    const int NUM_PRODUCERS = 2; // 生产者线程数
    const int NUM_CONSUMERS = 2; // 消费者线程数
    std::vector<std::thread> producers;
    std::vector<std::thread> consumers;

    // 创建生产者线程
    for (int i = 0; i < NUM_PRODUCERS; ++i) {
        producers.push_back(std::thread(producer, i));
    }

    // 创建消费者线程
    for (int i = 0; i < NUM_CONSUMERS; ++i) {
        consumers.push_back(std::thread(consumer, i));
    }

    // 等待所有线程完成
    for (auto& t : producers) {
        t.join();
    }
    for (auto& t : consumers) {
        t.join();
    }

    std::cout << "All threads completed. Program exits." << std::endl;
    return 0;
}

代码解释(逐步说明)

  1. 头文件导入

    • <thread>:用于创建和管理线程(std::thread)。
    • <mutex>:提供互斥锁(std::mutex)来保护共享数据。
    • <condition_variable>:实现条件变量(std::condition_variable)用于线程间协调。
    • <queue><vector>:用于缓冲区和线程管理。
    • <chrono>:用于模拟线程延迟。
  2. 共享数据和同步工具

    • std::queue<int> buffer:共享缓冲区,存储数据项。
    • BUFFER_SIZE = 5:缓冲区容量限制,防止溢出。
    • std::mutex mtx:互斥锁,确保对缓冲区的访问是原子的(避免数据竞争)。
    • std::condition_variable cv_producercv_consumer:条件变量,分别用于生产者和消费者的等待-通知机制。
  3. 生产者线程函数

    • 每个生产者(通过 id 标识)生产 NUM_ITEMS 个项目。
    • 使用 std::unique_lock 锁定互斥锁,并通过 cv_producer.wait 等待缓冲区不满的条件。
    • 生产数据项后,放入缓冲区,并通过 cv_consumer.notify_one() 通知一个消费者线程。
  4. 消费者线程函数

    • 每个消费者(通过 id 标识)消费 NUM_ITEMS 个项目。
    • 使用 std::unique_lock 锁定互斥锁,并通过 cv_consumer.wait 等待缓冲区不空的条件。
    • 取出数据项后,通过 cv_producer.notify_one() 通知一个生产者线程。
  5. 主函数

    • 创建多个生产者线程(NUM_PRODUCERS = 2)和消费者线程(NUM_CONSUMERS = 2)。
    • 使用 std::thread 启动线程,并通过 join() 等待所有线程结束。
    • 输出线程完成信息。

运行结果

cpp 复制代码
Producer 0 produced item: 0 (Buffer size: 1)
Consumer 1 consumed item: 0 (Buffer size: 0)
Producer 1 produced item: 1 (Buffer size: 1)
Consumer 0 consumed item: 1 (Buffer size: 0)
Producer 0 produced item: 100 (Buffer size: 1)
Producer 1 produced item: 101 (Buffer size: 2)
Consumer 1 consumed item: 100 (Buffer size: 1)
Consumer 0 consumed item: 101 (Buffer size: 0)
Producer 0 produced item: 200 (Buffer size: 1)
Producer 1 produced item: 201 (Buffer size: 2)
Consumer 1 consumed item: 200 (Buffer size: 1)
Producer 0 produced item: 300 (Buffer size: 2)
Consumer 0 consumed item: 201 (Buffer size: 1)
Producer 1 produced item: 301 (Buffer size: 2)
Producer 0 produced item: 400 (Buffer size: 3)
Producer 1 produced item: 401 (Buffer size: 4)
Consumer 1 consumed item: 300 (Buffer size: 3)
Consumer 0 consumed item: 301 (Buffer size: 2)
Producer 0 produced item: 500 (Buffer size: 3)
Producer 1 produced item: 501 (Buffer size: 4)
Consumer 1 consumed item: 400 (Buffer size: 3)
Consumer 0 consumed item: 401 (Buffer size: 2)
Producer 0 produced item: 600 (Buffer size: 3)
Producer 1 produced item: 601 (Buffer size: 4)
Producer 0 produced item: 700 (Buffer size: 5)
Consumer 1 consumed item: 500 (Buffer size: 4)
Producer 1 produced item: 701 (Buffer size: 5)
Consumer 0 consumed item: 501 (Buffer size: 4)
Producer 0 produced item: 800 (Buffer size: 5)
Consumer 1 consumed item: 600 (Buffer size: 4)
Producer 1 produced item: 801 (Buffer size: 5)
Consumer 0 consumed item: 601 (Buffer size: 4)
Producer 0 produced item: 900 (Buffer size: 5)
Consumer 1 consumed item: 700 (Buffer size: 4)
Consumer 0 consumed item: 701 (Buffer size: 3)
Producer 1 produced item: 901 (Buffer size: 4)
Consumer 1 consumed item: 800 (Buffer size: 3)
Consumer 0 consumed item: 801 (Buffer size: 2)
Consumer 0 consumed item: 900 (Buffer size: 1)
Consumer 1 consumed item: 901 (Buffer size: 0)
All threads completed. Program exits.

C:\Users\徐鹏\Desktop\新建文件夹\Project1\x64\Debug\Project1.exe (进程 35384)已退出,代码为 0 (0x0)。
要在调试停止时自动关闭控制台,请启用"工具"->"选项"->"调试"->"调试停止时自动关闭控制台"。
按任意键关闭此窗口. . .
相关推荐
近津薪荼8 分钟前
优选算法——前缀和(1):一维前缀和
c++·学习·算法
草莓熊Lotso2 小时前
Linux 基础 IO 初步解析:从 C 库函数到系统调用,理解文件操作本质
linux·运维·服务器·c语言·数据库·c++·人工智能
闻缺陷则喜何志丹2 小时前
P8699 [蓝桥杯 2019 国 B] 排列数|普及+
c++·数学·蓝桥杯·数论·洛谷·数列
D_evil__8 小时前
【Effective Modern C++】第三章 转向现代C++:16. 让const成员函数线程安全
c++
Queenie_Charlie9 小时前
前缀和的前缀和
数据结构·c++·树状数组
kokunka10 小时前
【源码+注释】纯C++小游戏开发之射击小球游戏
开发语言·c++·游戏
John_ToDebug12 小时前
浏览器内核崩溃深度分析:从 MiniDump 堆栈到 BindOnce UAF 机制(未完待续...)
c++·chrome·windows
txinyu的博客13 小时前
解析muduo源码之 SocketsOps.h & SocketsOps.cc
c++
ctyshr14 小时前
C++编译期数学计算
开发语言·c++·算法
努力写代码的熊大14 小时前
c++异常和智能指针
java·开发语言·c++