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

相关文章

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

C++中RAII资源获取即初始化

《C++中RAII资源获取即初始化》RAII通过构造/析构自动管理资源生命周期,确保安全释放,本文就来介绍一下C++中的RAII技术及其应用,具有一定的参考价值,感兴趣的可以了解一下... 目录一、核心原理与机制二、标准库中的RAII实现三、自定义RAII类设计原则四、常见应用场景1. 内存管理2. 文件操

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

SQL Server数据库死锁处理超详细攻略

《SQLServer数据库死锁处理超详细攻略》SQLServer作为主流数据库管理系统,在高并发场景下可能面临死锁问题,影响系统性能和稳定性,这篇文章主要给大家介绍了关于SQLServer数据库死... 目录一、引言二、查询 Sqlserver 中造成死锁的 SPID三、用内置函数查询执行信息1. sp_w

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

Java中实现线程的创建和启动的方法

《Java中实现线程的创建和启动的方法》在Java中,实现线程的创建和启动是两个不同但紧密相关的概念,理解为什么要启动线程(调用start()方法)而非直接调用run()方法,是掌握多线程编程的关键,... 目录1. 线程的生命周期2. start() vs run() 的本质区别3. 为什么必须通过 st

C++作用域和标识符查找规则详解

《C++作用域和标识符查找规则详解》在C++中,作用域(Scope)和标识符查找(IdentifierLookup)是理解代码行为的重要概念,本文将详细介绍这些规则,并通过实例来说明它们的工作原理,需... 目录作用域标识符查找规则1. 普通查找(Ordinary Lookup)2. 限定查找(Qualif

Java死锁问题解决方案及示例详解

《Java死锁问题解决方案及示例详解》死锁是指两个或多个线程因争夺资源而相互等待,导致所有线程都无法继续执行的一种状态,本文给大家详细介绍了Java死锁问题解决方案详解及实践样例,需要的朋友可以参考下... 目录1、简述死锁的四个必要条件:2、死锁示例代码3、如何检测死锁?3.1 使用 jstack3.2

Linux实现线程同步的多种方式汇总

《Linux实现线程同步的多种方式汇总》本文详细介绍了Linux下线程同步的多种方法,包括互斥锁、自旋锁、信号量以及它们的使用示例,通过这些同步机制,可以解决线程安全问题,防止资源竞争导致的错误,示例... 目录什么是线程同步?一、互斥锁(单人洗手间规则)适用场景:特点:二、条件变量(咖啡厅取餐系统)工作流