C++11 多线程

news2024/9/27 17:25:30

        线程(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;
}

运行结果:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/139869.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FreeRTOS教程——定时器(二)

Free RTOS定时器 一、概念 一、概论 软件定时器允许设置一段时间&#xff0c;当设置的时间到达之后就执行指定的功能函数&#xff0c;被定时器 调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期&#xff0c; 简而言之&#xff0c;当定时…

switch分支结构

一. 简介switch结合case&#xff0c;能够判断一个变量或表达式与一系列值中的某个值是否相等&#xff0c;这里的每个值都被称为一个分支。switch语句在执行时&#xff0c;会先进行值的匹配&#xff0c;匹配成功时会进入到对应case语句。再根据是否有 break语句&#xff0c;判断…

手把手教你正确地创建并配置一个SpringBoot项目

文章目录1. 安装Spring Boot Helper插件2. 创建SpringBoot项目3. 配置SpringBoot项目4. 选择修改配置&#xff08;选做&#xff09;4.1 修改端口号4.2 其他自定义配置5. SpringBoot热部署本文主要是针对IDEA社区版用户的&#xff0c;如果你是专业版的用户&#xff0c;那么是可以…

RHCE第三天之ssh远程连接服务

文章目录一、连接加密技术简介二、SSH的工作过程三、 SSH远程连接服务配置四、SSH实验SSH&#xff08;Secure Shell Protocol&#xff0c;安全的壳程序协议&#xff1a; 它可以通过数据包加密技术将等待传输的数据包加密后再传输到网络上。ssh协议本身提供两个服务器功能&#…

爬虫学习-验证码识别

反爬机制&#xff1a;验证码&#xff0c;识别验证码图片中的数据&#xff0c;用于模拟登陆识别验证码的操作人工肉眼识别(不推荐&#xff09;第三方自动识别(推荐)python第三方库&#xff1a;tesseract、ddddocr(7条消息) 小白都能轻松掌握&#xff0c;python最稳定的图片识别库…

探索用于NLP的Gensim库

Gensim的名字源自于"Generate Similar," 这个词是指Gensim可以用于生成类似的文本。这个词也可以被解释为"Generative Similarity," 表示Gensim可以用于生成相似的文本。Gensim是一个用于文本处理的库,可以用于计算文本之间的相似度,以及生成类似的文本。…

实验四:ESP8266WIFI通讯实验

本实验开发板基于&#xff1a;GD32F103我们首先需要看一下原理图 根据原理图可以看到&#xff0c;ESP8266是通过PA2 PA3这个串口进行通讯&#xff0c;PA13是控制它的复位&#xff0c;从芯片手册中可以看到PA2PA3是串口1&#xff0c;PA2是串口1的发送&#xff0c;PA3是串口1的接…

时间序列分析之ARIMA预测

预备知识 时间序列分析原理 时间序列分析之auto_arima自动调参 一、定义 ARIMA模型(Autoregressive Integrated Moving Average model)&#xff0c;差分整合移动平均自回归模型&#xff0c;又称整合移动平均自回归模型&#xff0c;时间序列预测分析方法之一。 ARIMA(p,d,q){A…

Go语言设计与实现 -- Channel

稍微需要注意一点的用法 类型断言 type dog struct {Name stringColor string }func main() {allChan : make(chan any, 10)allChan <- dog{Name: "lxy", Color: "yellow"}// 如果你这么写代码的话&#xff0c;你虽然拿到了一条狗&#xff0c;但是你…

Map遍历方法及效率

在大学的时候记得学过通过迭代器进行Map的遍历&#xff0c;但是从参加工作后&#xff0c;基本都是通过for循环遍历&#xff0c;没用过迭代器&#xff0c;于是去了解了Map的几种遍历方法并通过运行测试各自的速度。 注意&#xff1a;这里只讲通过遍历同时能过获取key和value的遍…

Imaging组件格式转换,Imaging图像转换

Imaging组件格式转换,Imaging图像转换 Imaging是一个.NET组件&#xff0c;它提供了一种加载、编辑和保存图片的简单方法。图像处理允许文件格式转换和图像转换(调整大小、裁剪或旋转以及翻转)。 使用GemBox.Imageing&#xff0c;您将获得一个快速可靠的组件&#xff0c;它易于使…

聊聊帮助别人这件事--爱摸鱼的美工(10)

聊聊帮助别人这件事 曾经我是一个不懂拒绝的人 有时帮助别人是本性的善良 有时内心也感觉是被迫而已 不快乐为什么还要做&#xff1f; 后来&#xff0c;我减少了无用社交 后来&#xff0c;我脸皮厚了学会了拒绝 才发现&#xff0c;恰如其分的帮助 让自己和别人都舒服 才发现&am…

【微服务】Nacos配置管理

文章目录统一配置管理在nacos中添加配置文件从微服务拉取配置配置热更新方式一方式二配置共享配置共享的优先级Nacos集群搭建集群结构图搭建集群统一配置管理 Nacos除了可以做注册中心&#xff0c;同样可以做配置管理来使用。 当微服务部署的实例越来越多&#xff0c;达到数十、…

慎用BeanUtils,性能真的拉跨

1 背景 之前在专栏中讲过“不推荐使用属性拷贝工具”&#xff0c;推荐直接定义转换类和方法使用 IDEA 插件自动填充 get / set 函数。 不推荐的主要理由是&#xff1a; 有些属性拷贝工具性能有点差 有些属性拷贝工具有“BUG” 使用属性拷贝工具容易存在一些隐患&#…

xxx.nginx转发+OpenResty(nginx升级版)_web服务器+lua

看上图&#xff0c;点开图发现图片空白不是网卡了&#xff0c;是内容有点多&#xff0c;缩小图片&#xff08;放大指定位置&#xff09;后食用。&#x1f601;内容分为三部分&#xff1a;nginx转发OpenResty(nginx升级版)_web服务器lua测试1.nginx转发1.1.搭建nginx 略&#xf…

【B样条曲线:计算系数】

对这篇文章的翻译&#xff0c;仅学习。 B样条曲线&#xff1a;计算系数 尽管de Boor的算法是计算b样条曲线上对应于给定u的点的标准方法&#xff0c;但我们在许多情况下确实需要这些系数(例如&#xff0c;曲线插值和逼近)。我们将举例说明一种简单的方法。 给定一条由n1个控制…

【PyQt】PyQt的缺陷及意外退出的可能原因

1 背景 最近在软件项目中比较深入的用到了 Qt GraphicsView Framework。PyQt 作为 Qt 的非官方Python绑定库&#xff08;Qt的官方Python绑定库是PySide&#xff09;总是出现很多意外崩溃/Bug&#xff0c;并且很难调试和追踪。 2 问题 软件项目中需要自定义 QGraphicsItem &a…

初识Cocos Creator

1.1 Cocos 不同时期与产品刚接触Cocos家族的时候&#xff0c;会有很多个Cocos的版本与分支&#xff0c;比如Cocos2d, Cocos2d-x, Cocos Creator 1.x, Cocos Creator 2.x, Cocos Cretor3D, Cocos Creator 3.x, CocosDashboard, 等我们先把Cocos 的主要产品脉络梳理一遍。对啦&am…

人体reid数据集

mars数据集 参考&#xff1a;https://blog.csdn.net/qq_34132310/article/details/83869605 整个Mars数据的大小大概有6.3G。 bbox_train文件夹中&#xff0c;有625个子文件夹&#xff08;代表着625个行人id&#xff09;&#xff0c;共包含了8298个小段轨迹&#xff08;track…

联合证券|战略新兴产业火了,高增长低估值股曝光

导读&#xff1a;2022年&#xff0c;A股商场值得记载的前史性大事件不断。这一年&#xff0c;A股商场上市公司数量正式打破5000家&#xff0c;战略新兴产业上市公司数量打破2500家&#xff0c;占比初次打破50%大关。这一年&#xff0c;A股商场顶住杂乱的外部环境因素&#xff0…