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

相关文章

Spring Boot3虚拟线程的使用步骤详解

《SpringBoot3虚拟线程的使用步骤详解》虚拟线程是Java19中引入的一个新特性,旨在通过简化线程管理来提升应用程序的并发性能,:本文主要介绍SpringBoot3虚拟线程的使用步骤,... 目录问题根源分析解决方案验证验证实验实验1:未启用keep-alive实验2:启用keep-alive扩展建

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa

Python异步编程中asyncio.gather的并发控制详解

《Python异步编程中asyncio.gather的并发控制详解》在Python异步编程生态中,asyncio.gather是并发任务调度的核心工具,本文将通过实际场景和代码示例,展示如何结合信号量... 目录一、asyncio.gather的原始行为解析二、信号量控制法:给并发装上"节流阀"三、进阶控制

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++从序列容器中删除元素的四种方法

《C++从序列容器中删除元素的四种方法》删除元素的方法在序列容器和关联容器之间是非常不同的,在序列容器中,vector和string是最常用的,但这里也会介绍deque和list以供全面了解,尽管在一... 目录一、简介二、移除给定位置的元素三、移除与某个值相等的元素3.1、序列容器vector、deque

C++常见容器获取头元素的方法大全

《C++常见容器获取头元素的方法大全》在C++编程中,容器是存储和管理数据集合的重要工具,不同的容器提供了不同的接口来访问和操作其中的元素,获取容器的头元素(即第一个元素)是常见的操作之一,本文将详细... 目录一、std::vector二、std::list三、std::deque四、std::forwa

Redis中高并发读写性能的深度解析与优化

《Redis中高并发读写性能的深度解析与优化》Redis作为一款高性能的内存数据库,广泛应用于缓存、消息队列、实时统计等场景,本文将深入探讨Redis的读写并发能力,感兴趣的小伙伴可以了解下... 目录引言一、Redis 并发能力概述1.1 Redis 的读写性能1.2 影响 Redis 并发能力的因素二、

C++字符串提取和分割的多种方法

《C++字符串提取和分割的多种方法》在C++编程中,字符串处理是一个常见的任务,尤其是在需要从字符串中提取特定数据时,本文将详细探讨如何使用C++标准库中的工具来提取和分割字符串,并分析不同方法的适用... 目录1. 字符串提取的基本方法1.1 使用 std::istringstream 和 >> 操作符示

C++原地删除有序数组重复项的N种方法

《C++原地删除有序数组重复项的N种方法》给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度,不要使用额外的数组空间,你必须在原地修改输入数组并在使用O(... 目录一、问题二、问题分析三、算法实现四、问题变体:最多保留两次五、分析和代码实现5.1、问题分析5.

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑