本文创建于 2024-12-04;修改于 2024-12-04

1.1 线程 thread

C++11 中提供的线程类叫做 std::thread,基于这个类创建一个新的线程非常的简单,只需要提供线程函效或者函效对象即可,并且可以同时指定线雇函数的參数。我们首先来了解一下这个类提供的一些常用 API :

1.2 互斥量

C++11提供如下4种语义的互斥量(mutex):

  • std::mutex,独占的互斥量,不能递归使用
  • std::time_mutex,带超时的独占互斥量,不能递归使用
  • std::recursive_mutex,递归互斥量,不带超时功能
  • std::recursive_timed_mutex,带超时的递归互斥量

lock_guard 和 unique_lock

lock_guard

  1. std:lock_guard 在构造函数中进行加锁,析构函数中进行解锁。

unique_lock

  1. .unique_ lock 是通用互斥包装體,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件交量一同使用。
  2. unique_ lockttlock_guard使用更加灵活,功能更加强大。
  3. 使用unique_lock需要付出更多的时间、性能成本。

使用场景:需要结合notify+wait的场景使用unique_lock;如果只是单纯的互斥使用lock guard

1.3 条件变量

条件变量的成员函数

1. wait

包含两种重载,第一种只包含unique_lock对象,另外一个Predicate 对象(等待条件),这里必须使用unique_lock,因为wait函数的工作原理

  • 当前线程调用wait() 后将被阻塞并且函数会解锁互斥量、直到另外某个线程调用notify_one或者notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait() 函数也是自动调用lock(),同理不能使用lock guard对象
  • 如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。
  • 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥量解锁,然后休眠,如果为true,则进行后面的操作。
2. wait_for

和wait不同的是,wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,该线程都会处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。

3. wait_until

函数原型:

void notify_one() noexcept;

作用:

与wait_for类似,只是wait_unti可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程 都会处于阻塞状态。如果超时或者收到唤醒通知,wait_until返回,剩下操作和wait类似

4. notify_one

函数原型:

void notify_one() noexcept;

作用:

解锁正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等待的线程多余一个,则唤醒的线程是不确定的

5. notify_all

函数原型:

void notify_all() noexcept;

作用:

解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。

1.4 原子变量

原子变量的作用大白话解释就是,把CPU对某一个数据的读(CPU从内存中读取数据)、修改(CPU对数据进行修改)、写(写回到内存)三步操作定义为一个整体操作,这个操作不可再被分割,必须一口气执行完读、修改、写三步操作。

atomic类

对原子变量对象的操作分为写、修改、读三种:

写:

store

exchange

赋值运算符 =

修改:

fetch_add

fetch_sub

fetch_and

fetch_or

fetch_xor

读:

load

#include <atomic>
#include <iostream>
#include <string>

class Base
{
public:
    Base(const std::string &name, const int age) :
        m_Name(name), m_Age(age) {}
    std::string m_Name;
    int m_Age;
};

int main(int argc, const char **argv)
{
    // 声明 atomic 对象
    std::atomic<char> a = 'a'; // 模版方法声明 atomic 对象
    std::atomic_int b; // 特化类型方法声明 atomic 对象,但是没有初始化
    std::atomic_init(&b, 9); // 使用 init 函数初始化 atomic 对象

    // 对 atomic 对象进行修改
    a.store('A');
    auto bb = b.exchange(99);
    b.fetch_add(1);

    // 指针类型
    Base base("luffy", 18);
    std::atomic<Base *> atc_base(&base);
    auto tmp = atc_base.load();

    // 打印结果
    std::cout << "a value: " << a.load() << std::endl;
    std::cout << "b value: " << b.load() << std::endl;
    std::cout << "bb value: " << bb << std::endl;
    std::cout << "base value: " << tmp->m_Name << " " << tmp->m_Age << std::endl;

    return 0;
}

使用原子变量进行线程同步

对需要保护的数据使用原子变量类型即可,注意不能是复合类型或浮点数类型。

内存顺序约束

1.5 异步操作

  • std:future类:异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
    • get()
    • wait()
    • wait_for()
    • wait_until()
  • std:promise:协助线程赋值的类,能够将数据和future对象绑定,为获取线程函数中的某个值提供便利。
    • set_value
    • set_value_at_thread_exit
  • std:aysnc:异步运行某个任务函数
  • std:packaged_task:将任务和feature绑定在一起的模板,是一种封装对任务的封装。