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

从零开始学习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 temp
    std::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):使用条件变量来同步读者和写者。当有写者访问资源时,所有读者必须等待;当写者完成写操作并释放互斥锁时,读者可以继续读取资源。


http://www.kler.cn/a/315743.html

相关文章:

  • 2024 年 8 个最佳 API 设计工具图文介绍
  • C语言第十一周课——函数的调用
  • 大语言模型在序列推荐中的应用
  • 【GPTs】MJ Prompt Creator:轻松生成创意Midjourney提示词
  • 使用Matlab神经网络工具箱
  • Elasticsearch中什么是倒排索引?
  • 『功能项目』QFrameWorkBug关联Slot(插槽)【67】
  • C++:使用tinyxml2获取节点下元素
  • android kotlin Extension扩展函数
  • HashMap源码
  • 【bug】通过lora方式微调sdxl inpainting踩坑
  • 用uniapp 及socket.io做一个简单聊天 升级 9
  • 【LeetCode】289.生命游戏
  • 模擬器怎麼多開換IP?
  • 【无人机设计与控制】 基于matlab的蚁群算法优化无人机uav巡检
  • Redis面试真题总结(一)
  • 数据库(选择题)
  • 【最快最简单的排序 —— 桶排序算法】
  • 华为HarmonyOS地图服务 11 - 如何在地图上增加点注释?
  • Java 入门基础篇08 - Java的变量与数据类型的认识
  • 在 Python 中使用 JSON
  • 【Linux取经之路】Linux项目自动化构建工具-make/makefile git三板斧
  • 基于web的工作管理系统设计与实现
  • MacOS升级Ruby版本的完整指南
  • Apache subversion 编译流程
  • Delphi 12.2 新增的 WebStencils 尝鲜