C++多线程编程

C++11 Thread线程库的基本使用

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

1、创建线程

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

要创建线程,我们需要一个可调用的函数或函数对象,作为线程的入口点。在C++11中,我们可以使用函数指针、函数对象或lambda表达式来实现。创建线程的基本语法如下:

cpp 复制代码
#include <thread>
std::thread t(function_name, args...);

function_name 是线程入口点的函数或可调用对象
args... 是传递给函数的参数

创建线程后,我们可以使用 t.join() 等待线程完成,或者使用 t.detach() 分离线程,让它在后台运行。

例如,下面的代码创建了一个线程,输出一条消息:

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;
}
  • .join():主线程阻塞,等待子线程执行完毕。必须调用 join()detach() 之一。
  • .detach():分离线程,使其独立运行,主线程不再等待。分离后不能再 join
join()等待线程完成

当我们创建一个线程后,我们可能需要等待它完成,以便获取线程的执行结果或执行清理操作。我们可以使用 t.join() 方法来等待线程完成。例如,下面的代码创建了两个线程,等待它们完成后输出一条消息:

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

void print_message(const std::string& message) {
    std::cout << message << std::endl;
}

int main() {
    std::thread t1(print_message, "Thread 1");
    std::thread t2(print_message, "Thread 2");

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

    std::cout << "All threads joined" << std::endl;
    return 0;
}

在这个例子中,我们创建了两个线程 t1t2,它们都调用 print_message函数输出一条消息。然后,我们使用 t1.join()和 t2.join()等待它们完成。最后,我们输出一条消息,表示所有线程都已经完成。

detach():分离线程

有时候我们可能不需要等待线程完成,而是希望它在后台运行。这时候我们可以使用 t.detach() 方法来分离线程。例如,下面的代码创建了一个线程,分离它后输出一条消息

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

void print_message(const std::string& message) {
    std::cout << message << std::endl;
}

int main() {
    std::thread t(print_message, "Thread 1");
    t.detach();
    std::cout << "Thread detached" << std::endl;
    return 0;
}

在这个例子中,我们创建了一个名为 t 的线程,调用print_message函数输出一条消息。然后,我们使用 t.detach()方法分离线程,让它在后台运行。最后,我们输出一条消息,表示线程已经被分离。

需要注意的是,一旦线程被分离,就不能再使用t.join()方法等待它完成。而且,我们需要确保线程不会在主线程结束前退出,否则可能会导致未定义行为。

joinable()

joinable()方法返回一个布尔值,如果线程可以被 join()或 detach(),则返回true,否则返回 false。如果我们试图对一个不可加入的线程调用 join()或 detach(),则会抛出一个 std::system_error异常。

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

void foo() {
    std::cout << "Thread started" << std::endl;
}

int main() {
    std::thread t(foo);
    if (t.joinable()) {
        t.join();
    }
    std::cout << "Thread joined" << std::endl;
    return 0;
}

2、传递参数

2.1**. 传递临时变量的问题,会产生未定义行为。**
cpp 复制代码
#include <iostream>
#include <thread>

void foo(int& x) {
    x += 1;
}

int main() {
    std::thread t(foo, 1); // 传递临时变量
    t.join();
    return 0;
}

在上述代码中,foo函数接受一个整数引用作为参数,并对其加 1。但在线程创建时,传入的1是一个临时变量,在std::thread解析参数时,该临时变量会被销毁,导致foo访问了已销毁的对象,产生未定义行为。

解决方案:使用 std::ref 传递一个持久化变量的引用:

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

void foo(int& x) {
    x += 1;
}

int main() {
    int x = 1;
    std::thread t(foo, std::ref(x)); // 传递变量的引用
    t.join();
    return 0;
}
2.2. 传递指针或引用指向局部变量的问题
cpp 复制代码
#include <iostream>
#include <thread>

void foo(int* ptr) {
    std::cout << *ptr << std::endl; // 访问已经被销毁的指针
}

int main() {
    int x = 1;
    std::thread t(foo, &x); // 传递指向局部变量的指针
    t.join();
    return 0;
}

局部变量 x在main函数作用域结束后被销毁,但std::thread可能尚未运行或访问了无效的地址,导致未定义行为。

解决方案

使用 new 在堆上分配内存,或者使用std::shared_ptr进行管理。

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

void foo(int* ptr) {
    std::cout << *ptr << std::endl;
    delete ptr; // 释放内存
}

int main() {
    int* ptr = new int(1);
    std::thread t(foo, ptr);
    t.join();
    return 0;
}
cpp 复制代码
#include <iostream>
#include <thread>
#include <memory>

void foo(std::shared_ptr<int> ptr) {
    std::cout << *ptr << std::endl;
}

int main() {
    auto ptr = std::make_shared<int>(1);
    std::thread t(foo, ptr);
    t.join();
    return 0;
}
2.3. 传递指针或引用指向已释放的内存的问题
cpp 复制代码
#include <iostream>
#include <thread>

void foo(int& x) {
    std::cout << x << std::endl;
}

int main() {
    int* ptr = new int(1);
    std::thread t(foo, *ptr); // 传递已释放的内存
    delete ptr;
    t.join();
    return 0;
}

问题分析

在线程 t 启动前,ptr已被delete,导致foo访问了已释放的内存,行为未定义。

解决方案

确保在线程执行期间,数据仍然有效。

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

void foo(int& x) {
    std::cout << x << std::endl;
}

int main() {
    int x = 1;
    std::thread t(foo, std::ref(x)); // 传递变量的引用
    t.join();
    return 0;
}

向线程函数传递参数遵循普通函数参数传递规则,但需注意参数是按值复制传递的。

若要传递引用,需使用 std::refstd::cref

2.4. 类成员函数作为入口函数,类对象被提前释放

当主线程结束或对象离开作用域时,子线程可能仍在尝试访问该对象,从而导致崩溃。

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

class MyClass {
public:
    void func() {
        std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl;
        // do some work
        std::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl;
    }
};

int main() {
    MyClass obj;
    std::thread t(&MyClass::func, &obj);

    // 错误:主线程没有等待子线程结束就退出了
    // 当 main 结束时,obj 被销毁,但线程 t 可能还在运行
    // 这会导致线程访问已销毁的对象,引发未定义行为或崩溃
    return 0;
} // obj 在这里被销毁,可能导致线程崩溃

解决方案:使用智能指针管理生命周期

为了解决上述问题,可以使用 std::shared_ptr 来管理对象的生命周期。通过传递 shared_ptr 的副本给线程,可以确保只要线程还在运行,对象就不会被销毁。

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

class MyClass {
public:
    void func() {
        std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl;
        // 模拟一些工作
        std::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl;
    }
};

int main() {
    // 使用 make_shared 创建共享指针
    auto obj = std::make_shared<MyClass>();

    // 将 shared_ptr 的副本传递给线程
    // 这会增加引用计数,保证对象在线程运行期间有效
    std::thread t(&MyClass::func, obj);

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

    return 0;
}
5. 入口函数为类的私有成员函数

通过友元函数来调用类的私有成员函数

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

class MyClass {
private:
    // 声明友元函数,使其能够访问本类的私有成员
    friend void myThreadFunc(MyClass* obj);

    void privateFunc() {
        std::cout << "Thread " << std::this_thread::get_id() << " privateFunc" << std::endl;
    }
};

// 友元函数的具体实现
void myThreadFunc(MyClass* obj) {
    obj->privateFunc();
}

int main() {
    MyClass obj;
    // 将友元函数作为线程的入口函数,并传递类对象的指针
    std::thread thread_1(myThreadFunc, &obj);
    thread_1.join();
    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;
    }
}
lock_guard

std::lock_guard是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。

特点
  • 当构造函数被调用时,该互斥量会被自动锁定。

  • 当析构函数被调用时,该互斥量会被自动解锁。

  • std::lock_guard对象不能复制或移动,因此它只能在局部作用域中使用。

std::unique_lock

std::unique_lock是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。

成员函数
  • lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。

  • try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false,否则返回true。

  • try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。

  • try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。

  • unlock():对互斥量进行解锁操作。

构造函数
  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的std::unique_lock对象。

  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量m进行初始化,并对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化,但不对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的std::unique_lock对象不与任何互斥量关联。

  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化,并假设该互斥量已经被当前线程成功加锁。

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

std::condition_variable 的使用步骤

std::condition_variable是 C++11 中用于线程间通信的同步机制,可以用于实现线程的等待与通知机制。其使用步骤如下:

  1. 创建一个 std::condition_variable 对象

  2. 创建一个互斥锁 std::mutex 对象,用于保护共享资源的访问。

  3. 在需要等待条件变量的地方

    • 使用 std::unique_lock<std::mutex> 对象锁定互斥锁。

    • 调用std::condition_variable::wait()、

      std::condition_variable::wait_for()或

      std::condition_variable::wait_until()函数等待条件变量。

  4. 在其他线程中需要通知等待的线程时,调用std::condition_variable::notify_one()或std::condition_variable::notify_all()函数来通知等待的线程。

生产者与消费者模型

下面是一个简单的生产者-消费者模型的案例,其中使用了std::condition_variable来实现线程的等待和通知机制:

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

// 全局互斥锁,用于保护共享资源 g_queue
std::mutex g_mutex;
// 全局条件变量,用于在生产者和消费者之间传递信号
std::condition_variable g_cv;
// 共享的任务队列(缓冲区)
std::queue<int> g_queue;

// 生产者线程函数
void Producer() {
    for (int i = 0; i < 10; i++) {
        {            
            // 1. 获取互斥锁(使用 unique_lock 以便配合条件变量)
            std::unique_lock<std::mutex> lock(g_mutex);
            // 2. 向共享队列中添加数据
            g_queue.push(i);            
            std::cout << "Producer: produced " << i << std::endl;
            // 离开此作用域时,lock 会自动释放互斥锁,避免阻塞消费者
        }
        // 3. 通知消费者线程:有新数据了,可以来取了
        // 放在锁外面通知,可以减少锁的竞争,提高性能
        g_cv.notify_one();        
        // 模拟生产耗时,休眠 100 毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

// 消费者线程函数
void Consumer() {    
    while (true) {        
        // 1. 获取互斥锁
        std::unique_lock<std::mutex> lock(g_mutex);      
        // 2. 等待条件满足(队列不为空)
        // wait 会自动释放锁并让线程进入休眠(不占用 CPU)
        // 当被唤醒时,它会重新加锁,并检查 lambda 表达式 []() { return !g_queue.empty(); }
        // 使用谓词(lambda)可以有效防止"虚假唤醒"问题
        g_cv.wait(lock, []() { return !g_queue.empty(); });
        // 3. 条件满足,安全地从队列中取出数据
        int value = g_queue.front();
        g_queue.pop();        
        std::cout << "Consumer: consumed " << value << std::endl;
        // 离开此作用域时,lock 会自动释放互斥锁
    }
}<websource>source_group_web_2</websource>

int main() {    
    // 创建并启动生产者线程
    std::thread producer_thread(Producer);    
    // 创建并启动消费者线程
    std::thread consumer_thread(Consumer);
    
    // 主线程等待子线程执行完毕
    producer_thread.join();
    // 注意:在当前代码中,由于 Consumer 是死循环且没有退出机制,
    // 主线程会一直阻塞在这里,程序无法正常结束。
    consumer_thread.join();    
    
    return 0;
}

使用 std::condition_variable可以实现线程的等待和通知机制,从而在多线程环境中实现同步操作。在生产者-消费者模型中,使用std::condition_variable可以让消费者线程等待生产者线程生产数据后再进行消费,避免了数据丢失或者数据不一致的问题。
示例2:

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

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() {
    // 模拟生产 5 个数据
    for (int i = 0; i < 5; ++i) {
        // 模拟生产耗时,休眠 100 毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        {
            // 获取互斥锁(使用 lock_guard 自动管理锁的生命周期)
            std::lock_guard<std::mutex> lock(mtx);
            data_queue.push(i); // 将数据放入队列
            // 注意:notify_one 放在锁的作用域内或外均可,
            // 但放在锁外面(如上个示例)通常能稍微减少锁的竞争。
        }
        cv.notify_one(); // 通知一个正在等待的消费者线程:有新数据了
    }
    // 生产任务全部完成,设置退出标志
    {
        std::lock_guard<std::mutex> lock(mtx);
        done = true; 
    }
    // 通知所有等待的线程(确保消费者能醒来检查 done 标志并退出)
    cv.notify_all(); 
}

// 消费者线程函数
void consumer() {
    while (true) {
        // 获取互斥锁(必须使用 unique_lock,因为它支持 wait 的自动解锁与重新加锁)
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待条件满足:队列不为空 或者 生产者已经完成任务(done == true)
        // 使用 lambda 谓词可以有效防止"虚假唤醒"
        cv.wait(lock, []{ return !data_queue.empty() || done; }); 
        
        // 优雅退出的核心逻辑:
        // 如果生产者已经做完(done == true)且队列里的数据也全部被消费完了,则跳出循环,结束线程
        if (done && data_queue.empty()) break;
        
        // 取出并处理数据
        int data = data_queue.front();
        data_queue.pop();
        
        // 手动释放锁。因为接下来的 cout 输出操作比较耗时,
        // 提前释放锁可以让其他线程(如生产者)更快获取到锁,提高并发效率
        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、跨平台线程池

本实现使用 C++11 标准库中的std::thread、std::mutex、std::condition_variable、std::function和 std::queue等组件来创建一个线程池。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <queue>
#include <vector> // 补充:std::vector 需要的头文件

class ThreadPool {
public:
    // 构造函数:预先创建指定数量的工作线程
    ThreadPool(int numThreads) : stop(false) {
        for (int i = 0; i < numThreads; ++i) {
            // 使用 emplace_back 直接在容器内构造线程对象
            threads.emplace_back([this] {
                while (true) {
                    std::unique_lock<std::mutex> lock(mutex);
                    // 条件变量等待:当线程池停止 或 任务队列不为空时,停止阻塞
                    condition.wait(lock, [this] {
                                  return stop || !tasks.empty(); 
                                });
                    
                    // 如果线程池已停止 且 任务队列已清空,则退出当前工作线程
                    if (stop && tasks.empty()) {
                        return;
                    }
                    
                    // 取出队列头部的任务(使用 std::move 移动语义,避免不必要的拷贝)
                    std::function<void()> task(std::move(tasks.front()));
                    tasks.pop();
                    
                    // 提前释放锁,避免在执行耗时任务时阻塞其他线程提交或获取任务
                    lock.unlock();
                    task(); // 执行取出的任务
                }
            });
        }
    }

    // 析构函数:负责优雅地关闭线程池
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(mutex);
            stop = true; // 设置停止标志
        }
        condition.notify_all(); // 唤醒所有处于等待状态的工作线程,让它们有机会退出
        // 等待所有工作线程执行完毕并退出
        for (std::thread& thread : threads) {
            thread.join();
        }
    }

    // 任务入队函数:支持任意可调用对象(如 Lambda、函数指针等)及其参数
    template<typename F, typename... Args>
    void enqueue(F&& f, Args&&... args) {
        // 使用 std::bind 将函数和参数绑定成一个无参的可调用对象,并使用完美转发
        std::function<void()> task(std::bind(std::forward<F>(f), 
                         std::forward<Args>(args)...));
        {
            std::unique_lock<std::mutex> lock(mutex);
            tasks.emplace(std::move(task)); // 将任务添加到任务队列中
        }
        condition.notify_one(); // 唤醒一个正在等待的工作线程来执行新任务
    }

private:
    std::vector<std::thread> threads;           // 存放工作线程的容器
    std::queue<std::function<void()>> tasks;    // 存放待执行任务的任务队列
    std::mutex mutex;                           // 互斥锁,用于保护任务队列的线程安全
    std::condition_variable condition;          // 条件变量,用于线程间的同步与通信
    bool stop;                                  // 线程池的停止标志位
};

int main() {
    // 创建一个包含 4 个工作线程的线程池
    ThreadPool pool(4);
    
    // 提交 8 个任务到线程池中
    for (int i = 0; i < 8; ++i) {
        pool.enqueue([i] {
            std::cout << "Task " << i << " is running in thread " << std::this_thread::get_id() << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟任务执行耗时
            std::cout << "Task " << i << " is done" << std::endl;
        });
    }
    // 注意:main 函数结束后 pool 会被析构,析构函数会等待所有任务执行完毕
    return 0;
}

核心机制解析

  1. 线程复用 :在构造函数中一次性创建好固定数量的线程。这些线程会在 while(true) 循环中不断从任务队列中取任务执行,避免了高并发场景下频繁创建和销毁线程的系统开销。
  2. 同步机制 :使用 std::mutex 保护共享的任务队列,防止多个线程同时操作导致数据竞争;使用 std::condition_variable 实现"生产者-消费者"模型,当队列为空时让工作线程休眠(不占用 CPU),有新任务时再将其唤醒。
  3. 完美转发与类型擦除enqueue 函数使用了模板、std::forward(完美转发)和 std::bind。这使得线程池可以接收任意形式的可调用对象(Lambda、普通函数、类成员函数等)和任意数量的参数,并将它们统一包装成 std::function<void()> 存入队列。
  4. 优雅退出 :析构函数中先将 stop 置为 true,然后调用 notify_all() 唤醒所有休眠的线程。工作线程被唤醒后,检测到 stop 为真且队列为空,便会安全地退出循环,最后主线程通过 join() 回收所有子线程资源。

说明

在这个示例中,我们定义了一个 ThreadPool类,并在构造函数中创建了指定数目的线程。在每个线程中,线程会不断地从任务队列中获取任务并执行,直到线程池被停止。

  • enqueue() 函数:将任务封装成一个std::function对象,并将其添加到任务队列中。

  • 析构函数:在析构函数中,我们等待所有线程执行完成后,再停止所有线程。

在主函数中,我们创建了一个ThreadPool对象,并向任务队列中添加了 8 个任务。每个任务会输出一些信息,并在执行完后等待 1 秒钟。由于线程池中有 4 个线程,因此这 8 个任务会被分配到不同的线程中执行。在任务执行完成后,程序会退出

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

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

  • std::async:异步执行函数,返回一个 std::future 对象。
  • std::future:存储异步操作的结果。通过 .get() 获取结果(会阻塞直到结果准备好)。

5.1. asyncfutur``e

std::async和std::future是 C++11 引入的一个功能,允许我们异步执行一个函数,并返回一个std::future对象,表示异步操作的结果。使用std::async可以方便地进行异步编程,避免了手动创建线程和管理线程的麻烦。

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

int calculate() {
    // 模拟一个耗时的计算
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
}

int main() {
    std::future<int> future_result = std::async(std::launch::async, calculate);
    // 在这里可以做其他的事情
    int result = future_result.get(); // 获取异步操作的结果
    std::cout << result << std::endl; // 输出42
    return 0;
}

在此例中,使用 std::async 函数异步执行了一个耗时的计算,这个计算在另一个线程中执行,不会阻塞主线程。通过 future_result.get() 获取异步操作的结果并输出。

5.2. packaged_task

std::packaged_task是一个类模板,用于将一个可调用对象(如函数、函数对象或 Lambda 表达式)封装成一个异步操作,并返回一个std::future对象,表示异步操作的结果。packaged_task可以方便地将一个函数或可调用对象转换成一个异步操作,供其他线程使用。

cpp 复制代码
#include <iostream>
#include <thread>
#include <future> // 需要包含此头文件以使用 std::packaged_task 和 std::future

// 定义带参数的任务函数
int calculate(int x, int y) {
    return x + y;
}

int main() {
    // 1. 包装任务:模板参数<int(int, int)> 必须与 calculate 的签名匹配
    std::packaged_task<int(int, int)> task(calculate);

    // 2. 获取关联的 future 对象
    std::future<int> future_result = task.get_future();

    // 3. 启动线程:将 task 移动到线程中,并传入参数 1 和 2
    std::thread t(std::move(task), 1, 2);

    // 4. 等待线程执行完毕
    t.join();

    // 5. 获取并打印结果 (1 + 2 = 3)
    std::cout << "Result: " << future_result.get() << std::endl;

    return 0;
}

在此例中,我们成功地将一个函数calculate封装成了一个异步操作,并在其他线程中执行。通过packaged_task和future对象,我们可以方便地实现异步编程,使得代码更加简洁和易于维护。

std::thread t(std::move(task), 1, 2);为啥move

这其实是由 std::packaged_task 的特性决定的。

简单来说:std::packaged_task 是不可复制的,只能被移动。

以下是详细的技术原因分析:

核心原因:独占所有权

std::packaged_task 内部持有一个共享状态 ,这个状态与 std::future 相关联。

  • 一对一关系 :一个 packaged_task 只能对应一个 future
  • 防止数据竞争 :如果允许复制 packaged_task,那么两个任务对象就会指向同一个共享状态。当这两个任务分别在不同线程执行时,它们都会尝试设置结果(set_value)。但 future 只能接收一次结果,第二次设置会导致程序崩溃(std::future_error)。

为了保证安全,C++ 标准库将 std::packaged_task复制构造函数复制赋值运算符 删除了(delete),只保留了移动构造函数

为什么 std::thread 需要 Move

当你创建一个 std::thread 对象时,你需要把要执行的函数(在这里是 task)传递给线程。

  • 值传递std::thread 的构造函数会拷贝移动你传入的参数到线程内部的存储中。
  • 强制 Move :因为你传入的是 task,而 task 不能被拷贝,所以你必须显式地使用 std::move(task) 将其转换为右值引用。这告诉编译器:"我知道 task 不可复制,请把它的所有权转移给新线程。"

5.3. promise

std::promise 是一个类模板,用于在一个线程中产生一个值,并在另一个线程中获取这个值。promise 通常与 futureasync 一起使用,用于实现异步编程。

cpp 复制代码
#include <iostream>
#include <future> // 必须包含,提供 promise 和 future
#include <thread> // 必须包含,提供 thread

using namespace std;

// 子线程执行的函数
// 注意:promise 对象不可复制,因此必须通过值传递(配合 move)或引用传递
void func(std::promise<int> f) {
    // 设置共享状态的值
    f.set_value(1000);
}

int main() {
    // 1. 在主线程创建 promise 对象
    std::promise<int> f;
    // 2. 获取与该 promise 关联的 future 对象
    auto future_result = f.get_future();
    // 3. 创建线程并传递 promise
    // 必须使用 std::move 将 promise 的所有权转移给新线程
    std::thread t1(func, std::move(f));
    // 4. 等待子线程执行完毕
    t1.join();
    // 5. 获取子线程设置的结果并打印
    // future_result.get() 会返回 1000
    cout << future_result.get() << endl;
    return 0;
}

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

std::atomic是 C++11 标准库中的一个模板类,用于实现多线程环境下的原子操作。它提供了一种线程安全的方式来访问和修改共享变量,可以避免多线程环境中的数据竞争问题。

std::atomic的使用方式类似于普通的 C++ 变量,但是它的操作是原子性的。也就是说,在多线程环境下,多个线程同时对同一个std::atomic变量进行操作时,不会出现数据竞争问题。

常用的 std::atomic 操作

  1. load() :将 std::atomic 变量的值加载到当前线程的本地缓存中,并返回这个值。

  2. store(val):将val的值存储到std::atomic变量中,并保证这个操作是原子性的。

  3. exchange(val):将val的值存储到std::atomic变量中,并返回原先的值。

  4. compare_exchange_weak(expected, val)和compare_exchange_strong(expected, val):

    • 比较std::atomic变量的值和expected的值是否相同,如果相同,则将val的值存储到std::atomic变量中,并返回true;否则,将std::atomic变量的值存储到expected中,并返回false。

以下是一个示例,演示了如何使用std::atomic进行原子操作:

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

std::atomic<int> count = 0;

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        count++;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << count << std::endl;
    return 0;
}

在这个示例中,我们定义了一个 std::atomic<int>类型的变量count,并将其初始化为 0。然后,我们启动两个线程分别执行increment函数,这个函数的作用是将count变量的值加一,执行一百万次。最后,我们在主线程中输出count 变量的值。

由于count变量是一个std::atomic类型的变量,因此对它进行操作是原子性的,不会出现数据竞争问题。在这个示例中,最终输出的count变量的值应该是 2000000。

7、线程局部存储 (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;
}

8、C++11 std::call_once 与其使用场景

单例模式的线程安全问题

单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。

cpp 复制代码
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    void setData(int data) {
        m_data = data;
    }
    int getData() const {
        return m_data;
    }

private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    int m_data = 0;
};

在这个单例类中,我们使用了一个静态成员函数getInstance()来获取单例实例,该函数使用了一个静态局部变量instance来存储单例实例。由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。

但是,该实现并不是线程安全的。如果多个线程同时调用getInstance()函数,可能会导致多个对象被创建,从而违反了单例模式的要求。此外,如果多个线程同时调用setData()函数来修改单例对象的数据成员m_data,可能会导致数据不一致或不正确的结果。

使用 std::call_once 解决线程安全问题

为了解决这些问题,我们可以使用std::call_once来实现一次性初始化,从而确保单例实例只会被创建一次。下面是一个使用std::call_once的单例实现:

cpp 复制代码
class Singleton {
public:
    static Singleton& getInstance() {
            std::call_once(m_onceFlag, &Singleton::init);
            return *m_instance;
    }    
    void setData(int data) {
        m_data = data;
    }    
    int getData() const {        
        return m_data;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;    

    static void init() {
        m_instance.reset(new Singleton);
    }    
    static std::unique_ptr<Singleton> m_instance;    
    static std::once_flag m_onceFlag;    
    int m_data = 0;
};

std::unique_ptr<Singleton> Singleton::m_instance;
std::once_flag Singleton::m_onceFlag;

在这个实现中,我们使用了一个静态成员变量m_instance来存储单例实例,

使用了一个静态成员变量m_onceFlag来标记初始化是否已经完成。

在getInstance()函数中,我们使用std::call_once来调用init()函数,确保单例实例只会被创建一次。在init()函数中,我们使用了std::unique_ptr来创建单例实例。

使用std::call_once可以确保单例实例只会被创建一次,从而避免了多个对象被创建的问题。此外,使用std::unique_ptr可以确保单例实例被正确地释放,避免了内存泄漏的问题。

std::call_once 函数

std::call_once是 C++11 标准库中的一个函数,用于确保某个函数只会被调用一次。其函数原型如下:

cpp 复制代码
emplate<class Callable, class... Args>
void call_once(std::once_flag& flag, Callable&& func, Args&&... args);

其中,flag是一个std::once_flag类型 的对象,用于标记函数是否已经被调用;

func是需要被调用的函数或可调用对象;args是函数或可调用对象的参数。

std::call_once的作用是,确保在多个线程中同时调用call_once时,只有一个线程能够成功执行func函数,而其他线程则会等待该函数执行完成。

使用 std::call_once 时需要注意的几点

  1. flag参数必须是一个 std::once_flag类型的对象,并且在多次调用call_once函数时需要使用同一个 flag对象。

  2. func 参数是需要被调用的函数或可调用对象。该函数只会被调用一次,因此应该确保该函数是幂等的。

  3. args 参数是 func 函数或可调用对象的参数。如果 func 函数没有参数,则该参数可以省略。

  4. std::call_once函数会抛出std::system_error异常,如果在调用func函数时发生了异常,则该异常会被传递给调用者。

相关推荐
weixin_444012931 小时前
Go语言GORM怎么做分页_Go语言GORM分页查询教程【实用】
jvm·数据库·python
khalil10201 小时前
代码随想录算法训练营Day-52 图论03 | 101.孤岛的总面积、102.沉没孤岛、103.水流问题、104.建造最大岛屿
c++·算法·图论
weixin_428005301 小时前
C#调用 AI学习从0开始-第1阶段(基础与工具)-第4天CoT思维链学习
开发语言·学习·ai·c#·cot
砍材农夫1 小时前
物联网 基于netty构建mqtt服务demo演示
开发语言·物联网·php
Devin~Y1 小时前
大厂Java面试实录:Spring Boot/Cloud + Redis + Kafka + JVM + RAG(Spring AI)三轮追问(小Y翻车版)
java·jvm·spring boot·redis·spring cloud·kafka·mybatis
JAVA面经实录9171 小时前
Java 并发工具类
java·大数据·开发语言
吃好睡好便好1 小时前
在Matlab中绘制变半径柱面图
开发语言·人工智能·学习·算法·matlab
汉克老师2 小时前
GESP6级C++考试语法知识(十五、数据结构(一、认识栈 Stack))
c++··gesp6级·gesp六级·数组模拟栈
驭渊的小故事2 小时前
Java数据结构集合框架(顺序表(ArrayList)的详细解析)(两千字详细解析)
java·开发语言