首页 > 后端开发 > C++ > 正文

现代C++的线程库怎么使用 std thread与异步任务管理

P粉602998670
发布: 2025-08-02 08:00:04
原创
694人浏览过

在现代c++++并发编程中,std::thread和std::async是主要工具,其中std::thread用于底层线程管理,而std::async提供更高层次的异步任务封装。1. std::thread需要手动处理线程生命周期(join或detach)及数据同步;2. std::async配合std::future简化异步编程,自动处理线程管理和异常传递;3. 使用std::launch::async可确保任务在独立线程执行,避免延迟模式带来的串行化问题;4. 线程同步需借助互斥量(std::mutex)和条件变量(std::condition_variable)来保障数据安全与线程协作;5. 推荐优先使用std::async以减少复杂性,除非需要对线程进行细粒度控制。

现代C++的线程库怎么使用 std thread与异步任务管理

现代C++中,使用线程库进行并发编程,主要围绕

std::thread
登录后复制
std::async
登录后复制
展开。简单来说,
std::thread
登录后复制
提供的是更底层的线程创建和管理能力,你需要手动处理线程的生命周期(
join
登录后复制
detach
登录后复制
)和数据同步;而
std::async
登录后复制
则是一种更高层次的抽象,它将异步任务的执行和结果获取封装起来,通常配合
std::future
登录后复制
来简化异步编程,让你不必直接面对线程的细节,更侧重于任务本身。选择哪一个,往往取决于你对控制粒度的需求和对复杂性的容忍度。

现代C++的线程库怎么使用 std thread与异步任务管理

解决方案

在C++11及更高版本中,

std::thread
登录后复制
是启动新执行流的基础工具,而
std::async
登录后复制
则提供了更高级的异步任务管理机制,通常是更推荐的选项,因为它能更好地处理异常和结果返回。

使用

std::thread
登录后复制
:
std::thread
登录后复制
允许你直接创建和管理操作系统级别的线程。它接受一个可调用对象(函数、Lambda表达式、函数对象或成员函数)作为新线程的入口点。

立即学习C++免费学习笔记(深入)”;

现代C++的线程库怎么使用 std thread与异步任务管理
#include <iostream>
#include <thread>
#include <vector>
#include <numeric> // For std::iota

void do_work(int id) {
    std::cout << "Thread " << id << " is working...\n";
    // 模拟一些计算
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Thread " << id << " finished.\n";
}

int main() {
    std::cout << "Main thread starts.\n";
    std::thread t1(do_work, 1); // 创建并启动线程t1

    // 也可以使用lambda表达式
    std::thread t2([](int data) {
        std::cout << "Lambda thread processing data: " << data << "\n";
    }, 42);

    // 线程的生命周期管理至关重要
    // 必须在线程对象销毁前调用 join() 或 detach()
    t1.join(); // 等待t1完成
    t2.join(); // 等待t2完成

    std::cout << "All threads joined. Main thread ends.\n";
    return 0;
}
登录后复制

这里有个小细节,如果你不

join()
登录后复制
detach()
登录后复制
一个
std::thread
登录后复制
对象,当它被销毁时,程序会直接崩溃,这是个常见的错误。
join()
登录后复制
意味着主线程会等待子线程执行完毕,而
detach()
登录后复制
则让子线程独立运行,主线程不再关心其结束。

使用

std::async
登录后复制
std::future
登录后复制
:
std::async
登录后复制
是更高级的并发工具,它返回一个
std::future
登录后复制
对象,通过这个
future
登录后复制
你可以获取异步任务的结果,甚至等待任务完成。它甚至可以决定是否真的创建一个新线程,或者只是延迟执行任务。

现代C++的线程库怎么使用 std thread与异步任务管理
#include <iostream>
#include <future> // For std::async and std::future
#include <chrono> // For std::chrono::milliseconds

int calculate_sum(int a, int b) {
    std::cout << "Calculating sum of " << a << " and " << b << "...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    return a + b;
}

int main() {
    std::cout << "Main thread launching async task.\n";

    // 启动一个异步任务,并获取其future
    // std::launch::async 强制在新线程中运行
    // std::launch::deferred 延迟到 future 的 get() 或 wait() 时才运行
    // 默认是 std::launch::async | std::launch::deferred,由系统决定
    std::future<int> result_future = std::async(std::launch::async, calculate_sum, 10, 20);

    std::cout << "Main thread continues its work...\n";
    // 可以在这里做其他事情,不必等待
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::cout << "Main thread waiting for async result...\n";
    try {
        int sum = result_future.get(); // 获取结果,会阻塞直到任务完成
        std::cout << "Async task result: " << sum << "\n";
    } catch (const std::exception& e) {
        std::cerr << "An error occurred: " << e.what() << "\n";
    }

    std::cout << "Main thread ends.\n";
    return 0;
}
登录后复制

std::async
登录后复制
的优势在于,它帮你处理了线程的创建、销毁、异常捕获和结果返回。你只需要关注你要执行的任务本身。
get()
登录后复制
方法会阻塞当前线程直到异步任务完成并返回结果(或抛出异常)。

std::thread 的基础用法与生命周期管理:避免崩溃的必修课

谈到

std::thread
登录后复制
,很多人可能初次尝试就会遇到一个问题:为什么我的程序会崩溃?这通常与线程的生命周期管理不当有关。一个
std::thread
登录后复制
对象,在其生命周期结束时,如果它所代表的线程还没有
join
登录后复制
(被主线程等待完成)或
detach
登录后复制
(独立运行),那么程序就会调用
std::terminate
登录后复制
,直接终止。这可不是什么友好的体验。

创建

std::thread
登录后复制
对象很简单,可以传入一个普通函数、一个lambda表达式、一个函数对象,甚至是类的成员函数。比如:

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

void simple_func(int count, const std::string& msg) {
    for (int i = 0; i < count; ++i) {
        std::cout << "Thread (func): " << msg << " - " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

struct MyFunctor {
    void operator()(double value) {
        std::cout << "Thread (functor): Processing value " << value << std::endl;
    }
};

class Worker {
public:
    void do_something(const std::string& task_name) {
        std::cout << "Thread (member): " << task_name << " is active." << std::endl;
    }
};

int main() {
    // 1. 使用普通函数
    std::thread t1(simple_func, 3, "Hello from t1");

    // 2. 使用lambda表达式
    std::thread t2([](int id) {
        std::cout << "Thread (lambda): ID " << id << " says hi!" << std::endl;
    }, 101);

    // 3. 使用函数对象
    MyFunctor mf;
    std::thread t3(mf, 3.14); // 注意:mf会被拷贝到线程内部

    // 4. 使用类的成员函数 (需要传递对象实例的指针或引用)
    Worker worker_obj;
    std::thread t4(&Worker::do_something, &worker_obj, "Complex Task");

    // 关键:管理线程生命周期
    // join():等待线程完成。这是最常见的用法,确保子线程的工作在主线程继续前完成。
    t1.join();
    t2.join();
    t3.join();
    t4.join(); // 必须等待所有线程,否则程序可能在它们完成前结束

    std::cout << "All threads have finished their work." << std::endl;

    // 另一个选择是 detach()
    // std::thread detached_t([](){
    //     std::cout << "I'm a detached thread, running independently!\n";
    //     std::this_thread::sleep_for(std::chrono::seconds(1));
    //     std::cout << "Detached thread finished.\n";
    // });
    // detached_t.detach(); // 一旦detach,你将无法再控制这个线程,也无法join它

    // 注意:如果一个线程被detach,主程序退出时,如果它还在运行,可能会被操作系统强制终止,
    // 或者继续运行直到完成,具体行为取决于操作系统。
    // 一般来说,除非你明确知道自己在做什么,否则优先使用 join()。

    return 0;
}
登录后复制

参数传递时,要注意引用传递。如果你想在线程函数中修改主线程的变量,需要使用

std::ref
登录后复制
包装,否则默认是值拷贝。

#include <iostream>
#include <thread>
#include <functional> // For std::ref

void increment_by_ref(int& counter) {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    counter++;
    std::cout << "Thread: counter is now " << counter << std::endl;
}

int main() {
    int my_counter = 0;
    // 错误:默认是值拷贝,线程内部修改的是拷贝
    // std::thread t1(increment_by_ref, my_counter);
    // 正确:使用std::ref进行引用传递
    std::thread t1(increment_by_ref, std::ref(my_counter));

    t1.join();
    std::cout << "Main: final counter is " << my_counter << std::endl; // 应该显示 1
    return 0;
}
登录后复制

掌握

join()
登录后复制
detach()
登录后复制
是使用
std::thread
登录后复制
的关键,它们决定了你如何处理线程的生命周期,以及主线程和子线程之间的协调关系。我个人更倾向于
join()
登录后复制
,因为它提供了一种明确的同步点,让程序的行为更可预测。除非有非常明确的理由,否则
detach()
登录后复制
可能会让调试变得复杂。

std::async 与 std::future:简化异步编程,但别忘了它的“懒惰”模式

std::async
登录后复制
std::future
登录后复制
的组合,简直是现代C++异步编程的瑞士军刀。它极大地简化了线程管理,尤其是当你只需要执行一个任务并获取其结果时。你不用再操心
join
登录后复制
detach
登录后复制
,也不用手动创建线程,甚至连异常处理都变得优雅了许多。

百度智能云·曦灵
百度智能云·曦灵

百度旗下的AI数字人平台

百度智能云·曦灵 83
查看详情 百度智能云·曦灵

它的核心思想是:你提交一个任务(可调用对象),

std::async
登录后复制
返回一个
std::future
登录后复制
对象。这个
future
登录后复制
对象就是你未来获取任务结果的凭证。当你调用
future.get()
登录后复制
时,如果任务还没完成,它会阻塞直到任务完成并返回结果;如果任务已经完成,它会立即返回结果。更棒的是,如果任务中抛出了异常,
get()
登录后复制
也会重新抛出这个异常,让你可以在主线程中统一处理。

#include <iostream>
#include <future>
#include <chrono>
#include <stdexcept> // For std::runtime_error

double divide(double numerator, double denominator) {
    std::cout << "Async task: Performing division...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟计算耗时
    if (denominator == 0) {
        throw std::runtime_error("Division by zero!");
    }
    return numerator / denominator;
}

int main() {
    std::cout << "Main: Launching division tasks.\n";

    // 正常情况:异步执行并获取结果
    std::future<double> f1 = std::async(divide, 10.0, 2.0);
    // 异常情况:异步执行,但会抛出异常
    std::future<double> f2 = std::async(divide, 10.0, 0.0);

    std::cout << "Main: Doing other stuff while tasks run...\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // 获取第一个任务的结果
    try {
        double result1 = f1.get(); // 阻塞直到f1完成
        std::cout << "Main: Result of 10.0 / 2.0 is: " << result1 << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Main: Error in f1: " << e.what() << std::endl;
    }

    // 获取第二个任务的结果
    try {
        double result2 = f2.get(); // 阻塞直到f2完成,并重新抛出异常
        std::cout << "Main: Result of 10.0 / 0.0 is: " << result2 << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Main: Error in f2: " << e.what() << std::endl;
    }

    std::cout << "Main: All tasks processed.\n";
    return 0;
}
登录后复制

这里有个非常重要的点,就是

std::async
登录后复制
的执行策略。它有一个可选的第一个参数,
std::launch
登录后复制

  • std::launch::async
    登录后复制
    : 强制在新的线程中执行任务。
  • std::launch::deferred
    登录后复制
    : 延迟执行任务。只有当
    future
    登录后复制
    get()
    登录后复制
    wait()
    登录后复制
    方法被调用时,任务才会在调用者的线程中执行。这就像一个懒惰的求值,任务根本不会异步运行。
  • 默认行为(不指定
    std::launch
    登录后复制
    ):是
    std::launch::async | std::launch::deferred
    登录后复制
    。这意味着系统会根据资源情况自行决定是立即启动新线程还是延迟执行。

这个默认行为有时候会让人困惑。我曾经就遇到过,写了一个

std::async
登录后复制
,结果发现它根本没在新线程里跑,而是
get()
登录后复制
的时候才执行,导致我以为的并发变成了串行。所以,如果你真的需要一个独立的线程来执行任务,我建议明确使用
std::launch::async
登录后复制

std::future
登录后复制
除了
get()
登录后复制
,还有
wait()
登录后复制
方法,它只等待任务完成而不获取结果。此外,
std::future_status
登录后复制
可以用来检查任务的状态(是否完成、是否延迟等),配合
wait_for
登录后复制
wait_until
登录后复制
可以实现超时等待,这在需要响应性或避免无限期阻塞的场景中非常有用。

总的来说,

std::async
登录后复制
是处理单个异步任务的首选,它隐藏了大部分线程管理的复杂性,让你可以专注于业务逻辑。但别忘了它的“懒惰”模式,必要时要显式指定执行策略。

线程同步与数据安全:互斥量和条件变量,并发编程的基石

一旦你开始在多个线程间共享数据,那么恭喜你,你已经踏入了并发编程最容易出错的雷区:数据竞争(Data Race)。没有适当的同步机制,多个线程同时读写同一个数据,其结果是不可预测的,这也就是所谓的未定义行为。为了避免这种混乱,我们需要互斥量(

std::mutex
登录后复制
)和条件变量(
std::condition_variable
登录后复制
)这样的工具。

互斥量(

std::mutex
登录后复制
):独占访问的守护者
std::mutex
登录后复制
是C++中最基本的同步原语,它提供了一种独占访问共享资源的方式。当一个线程锁住
mutex
登录后复制
后,其他试图锁住同一个
mutex
登录后复制
的线程都会被阻塞,直到第一个线程解锁。这保证了在任何时刻,只有一个线程可以访问被
mutex
登录后复制
保护的代码段(临界区)。

然而,直接使用

std::mutex::lock()
登录后复制
std::mutex::unlock()
登录后复制
很容易出错,比如忘记解锁,或者在异常发生时没有解锁,导致死锁。因此,C++提供了RAII(资源获取即初始化)风格的锁,如
std::lock_guard
登录后复制
std::unique_lock
登录后复制
,它们在构造时加锁,在析构时自动解锁,极大地简化了安全编程。

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <numeric> // For std::iota

std::mutex mtx; // 全局互斥量,保护 shared_data
int shared_data = 0;

void increment_shared_data(int id) {
    for (int i = 0; i < 1000; ++i) {
        // 使用 std::lock_guard 确保互斥访问
        // 当 lock_guard 对象超出作用域时,会自动解锁
        std::lock_guard<std::mutex> lock(mtx);
        shared_data++;
        // std::cout << "Thread " << id << ": shared_data = " << shared_data << std::endl; // 频繁输出会影响性能
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_shared_data, i);
    }

    for (auto& t : threads) {
        t.join();
    }

    // 理论上,shared_data 应该是 10 * 1000 = 10000
    std::cout << "Final shared_data: " << shared_data << std::endl; // 应该接近10000,如果没锁,会远小于
    return 0;
}
登录后复制

如果没有

std::lock_guard<std::mutex> lock(mtx);
登录后复制
shared_data
登录后复制
的最终值几乎不可能是10000,因为多个线程会同时读取、修改、写回,导致一些更新丢失。
std::lock_guard
登录后复制
是日常使用中最方便的选择。
std::unique_lock
登录后复制
则提供了更多灵活性,比如可以延迟加锁、手动解锁、尝试加锁等,适用于更复杂的场景。

条件变量(

std::condition_variable
登录后复制
):线程间的信号与等待 互斥量解决了数据竞争问题,但它无法解决线程间的协作问题。例如,一个线程需要等待另一个线程完成某个任务或满足某个条件后才能继续执行。这时,条件变量就派上用场了。

std::condition_variable
登录后复制
通常与
std::mutex
登录后复制
和某个共享的布尔条件变量一起使用。一个线程等待某个条件满足,它会调用
wait()
登录后复制
方法,这会自动释放互斥量并进入阻塞状态。当另一个线程改变了条件并通知(
notify_one()
登录后复制
notify_all()
登录后复制
)条件变量时,等待的线程会被唤醒,并重新尝试获取互斥量,然后检查条件是否真的满足。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue> // 模拟生产者-消费者队列

std::mutex mtx_cv;
std::condition_variable cv;
std::queue<int> data_queue;
bool producer_finished = 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_cv);
            data_queue.push(i);
            std::cout << "Producer: Produced " << i << std::endl;
        } // 锁在这里释放
        cv.notify_one(); // 通知一个等待的消费者
    }
    {
        std::lock_guard<std::mutex> lock(mtx_cv);
        producer_finished = true;
    }
    cv.notify_all(); // 生产完成后,通知所有可能还在等待的消费者
}

void consumer(int id) {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx_cv); // unique_lock 允许在 wait 期间释放锁
        // 等待条件:队列不为空 或者 生产者已完成
        cv.wait(lock, [&]{ return !data_queue.empty() || producer_finished; });

        if (data_queue.empty() && producer_finished) {
            std::cout << "Consumer " << id << ": Producer finished and queue is empty. Exiting.\n";
            break; // 退出循环
        }

        int data = data_queue.front();
        data_queue.pop();
        std::cout << "Consumer " << id << ": Consumed " << data << std::endl;
        // 锁在这里自动释放,或者手动释放 lock.unlock();
    }
}

int main() {
    std::thread prod_t(producer);
    std::thread cons1_t(consumer, 1);
    std::thread cons2_t(consumer, 2);

    prod_t.join();
    cons1_t.join();
    cons2_t.join();

    std::cout << "Main: All threads finished.\n";
    return 0;
}
登录后复制

条件变量的

wait()
登录后复制
方法通常接受一个lambda表达式作为谓词(predicate)。这个谓词会在
wait()
登录后复制
被唤醒后自动检查条件是否满足。如果条件不满足,它会再次进入等待状态。这避免了“虚假唤醒”(spurious wakeups)的问题,即线程被唤醒但条件并未真正满足。

除了`std::mutex

以上就是现代C++的线程库怎么使用 std thread与异步任务管理的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号