玩转C++11多线程:让你的程序飞起来的std::thread终极指南

大家好,我是小康。

你还在为 C++ 多线程编程发愁吗?别担心,今天咱们就用大白话彻底搞定std::thread!看完这篇,保证你对C++11多线程的理解从"一脸懵逼"变成"原来如此"!

前言:为啥要学多线程?

想象一下,你正在厨房做饭。如果你是单线程工作,那就只能先切菜,切完再炒菜,炒完再煮汤...一项一项按顺序来。但现实中的你肯定是多线程操作啊:锅里炒着菜,同时旁边的电饭煲在煮饭,热水壶在烧水,也许你还能同时看看手机...这就是多线程的威力!

在程序世界里,多线程就像多了几个"分身",可以同时处理不同的任务,充分利用多核CPU的性能,让程序跑得飞快。特别是现在谁的电脑不是多核啊,不用多线程简直是浪费资源!

C++11标准终于给我们带来了官方的多线程支持------std::thread,从此不用再依赖操作系统特定的API或第三方库,写多线程程序方便多了!

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

第一步:创建你的第一个线程

好,闲话少说,直接上代码看看怎么创建一个线程:

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

// 这是我们要在新线程中执行的函数
void hello_thread() {
    std::cout << "哈喽,我是一个新线程!" << std::endl;
}

int main() {
    // 创建一个执行hello_thread函数的线程
    std::thread t(hello_thread);

    // 主线程打个招呼
    std::cout << "主线程:我正在等一个线程干活..." << std::endl;

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

    std::cout << "所有线程都结束了,程序退出!" << std::endl;
    return 0;
}

输出结果可能是:

plain 复制代码
主线程:我正在等一个线程干活...
哈喽,我是一个新线程!
所有线程都结束了,程序退出!

或者是:

plain 复制代码
哈喽,我是一个新线程!
主线程:我正在等一个线程干活...
所有线程都结束了,程序退出!

咦?为啥输出顺序不固定?因为两个线程是并发执行的,谁先打印完全看 CPU 的心情!这就是多线程的特点------不确定性。

代码解析:

创建线程超简单,就一行代码:std::thread t(hello_thread);。线程一创建就立刻开始执行了,就像放出去的炮弹,发射了就收不回来了。

t.join() 是啥意思呢?它相当于说:"主线程,你等等这个新线程,等它干完活再继续"。如果没有这行,主线程可能提前结束,程序就崩溃了!

给线程传参数

线程不能只会喊"哈喽"吧?我们得给它点实际任务,还得告诉它一些参数。传参数超简单:

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

void greeting(std::string name, int times) {
    for (int i = 0; i < times; i++) {
        std::cout << "你好," << name << "!这是第 " << (i+1) << " 次问候!" << std::endl;
    }
}

int main() {
    // 创建线程并传递参数
    std::thread t(greeting, "张三", 3);

    std::cout << "主线程:我让线程去问候张三了..." << std::endl;

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

    std::cout << "问候完毕!" << std::endl;
    return 0;
}

输出结果:

plain 复制代码
主线程:我让线程去问候张三了...
你好,张三!这是第 1 次问候!
你好,张三!这是第 2 次问候!
你好,张三!这是第 3 次问候!
问候完毕!

传参就像普通函数调用一样,直接在线程构造函数后面加参数就行。但是有个坑:参数是"拷贝"到线程中的,所以小心对象的复制开销!

用Lambda表达式创建线程

每次都要单独写个函数太麻烦了,有没有简单方法?有啊,用Lambda表达式!

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

int main() {
    // 使用Lambda表达式创建线程
    std::thread t([]() {
        std::cout << "我是Lambda创建的线程,帅不帅?" << std::endl;
        for (int i = 5; i > 0; i--) {
            std::cout << "倒计时: " << i << std::endl;
        }
    });

    std::cout << "主线程:Lambda线程正在倒计时..." << std::endl;

    t.join();

    std::cout << "倒计时结束!" << std::endl;
    return 0;
}

Lambda表达式就像一个临时小函数,用完就扔,方便得很!特别适合那种只用一次的简单逻辑。

多线程通信的坑:数据竞争

多线程编程最大的坑就是多个线程同时访问同一数据时会出现"数据竞争"。来看个例子:

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

int counter = 0; // 共享的计数器

void increment_counter(int times) {
    for (int i = 0; i < times; i++) {
        counter++; // 危险操作!多线程同时修改
    }
}

int main() {
    std::vector<std::thread> threads;

    // 创建5个线程,每个线程将counter增加10000次
    for (int i = 0; i < 5; i++) {
        threads.push_back(std::thread(increment_counter, 10000));
    }

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

    std::cout << "理论上counter应该等于:" << 5 * 10000 << std::endl;
    std::cout << "实际上counter等于:" << counter << std::endl;

    return 0;
}

输出可能是:

plain 复制代码
理论上counter应该等于:50000
实际上counter等于:42568

咦?怎么少了那么多?因为 counter++ 看起来是一条语句,但实际上分三步:读取counter的值、加1、写回counter。当多个线程同时执行这个操作,就会互相"踩踏",导致最终结果小于预期。

这就是臭名昭著的数据竞争问题,解决方法有互斥锁、原子操作等,后面会讲。

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

线程管理的基本操作

1. join() - 等待线程完成

我们已经见过 join() 了,它会阻塞当前线程,直到目标线程执行完毕。

cpp 复制代码
std::thread t(some_function);
t.join(); // 等待t完成

2. detach() - 让线程"自生自灭"

有时候,我们启动一个线程后不想等它了,可以用 detach() 让它独立运行:

cpp 复制代码
std::thread t(background_task);
t.detach(); // 线程在后台独立运行
std::cout << "主线程不管子线程了,继续自己的事" << std::endl;

detach后的线程称为"分离线程"或"守护线程",它会在后台默默运行,直到自己的任务完成。但要小心:如果主程序结束了,这些分离线程会被强制终止!

3. joinable() - 检查线程是否可等待

在join之前,最好检查一下线程是否可以被等待:

cpp 复制代码
std::thread t(some_function);
// ... 一些代码 ...
if (t.joinable()) {
    t.join();
}

这避免了对已经 join 或 detach 过的线程再次操作,否则会崩溃。

防止忘记join:RAII风格的线程包装器

C++的经典模式:用对象的生命周期管理资源。我们可以创建一个线程包装器,在析构时自动join:

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

class thread_guard {
private:
std::thread& t;

public:
// 构造函数,接收线程引用
explicit thread_guard(std::thread& t_) : t(t_) {}

// 析构函数,自动join线程
~thread_guard() {
    if (t.joinable()) {
        t.join();
    }
}

// 禁止复制
thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
};

void some_function() {
    std::cout << "线程工作中..." << std::endl;
}

int main() {
    std::thread t(some_function);
    thread_guard g(t); // 创建守卫对象

    // 即使这里抛出异常,thread_guard的析构函数也会被调用,确保t被join
    std::cout << "主线程继续工作..." << std::endl;

    return 0; // 函数结束,g被销毁,自动调用t.join()
}

这样即使发生异常,或者开发者忘记手动join,线程也会被正确等待,避免程序崩溃。

线程间的互斥:mutex

前面说到数据竞争问题,最常用的解决方案是互斥锁(mutex):

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

int counter = 0;
std::mutex counter_mutex; // 保护counter的互斥锁

void safe_increment(int times) {
    for (int i = 0; i < times; i++) {
        counter_mutex.lock(); // 锁定互斥锁
        counter++; // 安全操作
        counter_mutex.unlock(); // 解锁
    }
}

int main() {
    std::vector<std::thread> threads;

    // 创建5个线程
    for (int i = 0; i < 5; i++) {
        threads.push_back(std::thread(safe_increment, 10000));
    }

    // 等待所有线程
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "现在counter正确等于:" << counter << std::endl;
    return 0;
}

输出:

plain 复制代码
现在counter正确等于:50000

太好了!结果正确了。但这样手动lock/unlock很容易出错,如果忘记unlock或者发生异常,就会死锁。所以更推荐使用RAII风格的std::lock_guard

cpp 复制代码
void better_safe_increment(int times) {
    for (int i = 0; i < times; i++) {
        std::lock_guard<std::mutex> lock(counter_mutex); // 自动锁定和解锁
        counter++;
    }
}

lock_guard在构造时锁定互斥锁,在析构时自动解锁,无论是正常退出还是异常退出都能保证互斥锁被释放。

高级话题:条件变量

线程间的同步不只有互斥,有时我们需要一个线程等待某个条件满足。条件变量就是干这个的:

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

std::queue<int> data_queue; // 共享的数据队列
std::mutex queue_mutex;
std::condition_variable data_cond;

// 生产者线程
void producer() {
    for (int i = 0; i < 5; i++) {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            data_queue.push(i); // 添加数据
            std::cout << "生产了数据: " << i << std::endl;
        } // 锁在这里释放

        data_cond.notify_one(); // 通知一个等待的消费者
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 稍微等一下
    }
}

// 消费者线程
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(queue_mutex);
        // 等待队列有数据(避免虚假唤醒)
        data_cond.wait(lock, [] { return !data_queue.empty(); });

        // 取出并处理数据
        int value = data_queue.front();
        data_queue.pop();

        std::cout << "消费了数据: " << value << std::endl;

        if (value == 4) break; // 收到最后一个数据后退出
    }
}

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

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

    std::cout << "所有数据都生产和消费完毕!" << std::endl;
    return 0;
}

这个例子展示了经典的"生产者-消费者"模式:生产者往队列里放数据,消费者从队列里取数据。条件变量确保消费者不会在队列为空时尝试取数据。

线程与异常安全

在多线程程序中处理异常尤为重要。如果线程执行时抛出异常,且没被捕获,整个程序会直接崩溃!以下是安全处理方式:

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

void function_that_throws() {
    throw std::runtime_error("故意抛出的异常!");
}

void thread_function() {
    try {
        function_that_throws();
    } catch (const std::exception& e) {
        std::cout << "线程捕获到异常: " << e.what() << std::endl;
    }
}

int main() {
    std::thread t(thread_function);
    t.join();

    std::cout << "程序正常结束" << std::endl;
    return 0;
}

输出:

plain 复制代码
线程捕获到异常: 故意抛出的异常!
程序正常结束

记住:每个线程都有自己独立的调用栈,异常不会跨线程传播!在哪个线程抛出,就必须在哪个线程捕获。

实用技巧

1. 获取线程ID

每个线程都有唯一的ID,用于标识:

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

void print_id() {
    std::cout << "线程ID: " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t1(print_id);
    std::thread t2(print_id);

    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
    std::cout << "t1的ID: " << t1.get_id() << std::endl;
    std::cout << "t2的ID: " << t2.get_id() << std::endl;

    t1.join();
    t2.join();

    return 0;
}

2. 线程休眠

有时需要让线程暂停一会儿:

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

void sleepy_thread() {
    std::cout << "我要睡觉了..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "睡醒了!" << std::endl;
}

int main() {
    std::thread t(sleepy_thread);
    t.join();
    return 0;
}

3. 获取CPU核心数

为了根据CPU核心优化线程数量:

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

int main() {
    unsigned int num_cores = std::thread::hardware_concurrency();
    std::cout << "你的CPU有 " << num_cores << " 个硬件线程(核心)" << std::endl;

    // 根据核心数创建线程
    unsigned int num_threads = num_cores;
    std::cout << "将创建 " << num_threads << " 个线程以充分利用CPU" << std::endl;

    return 0;
}

实际案例:并行图像处理

来个实际应用案例:用多线程加速图像处理。这里我们简化为操作一个二维数组:

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <algorithm>
#include <chrono>

// 模拟图像处理函数
void process_image_part(std::vector<std::vector<int>>& image, int start_row, int end_row) {
    for (int i = start_row; i < end_row; i++) {
        for (int j = 0; j < image[i].size(); j++) {
            // 模拟复杂处理,例如图像模糊
            image[i][j] = (image[i][j] + 10) * 2;
            // 模拟耗时操作
            std::this_thread::sleep_for(std::chrono::microseconds(1));
        }
    }
}

int main() {
    // 创建模拟图像 (1000x1000)
    std::vector<std::vector<int>> image(1000, std::vector<int>(1000, 5));

    // 获取CPU核心数
    unsigned int num_cores = std::thread::hardware_concurrency();
    unsigned int num_threads = num_cores; // 使用和核心数一样多的线程

    std::cout << "使用 " << num_threads << " 个线程处理图像..." << std::endl;

    // 开始计时
    auto start_time = std::chrono::high_resolution_clock::now();

    // 创建线程并分配工作
    std::vector<std::thread> threads;
    int rows_per_thread = image.size() / num_threads;

    for (unsigned int i = 0; i < num_threads; i++) {
        int start_row = i * rows_per_thread;
        int end_row = (i == num_threads - 1) ? image.size() : (i + 1) * rows_per_thread;

        threads.push_back(std::thread(process_image_part, std::ref(image), start_row, end_row));
    }

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

    // 结束计时
    auto end_time = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);

    std::cout << "图像处理完成!耗时: " << duration.count() << " 毫秒" << std::endl;

    // 验证结果(只显示部分)
    std::cout << "处理后的图像样本(左上角): " << std::endl;
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            std::cout << image[i][j] << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

这个例子展示了如何将大型任务分解成多个小块,分配给多个线程并行处理,充分利用多核CPU的优势。

多线程的最佳实践

  1. 保持简单:多线程代码难以调试,尽量简化每个线程的工作。
  2. 避免共享状态:尽可能减少线程间共享的数据,以降低同步复杂度。
  3. 适当的线程数量:通常等于或略多于CPU核心数,太多反而会因为频繁切换导致性能下降。
  4. 使用高级抽象 :考虑使用std::asyncstd::future或线程池,而不是直接管理线程。
  5. 测试和调试:在各种条件下测试多线程代码,包括高负载和边缘情况。

结语

从此,你已经掌握了C++11多线程编程的基础知识!从创建线程到传递参数,从互斥锁到条件变量,从简单示例到实际应用。多线程编程确实比单线程复杂,但掌握了这些技能,你就能写出更高效、响应更快的程序。

记住,多线程编程需要实践和耐心。开始时可能会遇到各种莫名其妙的问题,但随着经验积累,你会越来越熟练。不妨从简单的多线程程序开始,逐步挑战更复杂的场景。

最后的建议:写多线程程序时,时刻保持清醒和警惕,因为多线程bug可能是最难调试的bug之一!

愿你的多线程之旅愉快且充满成就感!


🎮 彩蛋时间!

哈喽,开发者朋友们!看到这里,你已经成功将多线程这个"魔法"收入囊中了!是不是感觉自己的代码突然有了开挂的能力?

如果这篇文章帮到了你,不妨点赞、收藏和关注。你的每次互动,都是我熬夜码字的动力源泉啊!⚡

🚀 想继续提升你的C++武器库?

关注我的公众号「跟着小康学编程」,还有更多干货等着你:

  • 指针让你头秃? 智能指针详解,从此告别段错误噩梦
  • STL容器傻傻分不清? 一文带你彻底搞懂它们的适用场景
  • 设计模式太抽象? 用生活案例秒懂,代码立马高大上
  • Linux系统编程? 从零带你玩转文件、进程、网络编程

每周更新,拒绝空谈,全是能立马用上的实战技巧!

📱 一键关注不迷路

扫码即可关注,让我们一起把 C++ 学得明明白白,用得舒舒服服!

记住:编程难?只是你还没遇到对的老师!跟着小康学,C++没那么难!

💬 加入我们的C++修仙群

遇到 Bug 卡住了?项目思路不清晰?在我们的交流群里,不仅有我这个博主在线答疑,还有一群同样热爱代码的小伙伴随时帮你解惑!