当前位置: 首页 > news >正文

从零开始学习Linux(14)---线程池

1.线程池

        线程池是一种多线程编程技术,它提供了一个线程队列,用于存储和管理可重用的线程。当需要执行任务时,线程池会从队列中取出一个空闲线程来执行任务,而不是创建一个新的线程。任务完成后,线程被放回线程池中,等待下一次任务的分配。

以下是线程池的一些关键特性:

  1. 线程复用:线程池中的线程可以被重复使用,而不是每次需要执行任务时都创建新的线程。这可以显著减少线程创建和销毁的开销。

  2. 任务分配:任务可以被分配给线程池中的空闲线程,这样可以提高系统性能,因为线程的创建和销毁是耗时的。

  3. 线程管理:线程池提供了一种机制来控制线程的数量,例如限制线程池中同时运行的线程数量。这有助于防止系统资源过度消耗。

  4. 任务队列:线程池通常包含一个任务队列,用于存储需要执行的任务。当线程完成一个任务后,它会从队列中取出下一个任务。

  5. 线程生命周期:线程池可以管理线程的生命周期,包括创建、运行和销毁。这有助于简化线程管理,并提高程序的稳定性。

1.可重入

        重入:重入(Reentrancy)是操作系统和编程语言中的一个概念,它指的是一个函数或方法可以被多次调用,而不会因为这些调用而产生任何错误或异常。在多线程环境中,重入性尤其重要,因为它允许同一个线程多次进入同一个函数或方法,而不会导致死锁或其他同步问题。

以下是一些常见的不可重入情况:

  1. 递归调用

    • 递归调用是指一个函数或方法在其内部调用了自身。在递归过程中,如果函数或方法没有正确处理锁的释放和获取,可能会导致死锁。
    • 例如,一个递归函数尝试在递归过程中获取同一个锁,但没有在递归结束时释放锁,这将导致死锁。
  2. 全局锁

    • 如果一个锁被全局地用于保护多个资源,而没有考虑资源的局部性,那么这个锁可能会变得不可重入。
    • 例如,一个全局锁被用来保护一个共享资源和一个私有资源,如果一个线程在访问私有资源时持有全局锁,那么其他线程在访问共享资源时也将无法获取锁,从而导致死锁。
  3. 资源依赖

    • 如果一个函数或方法在执行过程中依赖外部资源,而这些资源在同一时间内只能被一个线程访问,那么这个函数或方法可能会变得不可重入。
    • 例如,一个函数使用了一个外部锁来访问一个资源,如果该函数在执行过程中被另一个线程调用,而该线程也试图访问相同的资源,可能会导致死锁。
  4. 锁的递归性

    • 如果一个锁的实现不支持递归调用,那么任何尝试递归获取该锁的函数或方法都会导致死锁。
    • 例如,一个互斥锁的实现不支持递归,如果一个函数在持有该锁时再次尝试获取它,将会导致死锁。

2.线程安全

        线程安全性:线程安全是指程序在多线程环境中能够正确执行,即使多个线程同时访问和修改共享资源,也不会导致数据不一致或错误的结果。线程安全是多线程编程中的一个关键概念,它要求程序在多个线程同时运行时能够保持一致性和正确性。

以下是一些常见的线程不安全情况:

  1. 竞态条件(Race Condition)

    • 当两个或多个线程同时访问和修改同一变量时,如果没有适当的同步机制,可能会导致竞态条件。
    • 例如,两个线程同时读取一个变量,然后其中一个线程修改了该变量,而另一个线程仍在使用旧值。
  2. 数据不一致

    • 当多个线程同时修改共享数据时,如果没有同步机制,可能会导致数据不一致。
    • 例如,线程A读取一个变量,然后线程B修改了该变量,但线程A没有看到B的修改,因此继续使用旧值。
  3. 资源竞争

    • 当多个线程同时访问或修改同一资源时,可能会导致资源竞争。
    • 例如,多个线程同时尝试打开同一文件,如果没有适当的同步机制,可能会导致文件操作失败或数据损坏。
  4. 死锁(Deadlock)

    • 当两个或多个线程同时持有对方需要的资源,并等待对方释放资源时,可能会导致死锁。
    • 例如,线程A持有资源1并等待资源2,而线程B持有资源2并等待资源1。
  5. 内存泄漏

    • 当线程在分配内存后没有正确释放时,可能会导致内存泄漏。
    • 例如,一个线程分配了内存,但另一个线程使用了该内存,而第一个线程没有释放它。
  6. 条件竞争

    • 当线程之间通过条件变量进行同步时,如果条件变量被错误地使用,可能会导致条件竞争。
    • 例如,线程A和线程B都检查了一个条件,然后线程A修改了该条件,但线程B没有看到修改,因此继续等待。

3.死锁

        死锁(Deadlock)是操作系统和多线程编程中的一个重要概念,指的是两个或多个线程在等待对方持有的资源时,导致所有线程都无法继续执行的状态。死锁通常是由于线程间的资源竞争和同步机制的不当使用所引起的。

死锁的四个必要条件是:

  1. 互斥条件:至少有一个资源是互斥的,即同一时间只能被一个线程使用。

  2. 持有和等待条件:线程持有至少一个资源,并且等待获取其他线程所持有的资源。

  3. 非抢占条件:线程持有的资源不能被其他线程抢占。

  4. 循环等待条件:存在一个线程-资源对的循环等待链,即每个线程都在等待下一个线程所持有的资源。

        当这四个条件同时满足时,就可能发生死锁。例如,假设线程A持有资源1并等待资源2,线程B持有资源2并等待资源1,这时线程A和线程B都会被阻塞,因为它们都在等待对方释放资源,而对方又不会释放资源,从而形成了一个循环等待链。

以下是一些避免死锁的策略和最佳实践:

  1. 避免循环等待

    • 确保每个线程请求资源的顺序是固定的,这样就可以避免形成循环等待链。
    • 例如,如果一个线程必须请求多个资源,它应该按照一个固定的顺序请求这些资源,并且一旦开始请求资源,就不改变这个顺序。
  2. 避免资源独占

    • 尽量使资源成为可共享的,或者在尽可能短的时间内释放资源。
    • 如果资源必须独占使用,确保线程在释放资源之前不会再次请求其他资源。
  3. 避免长时间持有锁

    • 尽量减少线程持有锁的时间,尤其是全局锁。
    • 如果一个线程必须持有多个锁,确保它尽快释放不再需要的锁。
  4. 使用锁的兼容性

    • 确保锁的兼容性,即一个线程持有的锁不会阻止其他线程获取其他锁。
    • 例如,如果线程A持有锁A并请求锁B,而线程B持有锁B并请求锁A,这可能会导致死锁。
  5. 使用死锁检测和恢复

    • 一些操作系统和编程语言提供了死锁检测和恢复机制。
    • 这些机制可以在检测到死锁时自动尝试恢复,例如通过撤销某个线程的锁或者重新启动线程。
  6. 使用锁的互斥性

    • 确保锁是互斥的,即同一时间只能被一个线程持有。
    • 如果锁是可重入的,确保在递归调用时正确地处理锁的释放和获取。
  7. 使用最小粒度锁

    • 尽量使用最小的锁来保护共享资源,避免使用全局锁或大范围的锁。
    • 这样可以减少锁的竞争,并降低发生死锁的可能性。

4.单例模式

        单例模式(Singleton Pattern)是一种常用的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式适用于那些需要全局访问、只有一个实例、或者创建实例很昂贵的类。

单例模式有多种实现方式,以下是一些常见的方法:

  1. 饿汉式(Eager Initialization)

    • 类加载时就立即创建实例,并将其存储在一个私有静态变量中。
    • 优点:线程安全,没有延迟加载。
    • 缺点:实例在类加载时就创建,即使不需要也占用了资源。
  2. 懒汉式(Lazy Initialization)

    • 类加载时不创建实例,而是在第一次调用 getInstance 方法时创建。
    • 优点:延迟加载,只有在需要时才创建实例。
    • 缺点:线程不安全,需要额外的同步机制来保证只有一个线程可以创建实例。
  3. 双重校验锁(Double-Checked Locking)

    • 结合了懒汉式和同步锁的优点,通过双重检查加锁来保证线程安全。
    • 优点:线程安全,延迟加载。
    • 缺点:实现较为复杂,容易出错。
  4. 静态内部类(Static Inner Class)

    • 使用静态内部类来创建实例,类加载时不会立即创建,而是在第一次调用 getInstance 方法时创建。
    • 优点:线程安全,延迟加载。
    • 缺点:实现较为复杂,容易出错。

下面是用单例模式实现的线程池:

#pragma once#include <iostream>
#include <vector>
#include <queue>
#include <pthread.h>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"using namespace ThreadModule;const static int gdefaultthreadnum = 10;// 日志
template <typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}void ThreadWakeup(){pthread_cond_signal(&_cond);}void ThreadWakeupAll(){pthread_cond_broadcast(&_cond);}// 是要有的,必须是私有的ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum), _waitnum(0), _isrunning(false){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);LOG(INFO, "ThreadPool Construct()");}void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){std::string name = "thread-" + std::to_string(num + 1);_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1), name);LOG(INFO, "init thread %s done", name.c_str());}_isrunning = true;}void Start(){for (auto &thread : _threads){thread.Start();}}void HandlerTask(std::string name) // 类的成员方法,也可以成为另一个类的回调方法,方便我们继续类级别的互相调用!{LOG(INFO, "%s is running...", name.c_str());while (true){// 1. 保证队列安全LockQueue();// 2. 队列中不一定有数据while (_task_queue.empty() && _isrunning){_waitnum++;ThreadSleep();_waitnum--;}// 2.1 如果线程池已经退出了 && 任务队列是空的if (_task_queue.empty() && !_isrunning){UnlockQueue();break;}// 2.2 如果线程池不退出 && 任务队列不是空的// 2.3 如果线程池已经退出 && 任务队列不是空的 --- 处理完所有的任务,然后在退出// 3. 一定有任务, 处理任务T t = _task_queue.front();_task_queue.pop();UnlockQueue();LOG(DEBUG, "%s get a task", name.c_str());// 4. 处理任务,这个任务属于线程独占的任务t();LOG(DEBUG, "%s handler a task, result is: %s", name.c_str(), t.ResultToString().c_str());}}// 复制拷贝禁用ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;ThreadPool(const ThreadPool<T> &) = delete;public:static ThreadPool<T> *GetInstance(){// 如果是多线程获取线程池对象下面的代码就有问题了!!// 只有第一次会创建对象,后续都是获取// 双判断的方式,可以有效减少获取单例的加锁成本,而且保证线程安全if (nullptr == _instance) // 保证第二次之后,所有线程,不用在加锁,直接返回_instance单例对象{LockGuard lockguard(&_lock);if (nullptr == _instance){_instance = new ThreadPool<T>();_instance->InitThreadPool();_instance->Start();LOG(DEBUG, "创建线程池单例");return _instance;}}LOG(DEBUG, "获取线程池单例");return _instance;}void Stop(){LockQueue();_isrunning = false;ThreadWakeupAll();UnlockQueue();}void Wait(){for (auto &thread : _threads){thread.Join();LOG(INFO, "%s is quit...", thread.name().c_str());}}bool Enqueue(const T &t){bool ret = false;LockQueue();if (_isrunning){_task_queue.push(t);if (_waitnum > 0){ThreadWakeup();}LOG(DEBUG, "enqueue task success");ret = true;}UnlockQueue();return ret;}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:int _threadnum;std::vector<Thread> _threads; // for fix, int tempstd::queue<T> _task_queue;pthread_mutex_t _mutex;pthread_cond_t _cond;int _waitnum;bool _isrunning;// 添加单例模式static ThreadPool<T> *_instance;static pthread_mutex_t _lock;
};template <typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;template <typename T>
pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;

5.自旋锁

        自旋锁(Spinlock)是一种简单的锁机制,它不将阻塞的线程挂起,而是让线程反复检查锁是否可用。如果锁可用,线程就获取锁并继续执行;如果锁不可用,线程就重复检查,直到锁可用为止。自旋锁适用于处理器速度远大于锁的竞争频率的情况,因为线程的大部分时间都在等待锁而不是实际处理数据。

以下是自旋锁的一些关键特性:

  1. 非阻塞性:自旋锁不将等待锁的线程挂起,而是让线程在循环中等待。

  2. 轻量级:自旋锁不涉及线程的上下文切换,因此开销较小。

  3. 竞争性:自旋锁适用于锁的竞争不频繁的情况,因为线程大部分时间都在循环中浪费。

  4. CPU消耗:自旋锁可能会导致CPU消耗增加,因为线程在循环中会浪费CPU时间。

6.读者写者问题

        读者写者问题(Reader-Writer Problem)是一个经典的并发控制问题,它涉及到如何保护共享资源以避免读者和写者之间的冲突。在读者写者问题中,有多个读者可以同时读取共享资源,但写者必须独占访问资源。问题在于,如果读者和写者同时访问资源,可能会导致数据不一致或损坏。

        为了解决读者写者问题,通常需要使用互斥锁(Mutex)和条件变量(Condition Variable)来同步对资源的访问。以下是一个简单的解决方案,使用这两个同步机制来保护共享资源:

  1. 互斥锁(Mutex):使用互斥锁来保护共享资源,确保同一时间只有一个线程可以访问资源。

  2. 条件变量(Condition Variable):使用条件变量来同步读者和写者。当有写者访问资源时,所有读者必须等待;当写者完成写操作并释放互斥锁时,读者可以继续读取资源。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • python有main函数吗
  • 后端-navicat查找语句(单表与多表)
  • NLP 文本匹配任务核心梳理
  • 基于51单片机的汽车倒车防撞报警器系统
  • 【SpinalHDL】Scala编程之伴生对象
  • 《C++移动语义:解锁复杂数据结构的高效之道》
  • 『功能项目』QFrameWork更新道具图片UGUI【71】
  • 哈希简单介绍
  • 连续数组问题
  • CSS3 多媒体查询
  • 网关过滤器(Gateway Filter)
  • 【webpack4系列】设计可维护的webpack4.x+vue构建配置(终极篇)
  • 41. 如何在MyBatis-Plus中实现批量操作?批量插入和更新的最佳实践是什么?
  • 解决DockerDesktop启动redis后采用PowerShell终端操作
  • C++初阶-list用法总结
  • @jsonView过滤属性
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • ES6之路之模块详解
  • gitlab-ci配置详解(一)
  • iOS 颜色设置看我就够了
  • JSONP原理
  • JWT究竟是什么呢?
  • PHP 7 修改了什么呢 -- 2
  • PyCharm搭建GO开发环境(GO语言学习第1课)
  • Rancher-k8s加速安装文档
  • REST架构的思考
  • Transformer-XL: Unleashing the Potential of Attention Models
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • 手写双向链表LinkedList的几个常用功能
  • 思否第一天
  • 想使用 MongoDB ,你应该了解这8个方面!
  • elasticsearch-head插件安装
  • 昨天1024程序员节,我故意写了个死循环~
  • # windows 安装 mysql 显示 no packages found 解决方法
  • #WEB前端(HTML属性)
  • #前后端分离# 头条发布系统
  • (1)(1.19) TeraRanger One/EVO测距仪
  • (14)学习笔记:动手深度学习(Pytorch神经网络基础)
  • (2024)docker-compose实战 (8)部署LAMP项目(最终版)
  • (C语言版)链表(三)——实现双向链表创建、删除、插入、释放内存等简单操作...
  • (k8s中)docker netty OOM问题记录
  • (ZT)薛涌:谈贫说富
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (笔试题)合法字符串
  • (蓝桥杯每日一题)平方末尾及补充(常用的字符串函数功能)
  • (三)SvelteKit教程:layout 文件
  • (原创)boost.property_tree解析xml的帮助类以及中文解析问题的解决
  • (转)甲方乙方——赵民谈找工作
  • .net core 控制台应用程序读取配置文件app.config
  • .NET Core 项目指定SDK版本
  • .NET Core工程编译事件$(TargetDir)变量为空引发的思考
  • .NET程序员迈向卓越的必由之路
  • /dev/sda2 is mounted; will not make a filesystem here!
  • /proc/stat文件详解(翻译)
  • [000-01-008].第05节:OpenFeign特性-重试机制