【Linux】线程池和线程补充内容

发布于:2025-05-02 ⋅ 阅读:(8) ⋅ 点赞:(0)

在这里插入图片描述

个人主页~


一、线程池简介

池化技术我们并不陌生,我们在前面的文章中实现过进程池,这里线程池的作用也是先申请资源交给用户区,然后用户在使用的时候就不用再去内核申请了,直接去池中申请,效率提高,是一种以空间换时间的方法

单例模式线程池简介

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例,核心思想是限制一个类只能创建一个对象,并提供一个统一的方法让其他代码可以访问这个唯一的对象,这样可以避免在系统中创建多个功能相同的对象,从而节省系统资源,保证数据的一致性和操作的一致性

单例的实现有两种方式,被称为饿汉方式和懒汉方式,饿汉方式的核心思想是在类加载时就创建单例实例,无论后续是否会使用该实例,这种方式利用了静态成员变量的特性,在程序启动时,类的静态成员变量会被自动初始化,从而保证实例的唯一性,懒汉方式的核心思想是在第一次使用单例实例时才进行创建,即 “延迟加载”,这种方式避免了在程序启动时就创建实例,从而减少了不必要的资源消耗

二、单例模式线程池的实现

1、ThreadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 5;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:
    static void *HandlerTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();
            std::cout << name << " run, "
                      << "result: " << t.GetResult() << std::endl;
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr,
            						 HandlerTask, this);
        }
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();//唤醒线程
        Unlock();
    }
    //实现单例模式,确保线程池只有一个实例
    static ThreadPool<T> *GetInstance()
    {
    	//这里套两层if判断是为了避免多个线程同时通过第一次检查而创建多个实例
    	//只有在第一次进入的时候tp_有可能等于nullptr,之后就不可能会了,在外面再加一层
    	//可以判断完直接跳过括号中的代码,不去争夺锁
        if (nullptr == tp_)
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
	//单例模式要把构造函数私有化,不被类外访问到
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    //删除拷贝构造函数,防止线程池对象被拷贝
    ThreadPool(const ThreadPool<T> &) = delete;
    //删除赋值运算符,防止线程池对象被赋值
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; 
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;//存储任务的队列

    pthread_mutex_t mutex_;//用于保护任务队列的互斥锁
    pthread_cond_t cond_;//用于线程同步的条件变量

    static ThreadPool<T> *tp_;//静态指针,用于实现单例模式
    static pthread_mutex_t lock_;//静态互斥锁,用于在创建单例时进行线程同步
};

//静态成员变量初始化
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

2、Task.hpp

#pragma once
#include <iostream>
#include <string>

std::string opers="+-*/%";

enum{
    DivZero=1,
    ModZero,
    Unknown
};

class Task
{
public:
    Task()
    {}
    Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0)
    {}
    void run()
    {
        switch (oper_)
        {
        case '+':
            result_ = data1_ + data2_;
            break;
        case '-':
            result_ = data1_ - data2_;
            break;
        case '*':
            result_ = data1_ * data2_;
            break;
        case '/':
            {
                if(data2_ == 0) exitcode_ = DivZero;
                else result_ = data1_ / data2_;
            }
            break;
        case '%':
           {
                if(data2_ == 0) exitcode_ = ModZero;
                else result_ = data1_ % data2_;
            }            break;
        default:
            exitcode_ = Unknown;
            break;
        }
    }
    void operator ()()
    {
        run();
    }
    std::string GetResult()
    {
        std::string r = std::to_string(data1_);
        r += oper_;
        r += std::to_string(data2_);
        r += "=";
        r += std::to_string(result_);
        r += "[code: ";
        r += std::to_string(exitcode_);
        r += "]";

        return r;
    }
    std::string GetTask()
    {
        std::string r = std::to_string(data1_);
        r += oper_;
        r += std::to_string(data2_);
        r += "=?";
        return r;
    }
    ~Task()
    {}

private:
    int data1_;
    int data2_;
    char oper_;

    int result_;
    int exitcode_;
};

3、main.cpp

#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"
#include "Task.hpp"

int main()
{
    
    std::cout << "process running..." << std::endl;
    ThreadPool<Task>::GetInstance()->Start();
    srand(time(nullptr));

    while(true)
    {
        //构建任务
        int x = rand() % 10 + 1;
        usleep(10);
        int y = rand() % 5;
        char op = opers[rand()%opers.size()];

        Task t(x, y, op);
        ThreadPool<Task>::GetInstance()->Push(t);
        //交给线程池处理
        std::cout << "main thread make task: " << t.GetTask() << std::endl;

        sleep(1);
    }
}

在这里插入图片描述

三、其他常见锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁,当其它线程想要访问数据时,被阻塞挂起,互斥锁就是悲观锁

  • 乐观锁: 每次取数据的时候,总是乐观的认为数据不会被其他线程修改,因此不上锁,但是在更新数据前,会判断其他线程在更新前有没有对数据进行修改,主要采用两种方式:版本号机制和 CAS 操作

  • CAS 操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等,如果相等则用新值更新,若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试

  • 自旋锁:当一个进程申请锁失败时,不是将自己挂起,而是继续去申请锁,使用这种锁的前提是,线程在临界区中执行的时间要足够的短

读写锁

读写锁是一种同步机制,用于在多线程环境中对共享资源进行并发访问控制,它允许多个线程同时进行读操作,但在进行写操作时会独占资源,以保证数据的一致性和完整性

  • 基本概念:读写锁将对共享资源的访问分为读操作和写操作两种类型,多个线程可以同时获取读锁,并行地进行读操作,因为读操作不会修改共享资源,不会产生数据竞争问题,而写操作是独占的,当一个线程获取写锁时,其他线程无论是读操作还是写操作都必须等待,直到写锁被释放

  • 读写锁的三种状态

    • 无锁状态:此时没有线程持有读锁或写锁,任何线程都可以尝试获取读锁或写锁
    • 读锁状态:有一个或多个线程持有读锁,此时可以有其他线程继续获取读锁,但不能有线程获取写锁
    • 写锁状态:有一个线程持有写锁,此时其他线程不能获取读锁或写锁,直到写锁被释放
  • 优点

    • 并发性能高:允许多个线程同时进行读操作,提高了对共享资源的并发访问能力,特别适用于读多写少的场景
    • 数据一致性:写操作是独占的,保证了在写操作期间不会有其他线程同时访问共享资源,从而确保了数据的一致性
  • 缺点

    • 实现复杂:读写锁的实现比普通的互斥锁更复杂,需要处理读锁和写锁的竞争关系
    • 写饥饿问题:在高并发的读操作场景下,可能会出现写线程长时间无法获取写锁的情况,即写饥饿问题
  • 使用场景

    • 适用于读操作频繁、写操作较少的场景,例如:
    • 缓存系统:缓存系统通常需要频繁读取数据,而更新数据的操作相对较少,使用读写锁可以让多个线程同时读取缓存,提高缓存的访问性能
    • 配置文件管理:配置文件在程序运行过程中通常只需要读取,而修改配置文件的操作比较少,使用读写锁可以让多个线程同时读取配置文件,而在修改配置文件时进行独占访问

今日分享就到这了~

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到