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++ 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 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给