C++11 Thread线程池、死锁、并发

2024-09-06 05:20
文章标签 c++ 线程 并发 死锁 thread

本文主要是介绍C++11 Thread线程池、死锁、并发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、线程与进程

        进程:运行中的程序

        线程:进程中的小进程

二、线程库的使用

        包含头文件#include<thread>

2.1 thread函数

        具体代码

void show(string str) {cout << "This is my word : " << str << endl;
}int main() {thread thread1(show, str);return 0;
}

        函数声明:std::thread thread(Function *funp, 函数所需参数)

        解释:1.函数参数为一个函数名称,进程运行后会创建一个线程并执行函数内容

                   2. 返回值类型是一个thread类型的对象

        注意:单使用thread程序会报错,应该配合join函数使用,请看下文

2.2 join函数

        具体代码

void show() {cout << "This is my code." << endl;
}int main() {thread thread1(show);thread1.join();return 0;
}

        解释:如果不使用join,则show函数在打印完成之前,可能主函数main就已经return了。

                    当一个子线程调用join函数时,会阻塞主线程,直到当前子线程执行结束后,才

                    继续执行主线程。

        注意:join只会保证主线程阻塞,等待自己执行完。但两个子线程并不会因为彼此的join而互相阻塞,而是在同时进行。

 2.3 分离线程detach函数

        分离函数是指,主函数执行完毕后,需要保持子线程依旧在执行,并且程序不报错。

        具体代码

void show(string str) {cout << "This is my code:" << str << endl;
}int main() {thread thread1(show, "Jacker");thread1.detach();return 0;
}

        解释:一旦线程被分离出去,它就不再受原线程的控制和影响。因此,无法通过原线程

                  来获取该线程的执行结果或等待其结束。

2.4 joinable函数

        具体代码

void show(string str) {cout << "This is my code:" << str << endl;
}int main() {thread thread1(show, "Jacker");if(thread1.joinable()) {thread1.detach();}return 0;
}

        解释:判断当前线程是否可以调用join()或detach()

                        如果未判断,系统可能报错:SYSTEM_ERROR

2.5 ref函数

        ref函数可以对象的引用传递给线程

首先我们先来看以下代码,编译有错误

void add(int& num) {for (int i = 0; i < 10000; i++) {num += 1;}
}int main() {int num = 0;thread t1(add, num);if (t1.joinable()) {t1.detach();}std::cout << "num : " << num << std::endl;return 0;
}

        错误原因:以下是thread构造函数的声明

_NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) {_Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}

         可以看到,当我们传入num实参时,会被转换为右值引用_Args&&类型,但实际add函数中的参数为左值引用类型,二者不匹配,因此发生了错误。

        此时,只要将thread的第二个参数,从num改成std::ref(num),即可解决问题。具体如下:

thread t1(add, std::ref(num));

三、互斥量

        什么是线程安全?

答:当单线程执行的结果和多线程执行的结果相同时,线程安全。

3.1 共享竞争问题

        共享竞争问题指:当多个进程共享一个资源,而其中的一个进程对资源进行了操作。此时其他进程访问到的可能是写之前的数据,导致了共享数据不同步的问题。

        具体问题代码

void add(int& num) {for (int i = 0; i < 10000; i++) {num += 1;}
}int main() {int num = 0;thread t1(add, ref(num));thread t2(add, ref(num));if (t1.joinable()) {t1.join();}if (t2.joinable()) {t2.join();}std::cout << "num : " << num << std::endl;return 0;
}

        如果程序按照逻辑,执行结束后num应该是20000,但多次执行,会发现结果每次都不同,而且均在10000-20000之间。

        解释:这就是竞争产生的问题,因为num为t1和t2的共享资源

        解决方法:采用互斥锁。需将共享资源num设置为:如果有线程访问,则其他线程不允许访问的状态。在共享时,进行加锁。在共享结束后,在进行解锁

头文件#include<mutex>
初始化一个锁std::mutex mtx;
加锁mtx.lock()
解锁mtx.unlock()

        结合刚才的错误代码,修正后如下所示:

std::mutex mtx;void add(int& num) {for (int i = 0; i < 10000; i++) {mtx.lock();num += 1;mtx.unlock();}
}int main() {int num = 0;thread t1(add, ref(num));thread t2(add, ref(num));if (t1.joinable()) {t1.join();}if (t2.joinable()) {t2.join();}std::cout << "num : " << num << std::endl;return 0;
}

 3.2 互斥量死锁

        现有2个线程t1t2,还有2个互斥量m1m2t1要先访问m1再访问m2t2要先访问m2再访问m1。(一个互斥量在同一时间,只能被一个占用)

        (占用:对m加了锁,但还没解锁,此时其他线程不可以再对m进行加锁)

        当他们同时访问时,t1先占用了m1的所有权,同时t2占用了m2的所有权,此时t1t2都进行不了第二步,互相卡死,这种情况称为互斥量死锁

        具备互斥量死锁隐患的具体代码如下所示:(线程不安全)

//注意:以下代码只是具备隐患,执行可能成功,也可以卡死mutex m1;
mutex m2;//线程1所执行的函数
void func1() {m1.lock();m2.lock();cout << "Do func1" << endl;m1.unlock();m2.unlock();
}//线程2所执行的函数
void func2() {m2.lock();m1.lock();cout << "Do func2" << endl;m2.unlock();m1.unlock();
}int main() {int num = 0;thread t1(func1);thread t2(func2);if (t1.joinable()) {t1.join();}if (t2.joinable()) {t2.join();}cout << "Over" << endl;return 0;
}

        解决方法:调整加锁和解锁的顺序(要视具体情况而定)

四、lock_guard和unique_lock

4.1 lock_guard

        lock_guard是一个互斥量的模板类,具有以下特征:

std::mutex mtx;//互斥量 
std::lock_guard<std::mutex> lg(mtx);//自动加锁
//作用域结束后,执行析构函数,自动解锁

                1. 构造函数传入一个互斥量,会对其进行自动加锁

                2. 当析构函数被调用时,该互斥量会自动解锁

                3. lock_guard对象不能复制或移动,只能在局部作用域中使用

        针对3.1中的例子,修改后使用lock_guard为:

void add(int &num) {for(int i = 0; i < 10000; i++) {std::lock_guard<std::mutex> lg(mtx);//自动mtx加锁num++;//此处lg作用域结束,mtx自动解锁}
}

        以下是lock_guard的原码:

_EXPORT_STD template <class _Mutex>
class _NODISCARD_LOCK lock_guard { // class with destructor that unlocks a mutex
public:using mutex_type = _Mutex;explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock_MyMutex.lock();}lock_guard(_Mutex& _Mtx, adopt_lock_t) noexcept // strengthened: _MyMutex(_Mtx) {} // construct but don't lock~lock_guard() noexcept {_MyMutex.unlock();}lock_guard(const lock_guard&)            = delete;lock_guard& operator=(const lock_guard&) = delete;private:_Mutex& _MyMutex;
};

4.2 unique_lock

        unique_lock是一个封装了互斥量的类模板,不可以复制。它可以对互斥量进行更多的管理:延迟加锁等。此时就需要在构造函数不能自动加锁,应该使用二参构造函数,之后按需手动加锁

std::unique_lock<std::mutex> lg(mtx, std::defer_lock);//此处没有自动加锁
lg.lock();//手动加锁

        unique_lock常用的函数成员如下所示:

成员函数名称解释
lock()

        对互斥量进行加锁。

如果互斥量已经被其他线程所占有,则阻塞本线程,直到加锁成功

try_lock()

        尝试对互斥量进行加锁。

如果互斥量已经被其他线程所占有,则返回 false,成功加锁返回 true

try_lock_for(

     const std::chrono::

     duration<Rep, Period>&

     interval)

        对互斥量进行加锁。

如果互斥量已经被其他线程所占有,则阻塞本线程,

                                                    直到加锁成功超过指定的时间间隔

                                                                (超过了1分钟30秒)

try_lock_until(

     const std::chrono::

     time_point

        <Clock, Duration>&

     time)

        对互斥量进行加锁。

如果互斥量已经被其他线程所占有,则阻塞本线程,

                                                        直到加锁成功超过指定的时间

                                                                  (超过了12:33:54)

unlock()        对互斥量进行解锁。

        举例说明,假设需要在5秒内加锁,如果超过5秒还没加锁,则直接返回:

timed_mutex m1;void add(int& num) {for (int i = 0; i < 10000; i++) {unique_lock<timed_mutex> lg(m1, std::defer_lock);lg.try_lock_for(std::chrono::seconds(5));num++;}
}

五、单例设计模式

        单例设计模式是确保某个类只能创建一个实例。(比如日志类:Log

由于单例模式是全局唯一的,因此在多线程环境下需考虑线程安全的问题。

        一个单例设计模式的类具有以下特征:

                1. 禁用拷贝构造函数(设置为private权限)

                2. 禁用 = 运算符重载

                3.写一个静态方法获取静态对象

                4. 以此对象调用静态成员函数

5.1 饿汉模式

        饿汉模式指直接在类的内部实例化一个对象,通过静态方法返回这个对象的引用。

        (饿汉急不可耐,类加载后就马上创建了对象)

具体代码

class Log {
public :static Log& getInstance() {static Log log;//饿汉模式return log;}static void printMsg(string message) {cout << message << endl;}private:Log() {}Log& operator=(const Log& log) {}
};

5.2 懒汉模式

        懒汉模式是指在类的内部先声明一个指针,在调用静态方法时再动态申请new出具体的空间,最后返回这个指针指向的空间,也就是对象的引用。

        (懒汉只有需要时候才做事情,只有调用静态函数时才申请空间)

                (懒汉模式由于需要动态申请,所以线程不安全

具体代码

class Log {
public :static Log& getInstance() {static Log *log = nullptr;//懒汉模式if(!log) {log = new Log;}return *log;}static void printMsg(string message) {cout << message << endl;}private:Log() {}Log& operator=(const Log& log) {}
};

 5.3 call_once

        call_once函数的声明如下:

函数声明void call_once(flag, func, Args);
解释

        call_once()保证在多个线程调用一个函数时,同时只能有一个调用成功,其他需要等待

flag是once_flag的一个对象,表示标记函数是否已经被调用过

func是需要被调用的函数

Args为需要被调用的函数参数

        (这里建议直接对线程不安全的代码加锁,以保证线程安全)

六、条件变量

(需要包含头文件#include<condition_variable>

        C++的条件变量condition_variable的使用步骤如下:

                1. 创建一个std::condition_variable对象

                2. 创建一个互斥锁mutex对象,用于保护共享资源

                3. 在需要等待条件变量的地方,使用unique_lock对象锁定互斥锁

                        并调用std::condition_variable::wait()、std::condition_variable::wait_for()

                        或 std::condition_variable::wait_until()等待条件变量          (阻塞)

                4. 在其他线程中需要唤醒 “等待线程” 时,调用std::condition_variable::notify_one()

                        或 std::condition_variable::notify_all()通知等待的线程       (取消阻塞)

注意:wait函数在阻塞时,会自动解锁。)

        结合一个具体的生产者-消费者模型更好理解:(请注意看注释理解)

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>std::condition_variable g_cv;//条件变量
std::queue<int>  q;//任务队列
std::mutex mtx;//互斥锁//生产者
void producer() {for (int i = 0; i < 10; i++) {//总共生产10个任务/* 这里解释一下为什么要增加大括号?*//* unique_lock是作用域结束后自动放开锁的 *//* 如果不加大括号,那个暂停的 1ms 期间也没有释放锁,此时消费者无法获取任务,*//*这样做程序,影响并发性,就是效率低 */{ std::unique_lock<std::mutex> lock(mtx);//为 放任务 加锁/* 在生产者需要唤醒“等待线程”(判断队列是否满)的地方,使用notify_one() */g_cv.notify_one();q.push(i);//放任务std::cout << "生产者放入了一个" << i << std::endl;}//暂停1毫秒,避免生产的太快,消费者还没来得及拿第一个,生产者就已经放完10个任务了std::this_thread::sleep_for(std::chrono::microseconds(1));}
}//消费者
void consumer() {int count = 0;//用于计算完成任务的总数,为10时结束程序while (true) {std::unique_lock<std::mutex> lock(mtx);//为 取任务 加锁/* 在消费者需要等待(判断队列是否为空)的地方,使用wait() */g_cv.wait(lock, []() { //第二个参数是lamba表达式,一元谓词(返回值类型为bool)return !q.empty();});int task = q.front();//取任务q.pop();std::cout << "消费者拿走并完成了一个" << task << std::endl;//当完成任务的总数等于10,则跳出循环if (++count == 10) {break;}}
}int main() {std::thread t_pro(producer);//生产者线程std::thread t_con(consumer);//消费者线程if (t_pro.joinable()) {t_pro.join();}if (t_con.joinable()) {t_con.join();}return 0;
}

七、跨平台线程池

        

这篇关于C++11 Thread线程池、死锁、并发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1141109

相关文章

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

Java子线程无法获取Attributes的解决方法(最新推荐)

《Java子线程无法获取Attributes的解决方法(最新推荐)》在Java多线程编程中,子线程无法直接获取主线程设置的Attributes是一个常见问题,本文探讨了这一问题的原因,并提供了两种解决... 目录一、问题原因二、解决方案1. 直接传递数据2. 使用ThreadLocal(适用于线程独立数据)

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�