C++:线程库

C++:线程库


thread

操作线程需要头文件<thread>,头文件包含线程相关操作,内含两个内容:

  • thread类:操作线程的基本类
  • this_thread命名空间域:用于操作当前线程

thread

thread是用于操作线程的类,其实现了对多平台线程的封装,以统一的面向对象方式完成线程的操作。

  • thread构造:
cpp 复制代码
thread() noexcept;

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

thread (const thread&) = delete;

thread (thread&& x) noexcept;

从以上构造的声明可以得出以下要点:

  1. 可以不传参,直接构造一个空线程对象
  2. 禁止拷贝构造
  3. 支持移动构造

其中第二个声明是最重要的:

cpp 复制代码
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

这是一个函数模板,参数如下:

  • fn:一个可调用对象,创建线程后线程执行该函数内容
  • args:可变参数包,用于给fn传参,可以传任意数量的参数

示例:

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

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

int main()
{
    int a = 3;
    int b = 5;

    std::thread t(test, a, b);

    return 0;
}

这就创建了一个线程对象,并且设定好了该线程要执行的函数。

此处最大的便利在于,不论是Windows还是Linux,创建线程时传入的函数,都只允许传入一个参数,而此处可以通过参数包传入任意数量的参数。

  • 成员函数:
  1. operator=

thread线程类的赋值重载,同样禁止拷贝赋值,只允许移动赋值。

还记得我们可以创建一个空线程类吗,没有函数可以给一个空线程类绑定函数,只能通过移动赋值来完成初始化空线程类。

示例:

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

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

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

    for (auto& th : v)
    {
        th = std::thread(test, 100, 200);
    }

    return 0;
}

当要管理多个线程的时候,可以把多个线程对象放到一个容器内部统一管理,此处用一个vector管理了100个线程对象。但是一开始可能还不能确定线程对象要执行哪一个函数,所以会在容器内部构建空线程对象。需要初始化时,就得用一个匿名对象通过构造函数创建非空线程对象,随后通过operator=完成移动赋值。

以上示例中, th = std::thread(test, 100, 200)被放在for循环内部,完成对所有空线程对象的初始化。

  1. get_id

直接获取一个线程的id,其声明如下:

cpp 复制代码
id get_id() const noexcept;

如果仔细观察返回值,你会发现其返回的不是一个整数,而是一个叫做id的类型。其是thread的内部类,由于不同操作系统对线程的标识符不同,比如Linux使用整数tid标识,而Windows使用线程句柄标识不同线程。所以无法统一线程的id,因此C++将不同操作系统的标识符封装为一个类thread::id

thread::id重载了以下操作符:

cpp 复制代码
bool operator== (thread::id lhs, thread::id rhs) noexcept;
bool operator!= (thread::id lhs, thread::id rhs) noexcept;
bool operator< (thread::id lhs, thread::id rhs) noexcept;
bool operator<= (thread::id lhs, thread::id rhs) noexcept;
bool operator> (thread::id lhs, thread::id rhs) noexcept;
bool operator>= (thread::id lhs, thread::id rhs) noexcept;

允许进行比大小,判断相等的操作,因此可以放在容器中作为键值,用于管理线程比如mapset之类的容器。

那么能否作用于unordered_map这样的哈希容器呢?想要将数据放到哈希表中作为键,就需要一个哈希函数将数据转化为一个整数下标。而id是一个复杂的类,不同系统下内部的内容不一样,很难写出一个统一的哈希函数完成转化。而C++对此进行了模板特化,在thread::id作为哈希的键时,C++内部实现了哈希函数,所以thread::id可以直接在哈希表内部使用。

cpp 复制代码
template <class T> struct hash; // 通用模板声明
template<> struct hash<thread::id>; // 对 thread::id 的模板特化

另外的,thread::id还支持流输出:

cpp 复制代码
template <class charT, class traits>
basic_ostream<chasrT, traits>& operator<< (basic_ostream<charT,traits>& os, thread::id id);

可以直接用cout这样的流输出对象来输出id

cpp 复制代码
std::thread th(test, 100, 500);
std::cout << th.get_id() << std::endl;
  1. join

用于回收线程,调用该函数后,主线程进入阻塞,直到被join的线程结束,最后回收该线程。

示例:

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

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

int main()
{
    std::thread th(test, 100, 500);
    th.join();

    return 0;
}

只需要线程对象.join()就可以完成线程资源的回收,还是比较方便的。

  1. detatch

用于线程分离,被分离的线程将自己回收自己,无需再join

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

void test(int x, int y)
{
    std::cout << x + y << std::endl;
}

int main()
{
    std::thread th(test, 100, 500);
    th.detach();
    
    return 0;
}

但是此处你很有可能会看不到线程的输出结果,因为线程th被分离后,就无需主线程回收了,主线程直接return,线程结束。但是由于主线程退出,同一进程下的所有线程也会推出,导致线程th还没有来得及输出,就被强制退出了。

  1. joinable

用于检测一个线程是否允许被join,如果线程被detach或已经被join了,那么joinable就会返回false,反之返回true


this_thread

std::this_thread 是一个命名空间,用于访问当前线程。它提供了一组函数来操作当前线程。

  1. get_id(): 返回当前线程的 ID。
  2. yield(): 让出 CPU 时间片,让其他线程运行。
  3. sleep_until(): 使当前线程睡眠直到某个时间点。
  4. sleep_for(): 使当前线程睡眠一段时间。

get_idyield都可以直接执行,不用传入参数。而后两个函数与时间相关,要用到C++封装的时间chrono

chrono

<chrono>是一个头文件,内包含chrono命名空间域,该域内部封装了各种时间的相关操作。

  • 时钟 clock
  1. std::chrono::system_clock:系统时钟,表示从 Unix 纪元开始的时间(1970 年 1 月 1 日 00:00:00 UTC)。
  2. std::chrono::steady_clock:稳定时钟,表示从程序启动开始的时间。

这两个时钟都有一个now成员函数,返回当前的时间。但是system_clock会受到系统时钟影响,如果用户调整了系统时间,就有可能造成时间错误,而稳定时钟不受系统时钟影响。

cpp 复制代码
auto t1 = std::chrono::system_clock::now();
auto t2 = std::chrono::steady_clock::now();

这两个函数都返回一个time_point类型,表示当前时间点。

  • 时间段 duration

duration用于表示一个时间段,这个类的用法比较复杂,因此C++为我们封装了一些可以直接使用的类:

  1. std::chrono::nanoseconds (纳秒)
  2. std::chrono::microseconds (微秒)
  3. std::chrono::milliseconds (毫秒)
  4. std::chrono::seconds (秒)
  5. std::chrono::minutes (分钟)
  6. std::chrono::hours (小时)

这些类都是typedef后的duration,如果想要表示一个时间端,直接传数字即可:

cpp 复制代码
auto dur1 = std::chrono::seconds(3); // 3秒
auto dur2 = std::chrono::minutes(5); // 5分钟
  • 时间点 time_point

time_point表示一个时间点,之前时钟返回的now,就是当前的时间点。C++支持了以下运算:

  1. 时间点time_point + 时间段duration = 时间点time_point
  2. 时间段duration - 时间段duration = 时间段duration
  3. 时间点time_point - 时间点time_point = 时间段duration

关于时间类,就简单了解到这里,此处只讲解了最基础的概念,为了讲解线程库的相关接口。

回到this_thread内部的函数:

  1. sleep_until(): 使当前线程睡眠直到某个时间点。
  2. sleep_for(): 使当前线程睡眠一段时间。

sleep_until需要传入一个时间点time_point,比如想要睡眠10秒,就可以用当前时间 + 10秒得到一个时间点,再用sleep_until完成睡眠:

cpp 复制代码
auto t1 = std::chrono::steady_clock::now(); // 获取当前时间	
auto dur = std::chrono::seconds(10); // 获取十秒时间段
auto t2 = t1 + dur; // 时间点 + 时间段 = 时间点,十秒后

std::this_thread::sleep_until(t2); // 睡眠到 10 秒后

sleep_for需要传入一个时间段duration,同样的睡眠十秒:

cpp 复制代码
auto dur = std::chrono::seconds(10); // 获取十秒时间段
std::this_thread::sleep_for(dur); // 睡眠到 10 秒后

引用拷贝问题

再看thread类的声明:

cpp 复制代码
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

此处可以发现,其构造参数使用的是&&引用折叠,可以传左值/右值引用。

尝试一下:

cpp 复制代码
void test(int& x, int& y)
{
    x += 100;
    y += 200;
}

int main()
{
    int a = 30;
    int b = 50;

    std::thread t1(test, a, b);

    t1.join();

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;

    return 0;
}

这段代码无法正常运行,因为线程传参时,无法直接传引用,为什么?

不论是Linux还是Windows系统,创建多线程时,函数都只允许传入一个参数,比如Linux只允许传一个void*的变量。

但是C++封装后,允许传入多个值,最终一定要把这多个参数和函数封装在一个类内部,成为一个可调用对象(仿函数),一起通过一个变量传给线程函数。

而C++为了确保线程拿到的参数是有效的,不会出现线程拿到参数后,参数被主线程销毁了等情况。不论是普通变量,引用还是指针,都会进行一次拷贝。

引用一旦经过拷贝,拷贝后的变量和拷贝前的变量,就不是一个变量了。所以此处传入引用会报错,在线程传参时,不能直接传引用。

但是指针不怕拷贝,指针拷贝后,依然指向原先的变量。该问题的解决策略有三个:

  1. 使用指针进行传址调用
  2. 使用引用包装器
  3. 使用lambda传引用

引用包装器

引用包装器用于解决引用的拷贝问题,引用拷贝后依然是原先的变量。

引用包装器的原理如下:

cpp 复制代码
template <typename T>
class reference_wrapper
{
public:
    // 构造函数,接受一个引用
    explicit reference_wrapper(T& ref)
        : _ptr(&ref)
    {}

    // 拷贝构造
    reference_wrapper(const reference_wrapper& other)
        : _ptr(other._ptr)
    {}

    operator T& () const
    {
        return *_ptr;
    }

private:
    T* _ptr;
};

引用包装器内部存储一个指针,当包装引用时,_ptr成员会存储引用的指针。当拷贝时,也是对指针进行拷贝,指针拷贝后,依然指向原先的变量。

最核心的是重载了operator T&,也就是隐式类型转化,此时引用包装器可以转化为一个T&引用,所以引用包装器可以当作引用来使用。

如果想要将一个引用包装起来,可以使用std::ref()函数,其返回一个引用的引用包装器。另外的,如果是const引用,则使用std::cref()

对于刚才的线程问题,只需要在传递参数时用包装器包装一层即可:

cpp 复制代码
std::thread t1(test, std::ref(a), std::ref(b));

结合lambda

除了以上两种方式,也可以直接使用lambda的捕捉列表,以引用的形式捕捉变量:

cpp 复制代码
int main()
{
    int a = 30;
    int b = 50;

    auto func = [&a, &b]() {
        a += 100;
        b += 200;
        };

    std::thread t1(func);

    t1.join();

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;

    return 0;
}

lambda的直接捕捉,不用通过std::thread进行参数传递,所以就不会出现引用拷贝的问题。


mutex

既然要进行多线程并发编程,自然少不了线程安全的问题,<mutex>头文件内部,封装了各种锁,用于维护线程安全。

头文件内包含四种锁:

  1. mutex:互斥锁
  2. recursive_muetx:递归锁
  3. timed_mutex:时间锁
  4. recursive_timed_muetx:时间递归锁

以及两种基于ARII的加锁策略:

  1. lock_guard:作用域锁
  2. unique_lock:独占锁

mutex

mutex是最基础的互斥锁,可以对资源进行加锁解锁。

成员函数:

  • lock:加锁
  • unlock:解锁
  • try_lock:如果没上锁就加锁,上锁了就返回

可以看出,mutex的使用非常简单。

示例:

cpp 复制代码
int num = 0;

void test(int n)
{
    for (int i = 0; i < n; i++)
    {
        num++;
    }
}

int main()
{
    std::mutex mtx;

    std::thread t1(test, 2000);
    std::thread t2(test, 2000);

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

该代码中,两个线程一起对同一个num++,每个线程2000次,但是最后输出的num很有可能比2000少,因为num++不是原子性的。此时对其加锁:

cpp 复制代码
int num = 0;

void test(int n, std::mutex& mtx)
{
    for (int i = 0; i < n; i++)
    {
        mtx.lock();
        num++;
        mtx.unlock();
    }
}

int main()
{
    std::mutex mtx;

    std::thread t1(test, 2000, std::ref(mtx));
    std::thread t2(test, 2000, std::ref(mtx));

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

通过std::mutex mtx定义了一个名为mtx的锁,随后通过std::thread将锁作为参数传入线程,在每次num++前加锁,后释放锁。


timed_mutex

时间锁就是限定每次申请锁的时长,如果超过一定时间没有申请到锁,就返回。

成员函数:

  • lock:加锁
  • unlock:解锁
  • try_lock:如果没上锁就加锁,上锁了就返回
  • try_lock_until:如果到指定时间还没申请到锁就返回false,申请到锁返回true
  • try_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true

通过之前的经验,可以猜出try_lock_until要传入一个time_point时间点,而try_lock_for要传入一个时间段duration

try_lock_for为例:

cpp 复制代码
int num = 0;

void test(int n, std::timed_mutex& mtx)
{
    while (n)
    {
        bool ret = mtx.try_lock_for(std::chrono::microseconds(1));
        if (ret)
        {
            num++;
            n--;
            mtx.unlock();
        }
        else
        {
            std::cout << "加锁超时" << std::endl;
        }
    }
}

int main()
{
    std::timed_mutex mtx;

    std::thread t1(test, 2000, std::ref(mtx));
    std::thread t2(test, 2000, std::ref(mtx));

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

此处每次申请锁不超过一微秒,如果try_lock_for返回true说明抢到锁了,进行num++,反之则输出加锁超时


recursive_mutex

递归锁用于解决函数的递归造成的死锁,比如这样:

cpp 复制代码
int num = 0;

void test(int n, std::mutex& mtx)
{
    if (n <= 0)
        return;

    mtx.lock();

    test(n - 1, mtx);

    mtx.unlock();
}

int main()
{
    std::mutex mtx;
    std::thread t1(test, 2000, std::ref(mtx));

    t1.join();
    std::cout << "num = " << num << std::endl;

    return 0;
}

函数test中会产生死锁,第一次递归对mtx加锁,第二次递归时由于自己已经占有锁了,再次申请锁就会阻塞,导致死锁。

递归锁就是用于解决这样的自己与自己造成的死锁局面。

cpp 复制代码
int num = 0;

void test(int n, std::recursive_mutex& mtx)
{
    if (n <= 0)
        return;

    mtx.lock();

    test(n - 1, mtx);

    mtx.unlock();
}

int main()
{
    std::recursive_mutexmtx;
    std::thread t1(test, 2000, std::ref(mtx));

    t1.join();
    std::cout << "num = " << num << std::endl;

    return 0;
}

recursive_mutexmutex的用法完全一致,以上代码中只需要把mutex换为recursive_mutex就可以避免死锁。

因为mutex.lock()时,如果申请不到锁,不论是谁占有这把锁,都会陷入阻塞,直到锁被释放。recursive_mutex则会记录是谁占有这把锁,在recursive_mutex.lock()时,会检查申请锁的线程和占有锁的线程是不是同一个,如果是同一个,则直接申请成功,因此可以避免递归死锁。


lock_guard

在使用锁的过程中,最忌讳的就是忘记解锁,这就会导致一个线程一直持有锁,其他线程无法访问到资源。但是难道每次用完锁后解锁,就可以保证锁被释放吗?

看到这个例子:

cpp 复制代码
void test(int n, std::mutex& mtx)
{
    mtx.lock();
    // 抛异常
    mtx.unlock();
}

C++作为一门面向对象语言,带有异常机制,一旦抛出异常,就会直接结束函数栈帧,一直跳转到cache。以上代码中,如果抛异常了,那么mtx.unlock()根本就不会执行,导致锁没法释放。

因此C++引入了RAII机制来管理锁,利用对象的生命周期来实现加锁和解锁,原理如下:

cpp 复制代码
template <typename mutex_type>
class LockGuard
{
public:
    LockGuard(mutex_type& lock)
        : _lk(lock)
    {
        _lk.lock();
    }

    ~LockGuard()
    {
        _lk.unlock();
    }

private:
    mutex_type& _lk;
};

void test(int n, std::mutex& mtx)
{
    LockGuard<std::mutex> guard(mtx);
    // 抛异常
}

LockGuard这个类,在构造时接受一个锁,随后加锁,在析构时自动解锁。那么加锁的时间就与对象的生命周期绑定了。而就算经过异常退出,对象也会正常析构,从而加锁。

标准库std::lock_guard就是这个原理,其接收一个锁类型的模板参数,在构造中调用lock,析构中调用unlock,完成对锁的自动管理。

cpp 复制代码
void test(int n, std::mutex& mtx)
{
    std::lock_guard<std::mutex> guard(mtx);
    // 抛异常
}

使用了lock_guard后,就不用再自己手动解锁了。


unique_lock

lock_guard的可操作性很低,只有构造和析构两个函数,也就是只有自动释放锁的能力。而unique_lock功能更加丰富,而且可以自由操作锁。

unique_lock在构造时,可以传入一把锁,在构造的同时会对该锁进行加锁。在unique_lock析构时,判断当前的锁有没有加锁,如果加锁了就先释放锁,后销毁对象。

而在构造与析构之间,也就是整个unique_lock的生命周期,可以自由的加锁解锁:

  • lock:加锁
  • unlock:解锁
  • try_lock:如果没上锁就加锁,上锁了就返回
  • try_lock_until:如果到指定时间还没申请到锁就返回false,申请到锁返回true
  • try_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true

提供了以上五个接口,也就是说可以作用于前面的任何一款锁。另外的unique_lcok还允许赋值operator=,调用赋值时,如果当前锁没有持有锁,那么直接拷贝。如果当前锁持有锁,那么把锁的所有权转移给新的unique_lcok,自己不再持有锁。


atomic

在多线程情况下要加锁,就是因为很多操作不是原子性的。但是有一些简单的操作,比如num++,每次都加锁解锁,性能必然会降低。因此C++又提供了原子库<atomic>,其实现了简单操作的原子化,一些简单的++、--等都实现了原子化,可以不加锁也没有线程安全,需要头文件<atomic>

atomic

支持的类型如下:

  1. 可以通过简单的拷贝完成复制,而不需要调用构造函数与拷贝构造等
  2. 类型的大小不超过 std::atomic 的内部实现所支持的最大大小(通常是与机器字大小相同)

最常见的满足以上要求的类型就是内置类型,比如char,各种整型,浮点型,以及指针。

使用起来也很简单:

cpp 复制代码
atomic<类型> 变量名;

这样即可定义一个原子的类型。

示例:

cpp 复制代码
std::atomic<int> num = 0;

void test(int n)
{
    for (int i = 0; i < n; i++)
    {
        num++;
    }
}

int main()
{
    std::thread t1(test, 2000);
    std::thread t2(test, 2000);

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

    std::cout << "num = " << num << std::endl;

    return 0;
}

以上是一个多线程代码,但是我们并没有加锁,却是线程安全的,因为num是一个atomic<int> 类型的变量,num++是一个原子操作。

atomic类的成员函数如下:

首先就是实现了operator++operator--,自增自减的操作是原子的。

再比如说fetch_add,用于实现对一个原子类型增加指定值,该过程也是原子的。

cpp 复制代码
std::atomic<int> num = 3;
num.fetch_add(5);

以上代码完成了3 + 5的计算,且过程是原子性的,其余操作也是类似的:

  • fetch_add:原子性,增加指定的值
  • fetch_sub:原子性,减少指定的值
  • fetch_and:原子性,与指定值按位与
  • fetch_or:原子性,与指定值按位或
  • fetch_xor:原子性,与指定值按位异或

还有一些其它接口:

store用于设定原子类型为指定值:

cpp 复制代码
std::atomic<int> num = 3;
num.store(100);

num.store(100)相当于num = 100,但是过程是原子性的。

load用于获取原子类型当前的值,也是原子的。

operator T是隐式类型转换,也就是从atomic<T>转化为T类型,此时就可以把原子类型当作一般类型来使用了,不过要注意的是,隐式转换后就是一般类型,不再具有原子性了!


CAS

C++之所以可以实现变量的原子操作,是基于CAS的原子操作,这是一个硬件级别的操作,其涉及三个操作数:

  1. 内存位置
  2. 预期值
  3. 更新值

操作流程为:读取内存位置的当前值,判断是否与预期值相等,如果相等,将其变为更新值,如果不相等,返回当前值

比如在gcc编译器中,内置了函数__sync_bool_compare_and_swap,其用于进行CAS操作:

cpp 复制代码
bool __sync_bool_compare_and_swap(type* ptr, type oldval, type newval);
  1. ptr:内存位置,指向要操作的变量
  2. oldval:预期值,即预计该变量原先的值
  3. newval:更新值,希望把这个变量设置的值

返回值:如果修改成功返回true,修改失败返回false

比如通过CAS实现一个原子的自增:

cpp 复制代码
while(__sync_bool_compare_and_swap(&x, x, x + 1) == false);

这样短短一行代码就可以实现原子自增,首先读取&x,获取x的地址,随后传入变量x的当前值,预期值传入x + 1,表示自增。

比如说在&x后读取到了x的当前值为5,的此时另一个线程打断了操作,修改x = 10。进入函数__sync_bool_compare_and_swap后,发现预期值 = 5,而当前x = 10,说明被其他线程修改了,直接返回false表示修改失败,进入下一轮while循环。

也就是说基于CAS实现的原子性,不是真的原子性,而是检测到在修改变量的过程中,有其它人来修改了变量,就终止操作防止线程安全错误。

这个函数在C/C++里面是没法直接使用的,而是内置在编译器中,这是因为这个函数绕过了操作系统,直接与处理器的指令交互,因为CAS操作要非常迅速,否则就会出现相互打断的问题。直接通过编译器与处理器的原子指令交互,比通过操作系统内核要快得多。


condition_variable

谈到锁,自然也要谈条件变量,这是线程同步的重要手段,C++将条件变量放在头文件<condition_variable>中。

condition_variable

condition_variable只有一个无参的构造函数,且删除了拷贝构造,不允许拷贝。

  • 等待:
  1. wait:进入条件变量的等待队列
  2. wait_for:进入条件变量的等待队列,一定时间后如果没有被唤醒,则不再等待返回false
  3. wait_until:进入条件变量的等待队列,到指定时间后如果没有被唤醒,则不再等待返回false

第一个wait需要传入一把锁unique_lock<mutex>,此处要求必须使用unique_lock<mutex>。而后续两个与时间相关的等待,分别要传入时间段duration和时间点time_point。至于为什么要传入一把锁,这属于并发编程的知识,就不在博客中讲解了。

  • 唤醒:
  1. notify_one:唤醒等待队列的第一个线程
  2. notify_all:唤醒等待队列的所有线程

示例:让两个线程从0 - 100,轮流输出奇数偶数。

cpp 复制代码
int n = 0;
bool flag = true;

std::mutex mtx;
std::condition_variable cv; // 条件变量

void func(bool run) // run用于标识是否轮到当前线程输出
{
    while (n < 100)
    {
        std::unique_lock<std::mutex> lock(mtx);

        while (flag != run) // 使用while代替if,防止伪唤醒
            cv.wait(lock);  // 没轮到当前线程,进入条件变量等待

        std::cout << n << std::endl;
        n++;
        flag = !flag;
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(func, true);  // falg == true 输出偶数
    std::thread t2(func, false); // falg == false 输出奇数

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

    return 0;
}

这就是一个简单的,两个线程通过条件变量相互制约的案例,展示了condition_variable的基础用法。

再回到wait函数,wait的声明如下:

cpp 复制代码
void wait (unique_lock<mutex>& lck);

template <class Predicate>
  void wait (unique_lock<mutex>& lck, Predicate pred);

其有两个重载,第一个只有一个参数,也就是我刚刚提到的只要传入一个unique_lock<mutex>。第二个重载允许传入第二个参数pred,这是一个可调用对象,用于作为条件变量的判断值。

wait的第二个参数要求是一个可调用对象,返回值类型伪bool,作用如下:

  • 返回true:表示条件成立,wait直接返回,不进入等待队列
  • 返回false:表示条件不成立,wait阻塞,进入等待队列直到被唤醒

在刚刚的案例中,以下语句负责控制条件变量:

cpp 复制代码
while (flag != run)
	cv.wait(lock); 

实际上在condition_variable中,无需这样写判断语句,而是可以通过可调用对象传入条件变量内部:

cpp 复制代码
void func(bool run)
{
    auto cond_func = [&](){
        return flag == run;
        };

    while (n < 100)
    {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, cond_func);

        std::cout << n << std::endl;
        n++;
        flag = !flag;
        cv.notify_one();
    }
}

此处给wait函数第二个参数传入了一个lambda表达式cond_func,其返回一个boolflag == run,当这个值为true说明轮到当前线程执行,也就是条件成立,当前线程不会进入等待队列。

另外的,在等待队列的线程被唤醒后,也会触发一次该函数的条件判断,防止伪唤醒。


相关推荐
邂逅自己2 分钟前
Java入门程序-HelloWorld
java·开发语言
小周的C语言学习笔记27 分钟前
鹏哥C语言33---循环语句 for
c语言·c++·算法
自身就是太阳35 分钟前
Maven的高级特性
java·开发语言·数据库·后端·spring·maven
hakesashou41 分钟前
ruby和python哪个好学
开发语言·python·ruby
林一怂儿1 小时前
H5 three.js 实现六年级观察物体
开发语言·javascript
ZH_qaq1 小时前
【洛谷】P11062 【MX-X4-T2】「Jason-1」加法 的题解
c++·算法
NiNg_1_2341 小时前
Python协程详解
开发语言·python
黑白子20001 小时前
python定时任务,定时爬取水质和天气
开发语言·python
9ilk1 小时前
【与C++的邂逅】--- C++的IO流
开发语言·c++
是小满满满满吗1 小时前
C++中的继承
开发语言·c++·python