Bootstrap

C++11 多线程

        线程(thread)是操作系统能够进行运算调度的最小单位,它被包含在进程中,是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程执行不同的任务。

C++11 之前没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样的。

C++11 增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。

一、创建线程

头文件:

#include <thread>

线程类:

std::thread

构造函数:

1)默认构造函数,构建一个线程对象,不执行任何任务(不会创建 / 启动子线程)。

thread() noexcept;

2)创建线程对象,在线程中执行任务函数 Fx 中的代码,Ax 是要传递给任务函数 Fx 的参数。任务函数 Fx 可以是普通函数、类的非静态函数、类的静态函数、匿名函数、仿函数

 3)删除了拷贝构造函数,不允许线程对象之间的拷贝。

thread(const thread&) = delete;

4)移动构造函数,将线程 other 的资源所有权转移给新创建的线程对象。

thread(thread&& other) noexcept;

赋值函数

thread& operator=(thread&& other)noexcept;

thread& operator=(const other&)=delete;

线程中的资源不能被复制,如果 other 是右值,会进行资源所有权的转移,如果 other 是左值,禁止拷贝。

创建线程案例:

#include <thread>
#include <windows.h>
#include <iostream>

void func(int num, const std::string& str) {
    for (int i = 0; i < 10; i++) {
        std::cout << "func" << num << str << std::endl;
        Sleep(1000);
    }
}

int main() {

    std::thread t1(func, 3, "continue");
    std::cout << "start" << std::endl;
    for (int i = 0; i < 10; i++) {
        std::cout << "main continue" << std::endl;
        Sleep(1000);
    }
    std::cout << "end" << std::endl;
    return 0;
}

运行结果:

报错问题,而且内部执行顺序也很杂乱。

报错需要在程序结尾加上join(),回收线程 t1 的资源。

main 中的程序叫做主程序(主进程),t1 叫做子线程,主线程只有一个,子线程可以很多,与计算机硬件资源有关,一般电脑几百个,服务器几千个。

构造函数2 所有函数构建线程 

#include <thread>
#include <windows.h>
#include <iostream>

void func(int num, const std::string& str) {
    for (int i = 0; i < 10; i++) {
        std::cout << "func" << num << str << std::endl;
        Sleep(1000);
    }
}

// 仿函数
class MyThread1 {
public:
    void operator()(int num, const std::string& str) {
        for (int i = 0; i < 10; i++) {
            std::cout << "仿函数" << num << str << std::endl;
            Sleep(1000);
        }
    }
};

class MyThread2 {
public:
    // 类的静态成员函数
    static void s_fun(int num, const std::string& str) {
        for (int i = 0; i < 10; i++) {
            std::cout << "static members function" << num << str << std::endl;
            Sleep(1000);
        }
    }
};

class MyThread3 {
public:
    // 类的普通成员函数
    void fuc(int num, const std::string& str) {
        for (int i = 0; i < 10; i++) {
            std::cout << "class func" << num << str << std::endl;
            Sleep(1000);
        }
    }
};

int main() {
    // 普通函数 创建线程
    std::thread t1(func, 1, "continue1");

    // lambda 函数创建线程
    auto f = [](int num, const std::string& str) {
        for (int i = 0; i < 10; i++) {
            std::cout << "lambda" << num << str << std::endl;
            Sleep(1000);
        }
    };
    std::thread t2(f, 0, "continue2");

    // 仿函数 创建线程
    std::thread t3(MyThread1(), -1, "continue3");

    // 静态成员函数 创建线程
    std::thread t4(MyThread2::s_fun, -2, "continue4");

    // 类的普通成员数(要求类创建的对象的生命周期比子线程长) 创建线程
    MyThread3 mtf;
    std::thread t5(&MyThread3::fuc, &mtf, -3, "continue5"); // 第二个参数填写对象的地址

    std::cout << "start" << std::endl;
    for (int i = 0; i < 10; i++) {
        std::cout << "main  continue" << std::endl;
        Sleep(1000);
    }
    std::cout << "end" << std::endl;

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    return 0;
}

运行结果:

依旧是很乱的效果,这里只是说明一下各种函数创建线程的使用方法。

注意事项

  • 先创建的子线程不一定跑的最快(程序运行的速度有很大的偶然性)。
  • 线程的任务函数返回后,子线程将终止。
  • 如果主程序(主线程)退出(正常退出 / 意外终止),全部子线程都将强行终止。

测试代码:

#include <thread>
#include <windows.h>
#include <iostream>

void func(int num, const std::string& str) {
    for (int i = 0; i < 10; i++) {
        std::cout << "func" << num << str << std::endl;
        Sleep(1000);
    }
}

int main() {
    // 普通函数 创建线程
    std::thread t1(func, 1, "continue1");

    std::cout << "start" << std::endl;
    for (int i = 0; i < 10; i++) {
        std::cout << "main  continue" << std::endl;
        Sleep(1000);
    }
    std::cout << "end" << std::endl;

    t1.join();
    return 0;
}

运行结果:

运行没有问题 

修改代码,让主进程(主函数)的循环先结束。

#include <thread>
#include <windows.h>
#include <iostream>

void func(int num, const std::string& str) {
    for (int i = 0; i < 10; i++) {
        std::cout << "func" << num << str << std::endl;
        Sleep(1000);
    }
}

int main() {
    // 普通函数 创建线程
    std::thread t1(func, 1, "continue1");

    std::cout << "start" << std::endl;
    for (int i = 0; i < 5; i++)  // 5 s 结束
    {
        std::cout << "main  continue" << std::endl;
        Sleep(1000);
    }
    std::cout << "end" << std::endl;
    
    return 0; // 提前 return 先结束
    t1.join();
    return 0;
}

运行结果:

 程序 5 秒后报错,子线程依然在执行,主要原因是这里的主线程相当于这个调试窗口,主线程并没有完全退出,只要点击终止、重试和忽略的时间和主线程终止时间刚好,子线程就不会在继续执行了。主线程退出,子线程就会终止。      


二、线程资源的回收

        虽然同一个进程的多个线程共享进程的栈空间,但是每个子线程在这个栈中拥有自己私有的栈空间,所有需要回收资源。

回收子进程的资源的两种方法:

1)在主程序中,调用 join() 成员函数等待子线程退出,回收他的资源,如果子线程已退出,join() 函数立即返回,否则会产生阻塞,直到子线程退出。

2)在子程序中,调用 detach() 成员函数分离子线程,子线程退出时,系统自动回收资源。子线程被分离后,不能 join() ,否则程序会报错。在分离之后主线程不能退出,退出后就直接结束。

可以用 joinable() 成员函数可以判断子线程的分离状态,返回 bool,分离返回 false,未分离返回 true。


三、this_thread 的全局函数

        C++11 提供命名空间 this_thread 来表示当前线程,该命名空间中有四个函数:

  • get_id()
  • sleep_for()
  • sleep_until()
  • yield()

1)get_id()

thread::id get_id() noexcept;

该函数用于获取线程 ID,每个线程都有一个 ID,每个 ID 不同,返回值类型是 thread::id(thread自定义的数据类型),thread 类也有同名的成员函数。

测试代码:

#include <thread> 
#include <windows.h>
#include <iostream>

void func(int num, const std::string& str) {
    std::cout << "namespace 方法 " << str << " ID:" << std::this_thread::get_id() << std::endl;
    for (int i = 0; i < 10; i++) {
        std::cout << "func" << num << str << std::endl;
        Sleep(1000);
    }
}

int main() {
    // 普通函数 创建线程
    std::thread t1(func, 1, "Subthread 1");
    std::thread t2(func, 2, "Subthread 2");

    std::cout << "主线程 ID:" << std::this_thread::get_id() << std::endl;
    std::cout << "类方法 子线程 t1 ID:" << t1.get_id() << std::endl;
    std::cout << "类方法 子线程 t2 ID:" << t2.get_id() << std::endl;

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

运行结果:

相同的程序每次产生的线程 ID 是不一样的。 

2)sleep_for()

template <class _Rep, class _Period>
    void sleep_for(const chrono::duration<_Rep, _Period>& _Rel_time)

该函数让线程休眠一段时间。

使用代码:

std::this_thread::sleep_for(std::chrono::nanoseconds(1));  // 休眠 1 纳秒
std::this_thread::sleep_for(std::chrono::microseconds(1)); // 休眠 1 微秒
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 休眠 1 毫秒
std::this_thread::sleep_for(std::chrono::seconds(1));      // 休眠 1 秒
std::this_thread::sleep_for(std::chrono::minutes(1));      // 休眠 1 分钟
std::this_thread::sleep_for(std::chrono::hours(1));;       // 休眠 1 小时

3)sleep_until()

template <class _Clock, class _Duration>
    void sleep_until(const chrono::time_point<_Clock, _Duration>& _Abs_time)

该函数让线程休眠至指定时间点(实现定时任务)。使用较麻烦,需要将某年某月某日这个字符串转化为时间点。

使用代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <thread>
#include <chrono>
// Print Current Time
void print_time_point(std::chrono::system_clock::time_point timePoint)
{
    std::time_t timeStamp = std::chrono::system_clock::to_time_t(timePoint);
    std::cout << std::ctime(&timeStamp) << std::endl;
}
void threadFunc()
{
    std::cout << "Current Time :: ";
    // Print Current Time
    print_time_point(std::chrono::system_clock::now());
    // create a time point pointing to 10 second in future
    std::chrono::system_clock::time_point timePoint =
        std::chrono::system_clock::now() + std::chrono::seconds(10);
    std::cout << "Going to Sleep Until :: "; print_time_point(timePoint);
    // Sleep Till specified time point
    // Accepts std::chrono::system_clock::time_point as argument
    std::this_thread::sleep_until(timePoint);
    std::cout << "Current Time :: ";
    // Print Current Time
    print_time_point(std::chrono::system_clock::now());
}
int main()
{
    std::thread th(&threadFunc);
    th.join();
    return 0;
}

运行结果:

 4)yiled()

void yield() noexcept;

该函数让线程主动让出自己已经抢到的CPU时间片

作用是当前线程放弃执行,系统调度另一个线程执行。

while(!isDone()); // Bad
while(!isDone()) yield(); // Good

5)thread 类其他的成员函数

void swap(std::thread& other);   // 交换两个线程对象
static unsigned hardware_concurrency() noexcept; // 返回硬件线程

使用测试代码:

#include <thread> 
#include <windows.h>
#include <iostream>

void func(int num, const std::string& str) {
    for (int i = 0; i < 10; i++) {
        std::cout << "func" << num << str << std::endl;
        Sleep(1000);
    }
}

int main() {
    // 普通函数 创建线程
    std::thread t1(func, 1, "Subthread 1");
    std::thread t2(func, 2, "Subthread 2");

    std::cout << "主线程 ID:" << std::this_thread::get_id() << std::endl;
    std::cout << "t1 线程 ID:" << t1.get_id() << std::endl;
    std::cout << "t2 线程 ID:" << t2.get_id() << std::endl;
    t1.swap(t2); // 交换 t1 和 t2 的线程
    std::cout << "t1 线程 ID:" << t1.get_id() << std::endl;
    std::cout << "t2 线程 ID:" << t2.get_id() << std::endl;

    std::thread t3 = std::move(t2); // t2 转义为 右值,使用移动构造函数

    std::cout << "处理器核数:" << t1.hardware_concurrency() << std::endl;

    t1.join();
    //t2.join(); // t2 转移资源后不再代表线程
    t3.join();
    return 0;
}

四、call_once 函数

        在多线程环境中,某些函数只能被调用一次,初始化某些对象,对象只能被初始化一次。在线程的任务函数中,可以使用 std::call_once() 保证某个函数只被调用一次。

头文件:

#include <mutex>

函数原型:

template <class _Fn, class... _Args>
void(call_once)(once_flag& _Once, _Fn&& _Fx, _Args&&... _Ax)

第一个参数 once_flag,用于标记函数 Fx 是否已经被执行过。

第二个参数 Fx 为只调用一次的函数。

后面的参数为函数参数列表

使用测试代码:

#include <thread>
#include <mutex>
#include <iostream>
#include <Windows.h>

// define once_flag variable
std::once_flag onceflag;

void onceFunc(int num, const std::string& str) {
    std::cout << "once called function" << std::endl;
}

void func(int num, const std::string& str) {
    // called onceFunc function only once 
    std::call_once(onceflag, onceFunc, 0, "onceThread");
    for (int i = 0; i < 10; i++) {
        std::cout << str << num << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main() {
    std::thread t1(func, 1, "thread1");
    std::thread t2(func, 2, "thread2");

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

    return 0;
}

运行结果:

可以看到 onceFunc 函数只被调用一次。

如果使用一个全局变量作为标记,对全局变量进行判断,满足条件后执行函数,再对全局变量进行修改,使其下次运行时不满足判断条件。这种方法是不行的!


五、native_handle 函数

        C++11 定义了线程标准,不同的平台和编译器在实现的时候,本质上是对操作系统的线程库进行封装,会损失一部分功能。

为弥补 C++11 线程库的不足,thread 提供了 native_handle() 成员函数,用于获得与操作系统相关的原生线程句柄,操作系统原生的线程库就可以用原生线程句柄操作线程。

#include <thread>
#include <iostream>

void func() {
    for (int i = 0; i < 10; i++) {
        std::cout << "subThread" << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main() {

    std::thread t1(func);

    std::thread::native_handle_type t_native_handle = t1.native_handle();
    std::thread::id t_id = t1.get_id();

    std::cout << "t1 thread handle:" << t_id << std::endl;
    std::cout << "t1 native handle:" << t_native_handle << std::endl;

    t1.join();

    return 0;
}

get_id() 和 native_handle() 的区别:

get_id() 返回的线程ID

get_id() 实际返回的是一个 class(std::thread::id) 而不是数字也不是特定平台句柄。

native_handle() 返回底层实现定义的线程句柄。

native_handle 函数返回其名称所暗示的内容,可由底层操作系统线程函数使用的本机句柄


六、线程安全

        在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的运行,不会出现数据污染等意外情况。

同一进程中的多个线程共享该进程中全部的系统资源。多线程访问同一个资源时就是产生冲突。

冲突测试代码:

#include <iostream>
#include <thread>

// global variabl
int g_a = 0; 

// 普通函数
void func() {
    for (int i = 0; i < 10000000; i++) {
        g_a++;
    }
}


int main() {

    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();

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

运行结果:

每次的值都不一样,也都没有到预期的值。

顺序性

程序按照代码的先后顺序执行。

CPU 为了提高程序整体的执行效率,可能对代码进行优化,按更高效的顺序执行

CPU 不保证完全按照代码的顺序执行,但他会保证最终的结果与按代码顺序执行的结果一致。

int var = 10;
var += 5;
--var;
var -= 10;
var = 20;
// 对于 CPU 来说
// 可能直接将代码优化为 var = 20

可见性

        线程操作共享变量时,会将该变量从内存加载到 CPU 缓存中,修改该变量后,CPU 会立即更新缓存,但不会立即将他写回内存。这时候,如果其他进程访问该变量,从内存中读到的就是旧数据,而非经过线程操作后的数据。当多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程能立即看到。

原子性

CPU执行指令:读取指令 -> 读取内存 -> 执行指令 -> 写回内存

int a = 10;
a++;
// 从内存中读取 a 的值
// 把 i + 1
// 结果放回内存

原子操作:一个操作(多个步骤)要么全部执行,要么全部都不执行。

如何保证线程安全

  • volatile 关键字
  • 原子操作(原子类型)
  • 线程同步(锁)

volatile 关键字

  • 保证内存变量的可见性。
  • 禁止代码优化(重排序)。

volatile 关键字测试代码: 

#include <iostream>
#include <thread>

// global variabl
volatile int g_a = 0; 

// 普通函数
void func() {
    for (int i = 0; i < 10000000; i++) {
        g_a++;
    }
}

int main() {
    
    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();

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

运行结果:

非预期的结果。volatile 关键字只解决了内存可见性的问题。


七、线程同步

多个线程协同工作,协商如何使用共享资源

  • 互斥锁(互斥量)
  • 条件变量
  • 生产 / 消费者模型

互斥锁

  • 加锁和解锁,确保同一时间只有一个线程访问共享资源。
  • 访问共享资源之前加锁,访问完成后释放锁。
  • 如果某线程保持有锁,其他线程形成等待队列。

C++11 提供了四种互斥锁

  • mutex:互斥锁
  • timed_mutex:带超时机制的互斥锁
  • recursive_mutex:递归互斥锁
  • recursive_timed_mutex:带超时机制的递归互斥锁

头文件:

#include <mutex>

mutex类

1)加锁 lock()

        互斥锁有锁定和未锁定两种状态。

  • 如果互斥锁是未锁定状态,调用 lock() 成员函数的线程会的到互斥锁的所有权,并将其上锁。
  • 如果互斥锁是锁定状态,调用 lock() 成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。

2)解锁 unlock()

        只有持有锁的线程才能开锁。

3)尝试加锁 trylock()

  • 如果互斥锁是未锁定状态,则加锁成功,函数返回 true。
  • 如果互斥锁是锁定状态,则加锁失败,函数 立即 返回 false。(线程不会阻塞等待)

系统公共资源不是只有一个时,就可以对线程进行尝试加锁,如果加锁失败,就会选择别的资源继续尝试加锁。

互斥锁测试代码:

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

// global variable
int g_a = 0;

std::mutex mtx; // 创建互斥锁对象,保护共享资源 g_a 变量

void func() {
    for (int i = 0; i < 100000000; i++) {
        mtx.lock();
        g_a++;
        mtx.unlock();
    }
}

int main() {
    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();

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

运行结果:

预期结果是对的,但等了一段时间后出的结果,这就是因为线程开锁解锁,线程阻塞。

timed_mutex类

延迟互斥锁相对mutex类增加了两个成员函数:

  • bool try_lock_for(时间长度);
  • bool try_lock_until(时间点);

recursive_mutex类

递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题

递归互斥锁测试代码:

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

class classA {
    //std::mutex m_mtx;
    std::recursive_mutex m_mtx;
public:
    void func1() {
        m_mtx.lock();
        std::cout << "func1" << std::endl;
        m_mtx.unlock();
    }
    void func2() {
        m_mtx.lock();
        std::cout << "func2" << std::endl;
        func1();
        m_mtx.unlock();
    }
};

int main() {
    classA ca;
    //ca.func1();
    ca.func2();
    // 死锁,同一个线程只有在解锁后才能加锁
    // func2 没有解锁,func1 无法加锁
    // 需要将互斥锁改为递归互斥锁

    return 0;
}

运行结果:

递归锁就是运用于这种嵌套的线程,递归锁也有延时递归锁。 

lock_guard类

lock_guard 是模板类,可以简化互斥锁的使用。

lock_guard 在构造函数中加锁,在析构函数中解锁。

lock_guard 采用了 RAII 思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。

lock_guard运用测试代码:

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

int g_a = 0;
std::mutex mtx;

void func() {
    for (int i = 0; i < 100000000; i++) {
        // lock_guard 需要放在需要保护的共享资源之前
        // 否则会出现错误结果
        std::lock_guard<std::mutex> mlock(mtx);
        g_a++;
        // std::lock_guard<std::mutex> mlock(mtx); // X
    }
}   

int main() {
    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();

    std::cout << g_a << std::endl;

    return 0;
}

运行结果:


八、条件变量 - 生产 / 消费者模型

条件变量

  • 条件变量是一种线程机制,当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
  • 为了保护线程资源,条件变量需要和互斥锁一起使用。
  • 生产/消费者模型(可以实现高速缓存队列)

生产者可以是一个线程也可以是多个线程,消费者一般是多个线程,这多个线程有一个通俗的名称,就是线程池。

C++11 的条件变量提供两个类:

  • condition_variable:只支持与普通 mutex 搭配使用,效率更高。
  • condition_variable_any:是一种通用的条件变量,可以与任意 mutex 搭配使用(包括用户自定义的锁类型)。

 条件变量头文件:

#include <condition_variable>

 1)condition_variable类

主要成员函数:

  • condition_variable() 默认构造函数。初始化条件变量。
  • condition_variable(const condition_variable &)=delete 禁止拷贝。
  • condition_variable& condition_variable::operator=(const condition_variable &)=delete 禁止赋值。
  • notify_once() 通知一个等待的线程。
  • notify_all() 通知全部等待的线程。
  • wait(unique_lock<mutex> lock) 阻塞当前线程,直到通知到达。
  • wait(unique_lock<mutex> lock, Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。
  • wait_for(unique_lock<mutex> lock, 时间长度)
  • wait_for(unique_lock<mutex> lock, 时间长度, Pred pred)
  • wait_until(unique_lock<mutex> lock, 时间点)
  • wait_until(unique_lock<mutex> lock, 时间点, Pred pred)

unique_lock类

template<class Mutex>class unique_lock

unique_lock 是模板类,模板参数为互斥锁类型。

unique_lock 和 lock_guard 都是管理锁的辅助类,都是 RAII 风格。

区别在于:unique_lock 为配合 condition_variable,unique_lock 还有 lock() 和 unlock() 成员函数。

测试代码:

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

// 生产/消费者的实现写在一个类中。
class AA {
    std::mutex m_mutex; // 互斥锁
    std::condition_variable m_cond; // 条件变量
    std::queue<std::string, std::deque<std::string>> m_q; // 缓存队列
public:
    void incache(int num) { // 生产数据,num 指定数据的个数
        std::lock_guard<std::mutex> lock(m_mutex); // 申请加锁
        for (int i = 0; i < num; i++) {
            static int bh = 1; // 编号
            std::string message = std::to_string(bh++) + "号";
            m_q.push(message); // 把生产出来的数据入队
        }
        //m_cond.notify_one(); // 唤醒一个被当前条件变量阻塞的线程
        m_cond.notify_all(); // 唤醒全部线程
        // 生产的数据只有一个,用 notify_one()
        // 生产数据有多个,用 notify_all()
    }
    void outcache() { // 消费者线程任务函数
        std::string message; // 存放出队的数据
        while (true) {
            std::string message;
            {
                // 把互斥锁转换成 unique_lock,并申请加锁
                std::unique_lock<std::mutex> lock(m_mutex);

                // 如果队列非空,不进入循环,直接处理数据,必须循环不能选择
                // 条件变量存在虚假唤醒:消费者线程被唤醒后,缓存队列中没有数据
                // 拿不到线程的数据就是虚假唤醒
                while (m_q.empty()) {
                    m_cond.wait(lock); // 等待生产者唤醒信号
                }

                // 相当于上面的while循环
                //m_cond.wait(lock, [this] { return !m_q.empty(); });


                // 数据元素出队
                message = m_q.front(); m_q.pop();
                std::cout << "线程: " << std::this_thread::get_id() << "," << message << std::endl;
                
                lock.unlock();// unique_lock 也可以手动的解锁,作用域可以不加
            } // 加上作用域,在这里就可以解锁了。

            // 处理出队的数据(将数据消费)
            std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 假设处理数据需要 1ms
        }
    }
};

int main() {
    AA aa;

    std::thread t1(&AA::outcache, &aa); // 创建消费者线程 t1
    std::thread t2(&AA::outcache, &aa); // 创建消费者线程 t2
    std::thread t3(&AA::outcache, &aa); // 创建消费者线程 t3

    std::this_thread::sleep_for(std::chrono::seconds(2)); // 休眠 2s
    aa.incache(3); // 生产 3 个数据

    std::this_thread::sleep_for(std::chrono::seconds(3)); // 休眠 3s
    aa.incache(5); // 生产 5 个数据

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

    return 0;
}

九、原子类型

C++11 提供了atomic<T> 模板类(结构体),用于支持原子类型,模板参数可以是 bool、char、int、long、long long、指针类型(不支持浮点类型和自定义类型)。

原子操作由 CPU 指令提供支持,他的性能比锁和消息传递更高,并且不需要处理加锁和解锁的问题,支持修改、读取、交换、比较并交换等操作。

头文件:

#include <atomic>

测试代码:

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

std::atomic<int> g_a = 0; // 定义为原子类型

void func() {
    for (int i = 0; i < 100000000; i++) {
        g_a++;
    }
}

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

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

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

运行结果:

运行结果正确,运行速度相比用互斥锁要快。相当于轻量级的锁

;