【C++进阶】多线程

1. 线程

C++中的thread是一种多线程编程的支持,可以创建一个新的线程来执行指定的代码。C++中的thread可以通过标准库中的std::thread类来实现。std::thread类提供了创建、启动和管理线程的方法。

使用std::thread创建一个新线程的基本流程如下:

  1. 定义一个函数或lambda表达式,作为新线程要执行的代码;
  2. 创建std::thread对象,并将要执行的代码作为参数传递给它;
  3. 调用std::thread对象的join()方法,等待新线程执行完毕。

需要注意的是,在使用std::thread时应该避免资源竞争和死锁的情况。在多线程程序中,不同的线程可能会同时访问同一个共享资源,如果没有合适的同步机制,就会导致资源竞争的情况。死锁是指两个或多个线程互相等待对方释放锁的状态,导致程序无法继续执行。为避免资源竞争和死锁,应该使用互斥锁、条件变量等同步机制来保证线程的安全性。

除了std::thread之外,C++标准库还提供了其他类型的线程类,如std::asyncstd::futurestd::promise等,用于实现不同的多线程编程方式。

1.1 语法

① 构造函数

  1. 默认构造函数

创建一个空的thread执行对象

thread() noexcept : _Thr{
    
    } {
    
    }
  1. 初始化构造函数

创建一个新的线程对象,并在新线程中执行给定的函数f,并传递参数args

template <class Function, class... Args>
explicit thread(Function&& f, Args&&... args)
  1. 拷贝构造函数

拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造

thread(const thread&) = delete;
  1. 移动构造函数

将一个线程对象转移到另一个线程对象中

thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {
    
    })) {
    
    }

    thread& operator=(thread&& _Other) noexcept {
    
    
        if (joinable()) {
    
    
            _STD terminate();
        }

        _Thr = _STD exchange(_Other._Thr, {
    
    });
        return *this;
    }

使用移动构造函数需要注意如下几点:

  • 线程对象必须是可移动的,否则会编译错误
  • 移动构造函数不应该抛出异常,因为它们通常在析构函数中被调用,如果抛出异常,将会导致程序崩溃
  • 移动构造函数应该确保线程对象的状态正确,并且不会与其他线程对象发生冲突
  • 移动构造函数应该确保线程对象的资源正确释放,否则会导致内存泄漏或其他问题
  • 移动构造函数应该尽可能地避免使用锁,因为锁可能会导致死锁或性能问题
  • 移动构造函数应该确保线程对象的所有权正确转移,否则会导致未定义行为

② 主要成员函数

  • hardware_concurrency():获取系统支持的最大线程数
  • get_id():获取该线程的ID
  • joinable():判断该线程是否可以加入等待
  • join():等待该线程执行完成,当该线程执行完成后才能继续执行主线程
  • detach():将本线程从调用线程中分离出来,允许本线程独立执行,分离后线程执行完毕后会自动释放资源,不再需要手动调用join()函数等待线程执行完成。(但是当主进程结束的时候,即便是detach出去的子线程不管有没有完成都会被强制杀死)
  • swap():交换两个线程对象的状态

1.2 线程的创建

使用std::thread创建线程,提供线程函数或者函数对象,并可以同时指定线程函数的参数

#include <iostream>
#include <thread>

class A
{
    
    
public:
    static void func1(int a)
    {
    
    
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "param a " << a << std::endl;
    }
};

int main()
{
    
    
    std::thread t1([]{
    
    
        std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
    }); // 只传递函数
    t1.join();

    std::thread t2([] (int a, int b) {
    
    
        std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
        std::cout << "a + b = " << a + b << std::endl;
    }, 1, 2); // 加上参数传递,可以是任意参数
    t2.join();

    std::thread t3(&A::func1, 3); // 绑定类静态函数
    if (t3.joinable())
    {
    
    
        t3.join();
        std::cout << "t2 join" << std::endl;
    }

    return 0;
}

其中在绑定类静态函数时加上了&,原因如下:

在C++中,&符号通常用于获取变量的地址。但是,在使用std::thread的构造函数将类静态函数绑定到线程对象时,&符号用于指示要绑定的函数是一个成员函数或静态函数

对于成员函数,必须将其绑定到一个对象上才能调用。因此,需要使用std::bind或lambda表达式将对象指针作为第一个参数传递给std::thread的构造函数。

对于静态函数,不需要将其绑定到一个对象上才能调用。因此,只需要使用类名和作用域解析运算符(::)来指定函数的名称。&符号用于将函数名转换为函数指针,以便std::thread可以正确地将其作为参数进行传递。

因此,如果要将类静态函数绑定到std::thread对象上,必须在函数名前加上&符号,以指示该函数是一个静态函数并且需要转换为函数指针。

1.3 线程的封装

thread.h

#ifndef THREAD_H
#define THREAD_H
#include <thread>

class Thread
{
    
    
public:
    Thread(); // 构造函数
    virtual ~Thread(); // 析构函数
    bool start();
    void stop();
    bool isAlive() const; // 线程是否存活.
    std::thread::id id() {
    
     return _th->get_id(); }
    std::thread* getThread() {
    
     return _th; }
    void join();  // 等待当前线程结束, 不能在当前线程上调用
    void detach(); //能在当前线程上调用
    static size_t CURRENT_THREADID();
protected:
    static void threadEntry(Thread* pThread); // 静态函数, 线程入口
    virtual void run() = 0; // 运行
protected:
    bool  _running; //是否在运行
    std::thread* _th;
};

#endif // THREAD_H

thread.cpp

#include "thread.h"
#include <sstream>
#include <iostream>
#include <exception>
Thread::Thread() :
    _running(false), _th(NULL)
{
    
    

}

Thread::~Thread()
{
    
    
    if (_th != NULL)
    {
    
    
        // 如果资源没有被detach或者被join,则自己释放
        if (_th->joinable())
        {
    
    
            _th->detach();
        }

        delete _th;
        _th = NULL;
    }
    std::cout << "~Thread()" << std::endl;
}

bool Thread::start()
{
    
    
    if (_running)
    {
    
    
        return false;
    }
    try
    {
    
    
        _th = new std::thread(&Thread::threadEntry, this);
    }
    catch (...)
    {
    
    
        throw  "[Thread::start] thread start error";
    }
    return true;
}

void Thread::stop()
{
    
    
    _running = false;
}

bool Thread::isAlive() const
{
    
    
    return _running;
}

void Thread::join()
{
    
    
    if (_th->joinable())
    {
    
    
        _th->join();  // 不是detach才去join
    }
}

void Thread::detach()
{
    
    
    _th->detach();
}

size_t Thread::CURRENT_THREADID()
{
    
    
    // 声明为thread_local的本地变量在线程中是持续存在的,不同于普通临时变量的生命周期,
    // 它具有static变量一样的初始化特征和生命周期,即使它不被声明为static。
    static thread_local size_t threadId = 0;
    if (threadId == 0)
    {
    
    
        std::stringstream ss;
        ss << std::this_thread::get_id();
        threadId = strtol(ss.str().c_str(), NULL, 0);
    }
    return threadId;
}

void Thread::threadEntry(Thread* pThread)
{
    
    
    pThread->_running = true;

    try
    {
    
    
        pThread->run(); // 函数运行所在
    }
    catch (std::exception& ex)
    {
    
    
        pThread->_running = false;
        throw ex;
    }
    catch (...)
    {
    
    
        pThread->_running = false;
        throw;
    }
    pThread->_running = false;
}

main.cpp

#include <iostream>
#include <chrono>
#include "thread.h"
using namespace std;

class A : public Thread
{
    
    
public:
    void run()
    {
    
    
        while (_running)
        {
    
    
            cout << "print A " << endl;
            std::this_thread::sleep_for(std::chrono::seconds(5));
        }
        cout << "----- leave A " << endl;
    }
};

class B : public Thread
{
    
    
public:
    void run()
    {
    
    
        while (_running)
        {
    
    
            cout << "print B " << endl;
            std::this_thread::sleep_for(std::chrono::seconds(2));
        }
        cout << "----- leave B " << endl;
    }
};

int main()
{
    
    
    {
    
    
        A a;
        a.start();
        B b;
        b.start();
        std::this_thread::sleep_for(std::chrono::seconds(10));
        a.stop();
        a.join();
        b.stop();
        b.join();
    }
    cout << "Hello World!" << endl;
    return 0;
}

2. 互斥量

mutex是一种同步原语,用于实现多线程程序中的互斥访问。mutex是一个互斥量,可以确保在同一时间只有一个线程可以访问共享资源。当一个线程获得mutex的锁时,其他线程必须等待这个线程释放锁之后才能获得锁。

C++中的mutex可以通过标准库中的std::mutex类来实现。std::mutex类提供了lock()unlock()方法来控制对共享资源的访问。使用mutex的基本流程如下:

  1. 定义一个mutex对象;
  2. 在需要访问共享资源的代码块前调用mutex对象的lock()方法,获得锁;
  3. 访问共享资源;
  4. 在共享资源访问结束后调用mutex对象的unlock()方法释放锁。

需要注意的是,在使用mutex时应该避免死锁的情况。死锁是指两个或多个线程互相等待对方释放锁的状态,导致程序无法继续执行。为避免死锁,应该保证在同一时间只有一个线程持有一个或多个mutex的锁。

除了std::mutex之外,C++标准库还提供了其他类型的mutex,如std::recursive_mutexstd::timed_mutexstd::recursive_timed_mutex等,用于实现不同的互斥访问方式。

下面介绍一下这四种不同类型锁之间的区别和使用场景:

  • std::mutex:最基本的互斥量,用于保护共享数据的访问。只能被一个线程锁定,其他线程必须等待该线程解锁后才能访问共享数据。使用场景:适用于不需要长时间占用锁的场景。
  • std::timed_mutex:与std::mutex类似,但是可以指定超时时间,如果超时仍未获得锁,则返回错误。使用场景:适用于需要在一定时间内完成任务的场景。
  • std::recursive_mutex:递归互斥量,允许同一个线程多次获得锁,避免死锁。使用场景:适用于需要在同一个线程中多次锁定同一互斥量的场景。
  • std::recursive_timed_mutex:与std::recursive_mutex类似,但是可以指定超时时间,如果超时仍未获得锁,则返回错误。使用场景:适用于需要在一定时间内完成任务,并且需要在同一个线程中多次锁定同一互斥量的场景。

2.1 独占互斥量std::mutex

std::mutex是最基本的互斥锁,它用于保护共享资源的访问,确保在同一时间只有一个线程可以访问共享资源。

成员函数:

  • 构造函数:std::mutex不允许拷贝构造,也不允许移动拷贝构造,最初产生的mutex对象是处于unlocked状态的。
  • lock():调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock():解锁,释放对互斥量的所有权。
  • try_lock():尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
#include <iostream>
#include <thread>
#include <mutex>

std::mutex g_mutex;

void print(const std::string& str)
{
    
    
    g_mutex.lock();
    std::cout << str << std::endl;
    g_mutex.unlock();
}

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

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

在这个例子中,使用std::mutex保护std::cout的访问,确保在同一时间只有一个线程可以输出信息。

2.2 递归互斥量std::recursive_mutex

它是一个可重入锁,允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。

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

std::recursive_mutex mtx;

void func(int n)
{
    
    
    mtx.lock();
    std::cout << "Thread " << n << " locked the mutex." << std::endl;
    if (n > 1)
        func(n - 1);
    mtx.unlock();
    std::cout << "Thread " << n << " unlocked the mutex." << std::endl;
}

int main()
{
    
    
    std::thread t(func, 3);
    t.join();

    return 0;
}

虽然递归锁能解决这种情况的死锁问题,但是尽量不要使用递归锁,主要原因如下:

  • 需要用到递归锁的多线程互斥处理本身就是可以简化的,允许递归很容易放纵复杂逻辑的产生,并且产生晦涩,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁
  • 递归锁比起非递归锁,效率会低
  • 递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦超过一定的次数,再对lock进行调用就会抛出std::system错误

2.3 带超时的互斥量std::timed_mutex

它是一个带有超时功能的互斥锁,可以在一定时间内等待锁的释放。

成员函数:

  • try_lock_for():尝试获得互斥量的锁,在一定时间内等待锁的释放。如果在指定的时间内获得锁,则返回true,否则返回false
  • try_lock_until():尝试获得互斥量的锁,在指定的时间点之前等待锁的释放。如果在指定的时间点之前获得锁,则返回true,否则返回false

其中,try_lock_for()try_lock_until()std::timed_mutex的核心操作,用于在一定时间内等待锁的释放。这两个函数的第一个参数是一个std::chrono::duration对象,用于指定等待的时间长度;第二个参数是一个std::chrono::time_point对象,用于指定等待的时间点。

需要注意的是,std::timed_mutex并不是一个可重入的互斥量,因此在使用时需要注意避免死锁等问题。此外,std::timed_mutex在某些平台上可能不支持超时功能,因此在使用时需要进行平台兼容性检查。

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

std::timed_mutex mtx;

void print(const std::string& str)
{
    
    
    if (mtx.try_lock_for(std::chrono::seconds(1)))
    {
    
    
        std::cout << str << std::endl;
        mtx.unlock();
    }
    else
        std::cout << "Timeout" << std::endl;
}

int main()
{
    
    
    std::thread t1(print, "Hello");
    std::thread t2(print, "World");
    t1.join();
    t2.join();

    return 0;
}

2.4 带超时的递归互斥量std::recursive_timed_mutex

它是一个可重入的带有超时功能的互斥锁。

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

std::recursive_timed_mutex mtx;

void func(int n)
{
    
    
    if (mtx.try_lock_for(std::chrono::seconds(1)))
    {
    
    
        std::cout << "Thread " << n << " locked the mutex." << std::endl;
        if (n > 1)
            func(n - 1);
        mtx.unlock();
        std::cout << "Thread " << n << " unlocked the mutex." << std::endl;
    }
    else
    {
    
    
        std::cout << "Timeout!" << std::endl;
        return;
    }
}

int main()
{
    
    
    std::thread t(func, 5);
    t.join();

    return 0;
}

在这个例子中,使用std::recursive_timed_mutex保护递归函数的访问,并等待一定时间后获取锁的释放。

2.5 lock_guard和unique_lock

相对于手动lock和unlock,我们可以使用RAII(通过类的构造析构)来实现更好的编码方式,std::lock_guardstd::unique_lock都是C++中用于管理互斥量的RAII类,它们可以确保在作用域结束时自动释放互斥量的锁。

① 使用方式

std::lock_guard的使用方式非常简单,只需要在创建std::lock_guard对象时传入互斥量即可,如:

std::lock_guard<std::mutex> lock(mtx);

完整的例子:

#include <iostream>  // std::cout
#include <thread>    // std::thread
#include <mutex>     // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_error

std::mutex mtx;

void print_even(int x)
{
    
    
    if (x % 2 == 0)
        std::cout << x << " is even\n";
    else
        throw(std::logic_error("not even"));
}

void print_thread_id(int id)
{
    
    
    try
    {
    
    
        // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception :
        std::lock_guard<std::mutex> lck(mtx);
        print_even(id);
    }
    catch (std::logic_error &)
    {
    
    
        std::cout << "[exception caught]\n";
    }
}

int main()
{
    
    
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_thread_id, i + 1);
    for (auto &th : threads)
        th.join();

    return 0;
}

这里的lock_guard换成unique_lock是一样的


std::unique_lock的使用方式有两种,一种是和std::lock_guard一样,在创建std::unique_lock对象时传入互斥量,如:

std::unique_lock<std::mutex> lock(mtx);

另一种是在创建std::unique_lock对象时不传入互斥量,然后在后面通过调用std::unique_lock的成员函数lock()来锁定互斥量,如:

std::unique_lock<std::mutex> lock;
lock.lock();

使用unique_lock实现的生产者消费者模型:

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

std::queue<int> data;       // 共享数据队列
std::mutex mtx;             // 互斥锁
std::condition_variable cv; // 条件变量
bool ready = false;         // 数据是否准备好的标志

void producer()
{
    
    
    for (int i = 0; i < 10; ++i)
    {
    
    
        std::unique_lock<std::mutex> lock(mtx); // 获取互斥锁
        data.push(i);                           // 生产数据
        std::cout << "Producer produced " << i << std::endl;
        ready = true;                                                // 标记数据已经准备好
        lock.unlock();                                               // 释放锁
        cv.notify_one();                                             // 通知消费者线程
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等待一段时间
    }
}

void consumer()
{
    
    
    while (true)
    {
    
    
        std::unique_lock<std::mutex> lock(mtx); // 获取互斥锁
        cv.wait(lock, []
                {
    
     return ready; }); // 等待数据准备好的信号
        int value = data.front();   // 取出数据
        data.pop();                 // 移除数据
        std::cout << "Consumer consumed " << value << std::endl;
        ready = false;                                               // 标记数据已经被消费
        lock.unlock();                                               // 释放锁
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等待一段时间
    }
}

int main()
{
    
    
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.detach();
    t2.detach();

    std::this_thread::sleep_for(std::chrono::seconds(5));

    return 0;
}

wait():如果线程被唤醒或者超时那么会先进行lock获取锁,再判断条件(传入的参数)是否成立,如果成立则wait函数返回否则释放锁继续休眠
notify():进行notify动作并不需要获取锁

② 区别

std::lock_guardstd::unique_lock的主要区别在于灵活性和可扩展性。

std::lock_guard是一种比较简单的RAII类,它只能在创建时锁定互斥量,在作用域结束时解锁互斥量,而且不能手动解锁互斥量。因此,它的灵活性比较低,适用于简单的互斥访问场景。

std::unique_lockstd::lock_guard更加灵活,是通用互斥包装器,它可以在创建时不锁定互斥量,也可以手动解锁互斥量,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移,从而更加灵活地控制互斥访问。此外,std::unique_lock还支持条件变量等高级同步机制,可以实现更加复杂的多线程编程场景,但是使用unique_lock需要付出更多的时间、性能成本。

因此,如果只需要简单的互斥访问,可以使用std::lock_guard;如果需要更加灵活和可扩展的互斥访问,可以使用std::unique_lock

3. 条件变量

互斥量是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。但单靠互斥量无法实现线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量。它是一种线程同步机制,用于在线程之间传递信号和消息,以便它们可以相互通信和协调。条件变量通常与互斥锁一起使用,以防止多个线程同时访问共享资源。

3.1 使用步骤

  1. 定义一个互斥锁和条件变量:
std::mutex mutex;
std::condition_variable cond;
  1. 在需要等待条件变量的线程中,先锁住互斥锁,然后调用wait()方法等待条件变量:
std::unique_lock<std::mutex> lock(mutex);
cond.wait(lock);
// 等待条件变量
  1. 在需要通知条件变量的线程中,先锁住互斥锁,然后调用notify_one()notify_all()方法通知等待的线程:
std::unique_lock<std::mutex> lock(mutex);
cond.notify_one();
// 或者 cond.notify_all();
  1. 等待条件变量的线程在收到通知后,会重新尝试获取互斥锁,然后继续执行下去

需要注意的是,wait()方法会自动释放互斥锁,并在等待期间阻塞线程,直到条件变量发生变化。因此,在使用条件变量时,通常需要先锁住互斥锁,然后再调用wait()方法等待条件变量。

3.2 成员函数

① wait(lock, pred)

等待条件满足,如果条件不满足,则阻塞当前线程并释放锁,直到其他线程调用notify_one()notify_all()函数唤醒该线程。其中,lock是一个unique_lock对象,用于保护共享变量,pred是一个可调用对象,用于判断条件是否满足。

函数原型:

void wait(unique_lock<mutex>& _Lck) {
    
     // wait for signal
    // Nothing to do to comply with LWG-2135 because std::mutex lock/unlock are nothrow
    _Cnd_wait(_Mycnd(), _Lck.mutex()->_Mymtx());
}

template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) {
    
     // wait for signal and test predicate
    while (!_Pred()) {
    
    
        wait(_Lck);
    }
}

它包含两种重载,第一种的参数只有一个unique_lock对象,第二种需要额外传递一个Pred对象,即等待条件。

这里必须使用unique_lock的,因为wait函数的原理如下:

调用wait()函数会阻塞当前线程并释放互斥量的锁定。如果使用lock_guard来锁定互斥量,那么在该作用域结束之前,它不能被显式释放(unlock),因此必须使用unique_lock对象。直到另一个线程调用notify_one或notify_all函数唤醒当前线程。一旦当前线程被唤醒,wait()函数会自动重新获取互斥量的锁定(lock),因此也不能使用lock_guard对象。

② wait_for(lock, timeout, pred)

template <class _Rep, class _Period>
cv_status wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time) {
    
    
    // wait for duration
    if (_Rel_time <= chrono::duration<_Rep, _Period>::zero()) {
    
    
        return cv_status::timeout;
    }

    // TRANSITION, ABI: The standard says that we should use a steady clock,
    // but unfortunately our ABI speaks struct xtime, which is relative to the system clock.
    _CSTD xtime _Tgt;
    const bool _Clamped     = _To_xtime_10_day_clamped(_Tgt, _Rel_time);
    const cv_status _Result = wait_until(_Lck, &_Tgt);
    if (_Clamped) {
    
    
        return cv_status::no_timeout;
    }

    return _Result;
}

template <class _Rep, class _Period, class _Predicate>
bool wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time, _Predicate _Pred) {
    
    
    // wait for signal with timeout and check predicate
    return _Wait_until1(_Lck, _To_absolute_time(_Rel_time), _Pred);
}

等待条件满足,如果在timeout时间内条件不满足,则阻塞当前线程并释放锁,直到其他线程调用notify_one()或notify_all()函数唤醒该线程,或者超时。其中,lock是一个unique_lock对象,用于保护共享变量,timeout是等待时间,pred是一个可调用对象,用于判断条件是否满足。

③ wait_until(lock, timeout_time, pred)

template <class _Clock, class _Duration>
cv_status wait_until(unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time) {
    
    
    // wait until time point
#if _HAS_CXX20
    static_assert(chrono::is_clock_v<_Clock>, "Clock type required");
#endif // _HAS_CXX20
    for (;;) {
    
    
        const auto _Now = _Clock::now();
        if (_Abs_time <= _Now) {
    
    
            return cv_status::timeout;
        }

        _CSTD xtime _Tgt;
        (void) _To_xtime_10_day_clamped(_Tgt, _Abs_time - _Now);
        const cv_status _Result = wait_until(_Lck, &_Tgt);
        if (_Result == cv_status::no_timeout) {
    
    
            return cv_status::no_timeout;
        }
    }
}

template <class _Clock, class _Duration, class _Predicate>
bool wait_until(
    unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time, _Predicate _Pred) {
    
    
    // wait for signal with timeout and check predicate
#if _HAS_CXX20
    static_assert(chrono::is_clock_v<_Clock>, "Clock type required");
#endif // _HAS_CXX20
    return _Wait_until1(_Lck, _Abs_time, _Pred);
}

cv_status wait_until(unique_lock<mutex>& _Lck, const xtime* _Abs_time) {
    
    
    // wait for signal with timeout
    if (!_Mtx_current_owns(_Lck.mutex()->_Mymtx())) {
    
    
        _Throw_Cpp_error(_OPERATION_NOT_PERMITTED);
    }

    // Nothing to do to comply with LWG-2135 because std::mutex lock/unlock are nothrow
    const int _Res = _Cnd_timedwait(_Mycnd(), _Lck.mutex()->_Mymtx(), _Abs_time);
    switch (_Res) {
    
    
    case _Thrd_success:
        return cv_status::no_timeout;
    case _Thrd_timedout:
        return cv_status::timeout;
    default:
        _Throw_C_error(_Res);
    }
}

template <class _Predicate>
bool wait_until(unique_lock<mutex>& _Lck, const xtime* _Abs_time, _Predicate _Pred) {
    
    
    // wait for signal with timeout and check predicate
    return _Wait_until1(_Lck, _Abs_time, _Pred);
}

等待条件满足,如果在timeout_time时间点前条件不满足,则阻塞当前线程并释放锁,直到其他线程调用notify_one()或notify_all()函数唤醒该线程,或者超时。其中,lock是一个unique_lock对象,用于保护共享变量,timeout_time是等待的终止时间点,pred是一个可调用对象,用于判断条件是否满足。

④ notify_one()

void notify_one() noexcept {
    
     // wake up one waiter
    _Cnd_signal(_Mycnd());
}

唤醒一个等待线程,如果没有等待线程,则该函数不起作用。

⑤ notify_all()

void notify_all() noexcept {
    
     // wake up all waiters
    _Cnd_broadcast(_Mycnd());
}

唤醒所有等待线程。


在使用condition_variable时,需要注意的是,wait()函数在等待时会自动释放锁,而唤醒线程后会重新获取锁;而wait_for()和wait_until()函数在等待时也会自动释放锁,但在超时或被唤醒后不会自动重新获取锁,需要手动获取锁后再操作共享变量。

3.3 两种条件变量

在C++中,条件变量是一种用于线程间通信的同步原语。标准库中提供了两种条件变量:std::condition_variablestd::condition_variable_any

  1. std::condition_variable:是一个模板类,用于等待条件变量的线程可以被通知唤醒。它需要与std::unique_lock一起使用,以保证在等待期间锁是被持有的。

  2. std::condition_variable_any:也是一个模板类,用于等待条件变量的线程可以被通知唤醒。与std::condition_variable不同的是,它不需要与std::unique_lock一起使用,可以与任何类型的锁一起使用。

这两种条件变量的主要区别在于,std::condition_variable只能与std::unique_lock一起使用,因此可以更加高效地实现等待和通知的操作;而std::condition_variable_any可以与任何类型的锁一起使用,但会增加一些额外的开销。因此,如果需要高效的等待和通知操作,应该优先选择std::condition_variable


为什么std::condition_variable_any可以与任意类型的锁一起使用:

是因为它内部使用了一个私有的互斥锁来保护条件变量和等待队列。这个互斥锁只在内部使用,不会暴露给外部,因此可以与任何类型的锁一起使用。

具体来说,std::condition_variable_any内部有一个私有的std::mutex对象,用于保护条件变量和等待队列。当等待线程调用wait()方法时,它会先获取外部锁,然后再获取内部互斥锁,将自己加入等待队列并阻塞。当其他线程调用notify_one()notify_all()方法时,它会先获取内部互斥锁,从等待队列中取出一个或多个线程并唤醒它们,然后释放内部互斥锁。等待线程被唤醒后,会重新获取外部锁并检查条件变量,如果条件不满足,则重新加入等待队列并阻塞。如果条件满足,则继续执行下去。

由于std::condition_variable_any内部使用了一个私有的互斥锁,因此可以保证等待和通知的操作是线程安全的,并且可以与任何类型的锁一起使用。但需要注意的是,由于std::condition_variable_any需要获取两个锁,因此会增加一些额外的开销。如果能够使用std::condition_variable,则应该优先选择它来实现条件变量的等待和通知操作。


为什么std::condition_variable只能与std::unique_lock一起使用:

是因为它的等待和通知操作需要使用std::unique_lock的一些特性来保证正确性和效率。具体来说,std::condition_variable的等待和通知操作需要满足以下几个条件:

  1. 在等待期间,线程必须释放锁,以便其他线程可以访问共享资源。

  2. 在等待期间,线程必须阻塞,以免浪费CPU资源。

  3. 在唤醒线程时,必须确保线程能够正确地获取锁,并且能够立即访问共享资源。

为了满足这些条件,std::condition_variable需要使用std::unique_lock的以下特性:

  1. std::unique_lock可以在构造函数中获取锁,并在析构函数中释放锁,从而实现自动加锁和解锁。

  2. std::unique_lock可以在构造函数中指定锁的类型,从而支持不同类型的锁。

  3. std::unique_lock可以在构造函数中指定是否延迟加锁,从而支持等待期间的锁释放和阻塞。

由于std::condition_variable需要使用std::unique_lock的这些特性,因此只能与std::unique_lock一起使用,以确保等待和通知操作的正确性和效率。如果使用其他类型的锁,可能会导致死锁、竞态条件等问题。

3.4 例子

  1. 简单例子

std::condition_variable

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

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

void worker()
{
    
    
    std::unique_lock<std::mutex> lock(mtx);
    while (!ready)
    {
    
    
        cv.wait(lock);
    }
    std::cout << "Worker thread is processing data" << std::endl;
}

int main()
{
    
    
    std::thread worker_thread(worker);

    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
    
    
        std::unique_lock<std::mutex> lock(mtx);
        ready = true;
        cv.notify_one();
        std::cout << "main() signals data ready for processing" << std::endl;
    }

    worker_thread.join();

    return 0;
}

condition_variable_any

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

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

void worker()
{
    
    
    std::unique_lock<std::mutex> lock(mtx);
    while (!ready)
        // 等待条件变量
        cv.wait(lock);

    std::cout << "Worker thread is processing data" << std::endl;
}

int main()
{
    
    
    std::thread worker_thread(worker);

    // 做一些工作
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 发送通知
    {
    
    
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
        cv.notify_one();
        std::cout << "main() signals data ready for processing" << std::endl;
    }

    worker_thread.join();

    return 0;
}
  1. 同步队列

同步队列是一种用于线程间通信的同步机制,它允许一个线程向队列中插入数据,另一个线程从队列中取出数据,从而实现线程间的同步和协作。同步队列通常用于解决生产者-消费者问题,即多个线程共享一个队列,其中一个或多个线程向队列中插入数据,另一个或多个线程从队列中取出数据进行处理。

同步队列通常具有以下特点:

  • 线程安全:同步队列需要保证多个线程之间的访问是线程安全的,即在并发访问时不会出现竞态条件等问题
  • 队列大小:同步队列通常有一个固定的大小,当队列已满时,插入线程会被阻塞,直到队列中有空间可以插入数据;当队列为空时,取出线程会被阻塞,直到队列中有数据可供取出
  • 插入和取出:同步队列通常提供插入和取出两个操作,插入操作用于向队列中插入数据,取出操作用于从队列中取出数据。当插入线程往队列中插入数据时,取出线程会被唤醒并取出数据;当取出线程从队列中取出数据时,插入线程会被唤醒并继续插入数据

std::condition_variable_anystd::lockguard

sync_queue.cpp

#pragma once
#include <list>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <iostream>

template <typename T>
class SyncQueue
{
    
    
private:
    bool IsFull() const
    {
    
    
        return _queue.size() == _maxSize;
    }

    bool IsEmpty() const
    {
    
    
        return _queue.empty();
    }

public:
    SyncQueue(int maxSize) : _maxSize(maxSize)
    {
    
    
    }

    void Put(const T &x)
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);

        while (IsFull())
        {
    
    
            std::cout << "full wait... size " << _queue.size() << std::endl;
            _notFull.wait(_mutex);
        }

        _queue.push_back(x);
        _notEmpty.notify_one();
    }

    void Take(T &x)
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);

        while (IsEmpty())
        {
    
    
            std::cout << "empty wait.." << std::endl;
            _notEmpty.wait(_mutex);
        }

        x = _queue.front();
        _queue.pop_front();
        _notFull.notify_one();
    }

    bool Empty()
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);
        return _queue.empty();
    }

    bool Full()
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);
        return _queue.size() == _maxSize;
    }

    size_t Size()
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);
        return _queue.size();
    }

    int Count()
    {
    
    
        return _queue.size();
    }

private:
    std::list<T> _queue;                   // 缓冲区
    std::mutex _mutex;                     // 互斥量和条件变量结合起来使用
    std::condition_variable_any _notEmpty; // 不为空的条件变量
    std::condition_variable_any _notFull;  // 没有满的条件变量
    int _maxSize;                          // ͬ同步队列最大的size
};

代码中用到了std::lock_guard,它利用RAII机制可以保证安全释放mutex。

std::lock_guard<std::mutex> locker(_mutex);

while (IsFull())
{
    
    
    std::cout << "full wait... size " << _queue.size() << std::endl;
    _notFull.wait(_mutex);
}

可以修改为:

std::lock_guard<std::mutex> locker(_mutex);
_notFull.wait(_mutex, [this] {
    
    
	return !IsFull();
});

在第二种方法中,条件变量会先检查判断式是否满足条件,如果满足条件则重新获取mutex,然后结束wait继续往下执行;如果不满足条件则释放mutex,然后将线程置为waiting状态继续等待。

这里需要注意的是,wait函数中会释放mutex,而lock_guard这时还拥有mutex,而它只会在出了作用域之后才会释放mutex,所以这时它并不会释放,但执行wait时,如果条件满足会提前释放mutex。从语义上看这里使用lock_guard会产生矛盾,但是实际上并不会出问题,因为wait提前释放锁之后会处于等待状态,在被notify_one或者notify_all唤醒后会先获取mutex,这相当于lock_guard的mutex在释放之后又获取到了,而且在出了作用域之后lock_guard自动释放mutex,因此不会有问题。

但在这里如果使用unique_lock在wait时让unique_lock释放锁从语义上更加准确,因为unique_lock不像lock_guard一样只能在析构时才释放锁,它可以随时释放锁。

std::condition_varible+std::unique_lock
sync_queue.cpp:

#pragma once

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

template <typename T>
class SimpleSyncQueue
{
    
    
public:
    SimpleSyncQueue() {
    
    }

    void Put(const T &x)
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);
        _queue.push_back(x);
        _notEmpty.notify_one();
    }

    void Take(T &x)
    {
    
    
        std::unique_lock<std::mutex> locker(_mutex);
        _notEmpty.wait(locker, [this] {
    
    
            return !_queue.empty();
        });

        x = _queue.front();
        _queue.pop_front();
    }

    bool Empty()
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);
        return _queue.empty();
    }

    size_t Size()
    {
    
    
        std::lock_guard<std::mutex> locker(_mutex);
        return _queue.size();
    }

private:
    std::list<T> _queue;
    std::mutex _mutex;
    std::condition_variable _notEmpty;
};

main.cpp:

#include "sync_queue.h"
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

SimpleSyncQueue<int> syncQueue;

void PutData()
{
    
    
    for (int i = 0; i < 20; ++i)
        syncQueue.Put(i);

    std::cout << "PutData finish" << std::endl;
}

void TakeData()
{
    
    
    int x = 0;

    for (int i = 0; i < 20; ++i)
    {
    
    
        syncQueue.Take(x);
        std::cout << x << std::endl;
    }

    std::cout << "TakeData finish" << std::endl;
}

int main(void)
{
    
    
    std::thread t1(PutData);
    std::thread t2(TakeData);

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

    std::cout << "main finish\n";
    return 0;
}

4. 原子变量

原子变量atomic是一种线程安全的变量类型,可以在多线程程序中用于同步访问共享变量。原子变量的操作是原子的,即在执行期间不会被其它线程中断,保证了多线程环境下的数据一致性。

原子变量的定义和使用非常简单,只需在变量类型前加上关键字atomic即可。例如:

atomic<int> counter(0);

上面的代码定义了一个原子变量counter,初始值为0,类型为int。

原子变量的常用操作包括:

  1. load():从原子变量中读取当前值。

  2. store():将一个值存储到原子变量中。

  3. exchange():交换原子变量的当前值和一个新值,并返回原来的值。

  4. compare_exchange_weak()compare_exchange_strong():比较原子变量的当前值和一个期望值,如果相等则将原子变量的值设置为一个新值,否则不做任何操作。

  5. fetch_add()fetch_sub():将原子变量的当前值加上或减去一个给定的值,并返回原来的值。

  6. fetch_and()fetch_or()fetch_xor():将原子变量的当前值与一个给定的值进行按位与、按位或或按位异或操作,并返回原来的值。

需要注意的是,原子变量的操作必须是原子的,即不能被其它线程中断。在实现原子变量时,可能会使用CPU提供的原子指令,或者使用锁机制来保证操作的原子性。

假设有两个线程同时对一个共享变量进行递增操作,代码如下:

#include <iostream>
#include <thread>

int counter = 0;

void increment()
{
    
    
    counter++;
}

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

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

    std::cout << counter << std::endl;

    return 0;
}

stdout:

1

在上面的代码中,两个线程同时对共享变量counter进行递增操作。由于递增操作不是原子的,可能会出现竞争条件,导致计数器的值不正确。

例如,线程1读取counter的值为0,将其加1,此时线程2也读取counter的值为0,将其加1,然后两个线程都将新的值1写回到counter中,导致计数器只递增了1,而不是2。

为了解决这个问题,可以使用原子变量来保证递增操作的原子性。例如:

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

void increment() {
    
    
    counter++;
}

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

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

    std::cout << counter << std::endl;
    return 0;
}

stdout

1

在上面的代码中,使用原子变量std::atomic<int>来代替普通变量int,并对其进行递增操作。由于递增操作是原子的,不会出现竞争条件,因此计数器的值一定是正确的。

全面一点的例子:

#include <iostream> // std::cout
#include <atomic>   // std::atomic, std::memory_order_relaxed
#include <thread>   // std::thread

// std::atomic<int> foo = 0;//错误初始化
std::atomic<int> foo(0); // 准确初始化

void set_foo(int x)
{
    
    
    foo.store(x, std::memory_order_relaxed); // set value atomically
}

void print_foo()
{
    
    
    int x;
    do
    {
    
    
        x = foo.load(std::memory_order_relaxed); // get value atomically
    } while (x == 0);
    std::cout << "foo: " << x << '\n';
}

int main()
{
    
    
    std::thread t1(print_foo);
    std::thread t2(set_foo, 10);

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

    std::cout << "main finish\n";

    return 0;
}

stdout:

foo: 10
main finish

5. call_once和once_flag

C++中call_once和once_flag是用于实现函数只被调用一次的功能。它们通常与std::mutex一起使用,以确保只有一个线程调用该函数。

在使用call_once和once_flag时,需要遵循以下步骤:

  1. 定义一个once_flag对象,用于标记函数是否已经被调用过。

  2. 定义一个函数,该函数只会被调用一次,它会被传递给std::call_once()

  3. 在主程序中,使用std::call_once()函数来调用该函数,并将once_flag对象作为参数传递给它。

下面是一个简单的示例代码:

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

std::once_flag flag;

void init()
{
    
    
    std::cout << "Initializing resource" << std::endl;
}

void use_resource()
{
    
    
    std::call_once(flag, init);
    std::cout << "Using resource" << std::endl;
}

int main()
{
    
    
    std::thread t1([]() {
    
    
        use_resource();
    });

    std::thread t2([]() {
    
    
        use_resource();
    });

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

    return 0;
}

在上面的代码中,我们定义了两个函数:init()use_resource()。其中,init()函数用于初始化某个资源,而use_resource()函数用于使用该资源。

在主程序中,我们使用std::call_once()函数来调用init()函数,并将once_flag对象flag作为参数传递给它。由于once_flag对象只能被调用一次,因此init()函数也只会被调用一次。

use_resource()函数中,我们使用std::call_once()函数和once_flag对象flag来保证init()函数只被调用一次。这样,无论有多少个线程同时调用use_resource()函数,init()函数都只会被调用一次。

需要注意的是,once_flag对象应该在全局作用域中定义,以确保所有线程都可以访问它。

参考:https://www.apiref.com/cpp-zh/cpp/thread/call_once.html

6. 异步操作

  1. std::async:这个函数可以在一个新的线程中异步执行一个函数,并返回一个std::future对象,可以通过它获取函数返回值。

  2. std::promisestd::future:这两个类可以用来在两个线程之间传递数据,其中std::promise用于存储一个值,而std::future用于获取这个值。

  3. std::packaged_task:这个类可以将一个函数包装成一个可调用对象,并返回一个std::future对象,可以通过它获取函数返回值。

6.1 std::aysnc

std::async用于在一个新的线程中异步执行一个函数,并返回一个std::future对象,可以通过它获取函数返回值。

std::async函数的语法如下:

template <class Function, class... Args>
std::future<typename std::result_of<Function(Args...)>::type>
    async(Function&& f, Args&&... args);

其中,Function表示要执行的函数,Args表示函数的参数。std::result_of是一个模板元函数,用于获取函数调用的返回类型。

下面是一个使用std::async函数的例子:

#include <iostream>
#include <future>
#include <chrono>

int foo(int a, int b)
{
    
    
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    return a + b;
}

int main()
{
    
    
    std::future<int> result = std::async(foo, 1, 2); // 启动异步任务
    std::cout << "Wait..." << std::endl;
    std::cout << "Sum = " << result.get() << std::endl; // 获取异步任务结果

    return 0;
}

在这个例子中,我们定义了一个foo函数,它会在执行时暂停1秒钟,然后返回两个参数的和。在main函数中,我们使用async函数异步执行foo函数,并获取它的返回值。在调用get函数之前,程序会输出"Waiting…“,表示正在等待foo函数执行完成。当foo函数执行完成后,程序会输出"Sum = 3”,表示foo函数的返回值是3。

6.2 std::future

std::future用于获取异步操作的结果。在使用std::async函数启动一个异步操作时,它会返回一个std::future对象,我们可以通过这个对象获取异步操作的返回值。std::future对象的状态可以是未就绪、已就绪或无效三种状态。

std::future类的语法如下:

template <class T>
class future
{
    
    
public:
    T get(); // 获取异步操作的结果
    bool valid() const; // 判断future对象是否有效
};

其中,T表示异步操作的返回值类型,get函数用于获取异步操作的结果,如果异步操作还未完成,则get函数会阻塞当前线程,直到异步操作完成。valid函数用于判断future对象是否有效。

下面是一个使用std::future的例子:

#include <iostream>
#include <future>
#include <chrono>

int foo(int a, int b)
{
    
    
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    return a * b;
}

int main()
{
    
    
    std::future<int> result = std::async(std::launch::async, foo, 2, 3); // 启动异步任务
    std::cout << "Wait..." << std::endl;
    
    while (result.wait_for(std::chrono::milliseconds(200)) != std::future_status::ready)
        std::cout << "." << std::flush;
    std::cout << std::endl;
    
    int sum = result.get();
    std::cout << "Mul = " << sum << std::endl; // 获取异步任务结果

    return 0;
}

在这个例子中,我们使用std::async函数异步执行foo函数,并获取它的返回值。在调用get函数之前,我们使用wait_for函数等待异步操作完成。wait_for函数会在指定的时间内等待异步操作完成,如果在等待时间内异步操作还未完成,则返回future_status::timeout;如果异步操作已经完成,则返回future_status::ready。在等待过程中,我们会输出一个".“,表示正在等待异步操作完成。当异步操作完成后,我们会输出"Mul = 6”,表示foo函数的返回值是3。

6.3 std::promise

std::promise用于在两个线程之间传递数据。std::promise对象可以用于存储一个值,而std::future对象则可以用于获取这个值。当一个线程需要获取另一个线程的计算结果时,可以使用std::promisestd::future实现线程间的数据传递。

std::promise类的语法如下:

template <class T>
class promise
{
    
    
public:
    void set_value(const T& value); // 存储一个值
    void set_exception(std::exception_ptr p); // 存储一个异常
    std::future<T> get_future(); // 获取与promise关联的future对象
};

其中,T表示要存储的值的类型,set_value函数用于存储一个值,set_exception函数用于存储一个异常,get_future函数用于获取与promise关联的future对象。

下面是一个使用std::promise的例子:

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

void foo(std::promise<int>& p)
{
    
    
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    p.set_value(10);
}

int main()
{
    
    
    std::promise<int> p; // 创建一个promise对象
    std::future<int> f = p.get_future(); // 获取与promise关联的future对象
    std::thread t(foo, std::ref(p)); // 在另一个线程中执行foo函数

    std::cout << "Wait..." << std::endl;
    std::cout << "Result = " << f.get() << std::endl; // 获取异步任务结果
    t.join();

    return 0;
}

在这个例子中,我们创建了一个promise对象p,并使用get_future函数获取与p关联的future对象f。然后,我们创建了一个新的线程,并在这个线程中执行foo函数,将promise对象p作为参数传递给foo函数。在foo函数中,我们模拟了一个耗时操作,并使用set_value函数存储了一个值。在主线程中,我们使用f.get函数获取foo函数的返回值,并输出这个返回值。需要注意的是,在调用get函数之前,我们需要等待foo函数执行完成。在这个例子中,我们使用cout输出"Waiting…"表示正在等待foo函数执行完成。

这里我们在向线程t传递参数时使用了std::ref来将promise对象p作为参数传递给线程t的foo函数。这是因为在C++中,函数参数默认是按值传递的,也就是说,函数会创建一个参数的副本,并将这个副本传递给函数。但是,对于某些类型的参数,如std::promise,我们希望将它们作为引用传递,这样才能实现线程间的数据传递。

如果不使用std::ref,那么线程t的foo函数将会接收到promise对象的一个副本,而不是原始的promise对象。这意味着,当foo函数调用set_value函数时,它会修改副本而不是原始的promise对象,从而无法实现线程间的数据传递。

因此,为了将promise对象作为引用传递给线程t的foo函数,我们需要使用std::ref函数来将promise对象p包装成一个引用类型。这样,当线程t的foo函数接收到这个引用时,它就可以直接操作原始的promise对象,从而实现线程间的数据传递。

6.4 std::packaged_task

std::packaged_task用于将一个函数包装成一个可调用对象,并返回一个std::future对象,可以通过它获取函数返回值。std::packaged_task可以用于实现一些需要延迟执行的任务,例如在多个线程中共享一个任务等。

std::packaged_task类的语法如下:

template <class Result, class... Args>
class packaged_task<Result(Args...)>
{
    
    
public:
    template <class Function>
    explicit packaged_task(Function&& f); // 构造函数,将函数f包装成可调用对象
    std::future<Result> get_future(); // 获取与packaged_task关联的future对象
    void operator()(Args... args); // 执行包装的函数
};

其中,Result表示包装的函数的返回值类型,Args表示包装的函数的参数类型,get_future函数用于获取与packaged_task关联的future对象,operator()函数用于执行包装的函数。

下面是一个使用std::packaged_task的例子:

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

int foo(int a, int b)
{
    
    
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    return a - b;
}

int main()
{
    
    
    std::packaged_task<int(int, int)> task(foo); // 将foo函数包装成可调用对象
    std::future<int> result = task.get_future(); // 获取与task关联的future对象
    std::thread t(std::move(task), 2, 1); // 在另一个线程中执行task函数

    std::cout << "Wait..." << std::endl;
    std::cout << "Sub = " << result.get() << std::endl; // 获取异步任务结果
    t.join();

    return 0;
}

在这个例子中,我们使用std::packaged_task将foo函数包装成可调用对象task,并使用get_future函数获取与task关联的future对象f。然后,我们创建了一个新的线程,并在这个线程中执行task函数,将1和2作为参数传递给task函数。在主线程中,我们使用f.get函数获取foo函数的返回值,并输出这个返回值。需要注意的是,在调用get函数之前,我们需要等待foo函数执行完成。在这个例子中,我们使用cout输出"Waiting…"表示正在等待foo函数执行完成。

这里我们在向线程t传递参数时使用了std::move来将packaged_task对象task作为参数传递给线程t的构造函数。这是因为,std::thread的构造函数要求所有的参数都是可移动的类型。而packaged_task对象是一个不可复制的类型,因此我们不能直接将它作为参数传递给线程的构造函数。

使用std::move函数可以将packaged_task对象的所有权从当前线程转移到新线程中。当我们将packaged_task对象task作为参数传递给线程t的构造函数时,它将被移动到线程t中,从而保证线程t拥有task对象的所有权。

因此,为了将packaged_task对象作为参数传递给线程t的构造函数,我们需要使用std::move函数将对象的所有权转移。

猜你喜欢

转载自blog.csdn.net/weixin_52665939/article/details/129970990