【Linux】线程中的线程池与读写锁

线程池

  1. 线程池的运用场景

多线程程序是为了解决程序运行效率问题

而单线程的代码一定是串行化运行的

线程池不仅要能够提高程序运行效率,还要提高程序处理业务的种类

存在的问题:当业务种类较少的时候可以用switch case/if else列举,业务多的时候就用不了了

  1. 线程池的原理(定义)

线程池=一堆线程+线程安全队列(元素带有任务接口)

队列元素的类型=需要处理的数据+处理数据对应的函数(任务接口)

创建固定数量线程,循环从任务队列中获取任务对象(队列元素=数据+函数)

获取到任务对象后,执行任务对象中的任务接口(函数处理数据)

  1. 线程池的代码实现

线程安全的队列:线程安全=同步+互斥

元素类型:数据、处理数据的函数(任务接口)

队列:STL queue

#include 
#include 
#include 
#include 

/*
 * 定义队列元素的类型
 *    数据
 *    处理数据的方法
 * */

typedef void (*Handler)(int data);

class QueueData{
    public:
        QueueData(){

        }

        QueueData(int data, Handler handler){
            data_ = data;
            handler_ = handler;
        }

        /*
         * 怎么通过函数处理数据
         * */
        void Run(){
            handler_(data_);
        }
    private:
        //要处理的数据
        int data_;
        //要处理数据的函数(要保存一个函数的地址)
        //Handler 函数指针, handler 函数指针变量, 保存函数的地址
        Handler handler_;
};


/*
 * 线程队列
 *   互斥+同步
 *   元素类型
 *   队列
 * */

class SafeQueue{
    public:
        SafeQueue(){
            capacity_ = 1;
            pthread_mutex_init(&lock_, NULL);
            pthread_cond_init(&prod_cond_, NULL);
            pthread_cond_init(&cons_cond_, NULL);
        }

        ~SafeQueue(){
            pthread_mutex_destroy(&lock_);
            pthread_cond_destroy(&prod_cond_);
            pthread_cond_destroy(&cons_cond_);
        }

        void Push(QueueData& data){
            pthread_mutex_lock(&lock_);
            while(que_.size() >= capacity_){
                pthread_cond_wait(&prod_cond_, &lock_);
            }
            que_.push(data);
            pthread_mutex_unlock(&lock_);

            pthread_cond_signal(&cons_cond_);
        }

        /*
         * data : 出参, 返回给调用者的
         * */
        void Pop(QueueData* data, int flag_exit){
            pthread_mutex_lock(&lock_);
            while(que_.empty()){
                if(flag_exit == 1){
                    pthread_mutex_unlock(&lock_);
                    pthread_exit(NULL);
                }
                pthread_cond_wait(&cons_cond_, &lock_);
            }
            *data = que_.front();
            que_.pop();
            pthread_mutex_unlock(&lock_);

            pthread_cond_signal(&prod_cond_);
        }

        void BroadcaseAllConsume(){
            pthread_cond_broadcast(&cons_cond_);
        }
    private:
        std::queue que_;
        size_t capacity_;
        /* 互斥锁 */
        pthread_mutex_t lock_;

        /*
         * 同步 :
         *   生产者的条件变量()
         *   消费者的条件变量(线程池当中的一堆线程, 在逻辑上就是消费线程)
         * */
        pthread_cond_t prod_cond_;
        pthread_cond_t cons_cond_;
};


class ThreadPool{
    public:
        ThreadPool(){
        }

        ~ThreadPool(){
            if(sq_ != NULL){
                delete sq_;
            }
        }

        int InitThreadPool(int thread_count){
            /*
             * 0 : 线程继续运行
             * 1 : 线程退出
             * */
            flag_exit_ = 0;

            sq_ = new SafeQueue;
            if(sq_ == NULL){
                printf("Init thread pool failed\n");
                return -1;
            }

            thread_count_ = thread_count;

            /*
             * thread_count : 10
             * */
            for(int i = 0; i < thread_count_; i++){
                pthread_t tid;
                int ret = pthread_create(&tid, NULL, worker_start, (void*)this);
                if(ret < 0){
                    thread_count--;
                    continue;
                }
            }

            /* 
             * 判断一下线程的数量
             *    thread_count_ <= 0 : 创建工作线程全部失败, 程序就返回负数,告诉给调用者
             *    thread_count_ > 0 : 说明线程池初始化成功了
             * */
            if(thread_count_ <= 0){
                printf("create thread all failed\n");
                return -1;
            }
            //代表初始化成功
            return 0;
        }

        /* 
         * 线程池的使用接口 
         *   只需要给使用者提供push接口,让他能够将数据push到队列当中就好
         *   而 pop接口不需要提供, 因为线程池当中的线程可以自己调用到
         * */
        void Push(QueueData& qd){
            sq_->Push(qd);
        }

        /* 线程入口函数 */
        static void* worker_start(void* arg){
            pthread_detach(pthread_self());
            /*
             * 1. 从队列当中拿元素
             * 2. 处理元素
             * */
            ThreadPool* tp = (ThreadPool*)arg;
            while(1){
                QueueData qd;
                tp->sq_->Pop(&qd, tp->flag_exit_);
                
                qd.Run();
            }
        }

        void thread_pool_exit(){
            flag_exit_ = 1;
            sq_->BroadcaseAllConsume();
        }
    private:
        /* 线程安全的队列 */
        SafeQueue* sq_;
        /* 线程池当中线程的数量 */
        int thread_count_;
        /* 标志线程是否退出的标志位 */
        int flag_exit_;
};



void Deal1(int data){
    printf("i am Deal1, i deal %d\n", data);
}

void Deal2(int data){
    printf("hhhhhh, i am Deal2, deal %d\n", data);
}

int main(){
    /*
     * 1. create thread pool
     *
     * 2. push data to thread pool
     * */

    ThreadPool tp;
    int ret = tp.InitThreadPool(2);
    if(ret < 0){
        return 0;
    }

    for(int i = 0; i < 100; i++){
        QueueData qd(i, Deal1);
        tp.Push(qd);
    }

    tp.thread_pool_exit();

    while(1){
        sleep(1);
    }
    return 0;
}
  1. 线程池当中的线程应该如何退出

主线程退出了,导致线程退出。

优雅的退出方式:1)线程自己退出(收到某一种指令),而不是因为进程退出了被迫销毁

2)队列的元素没有带要处理的

读写锁

  1. 读写锁的模式

以读模式加锁/以写模式加锁(读写锁使用的场景一定是大量读少量写的情况,因为读写锁允许多个线程并行的读,多个线程是互斥写的:读-写、写-写)

多个线程,访问临界资源的时候都是读临界资源的内容,不会产生二义性的结果。

  1. 读写锁的接口

初始化:int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

销毁:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁:

只读模式加锁(读模式可共享):int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

只写模式加锁(相当于互斥锁):int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

解锁:int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

代码示例:

#include 
#include 
#include 

pthread_rwlock_t  rwl;
void* Thread_start1(void* arg){
    pthread_rwlock_wrlock(&rwl);
    printf("线程1 写模式加锁成功...\n");

    sleep(10000);
    return NULL;
}

void* Thread_start2(void* arg){
    sleep(2);

    pthread_rwlock_rdlock(&rwl);
    printf("线程2 读模式加锁成功...\n");


    sleep(10000);
    return NULL;
}

int main(){
    /*
     * 读模式 + 读模式
     * */
    for(int i = 0; i < 1; i++){
        pthread_t  tid;
        pthread_create(&tid, NULL, Thread_start1, NULL);
        pthread_create(&tid, NULL, Thread_start2, NULL);
    }
    pthread_rwlock_init(&rwl, NULL);



    //pthread_rwlock_wrlock(&rwl);
    //printf("第三次写模式加锁成功...\n");
    //
    while(1){
        sleep(1);
    }

    pthread_rwlock_destroy(&rwl);
    return 0;
}

引用计数

引用计数:用来记录当前读写锁有多少个线程以读模式获取了读写锁

  1. 当有线程以读模式进行加锁,加锁成功,则引用计数++

  1. 当以读模式打开读写锁的进程,释放了读写锁之后,引用计数--

  1. 只有当引用计数为0的时候,线程才可以以写的模式打开读写锁

  1. 如果读写锁已经以读模式打开了,有一个线程A想要以写模式打开读写锁,则需要等待,等待期间又来了新的读模式加锁的线程,读模式的线程也要跟着等待

单例模式

  1. 单例类只能有一个实例

  1. 单例类必须自己创建自己的唯一实例

  1. 单例类必须给所有其他对象提供这一实例

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁的创建于销毁。

何时使用:当你想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有了这个单例,有返回,无创建。

关键代码:构造函数是私有的。

单例模式的两种形式(懒汉&饿汉)

饿汉模式:在程序启动的时候就创建唯一实例对象,饿汉模式不需要加锁。

代码模拟:

#include
/*
 * 饿汉模式的单例类
 * */
class sigleton{
    public:
        static sigleton* GetInstance();
        void Print(){
            std::cout << "sigleton print" << std::endl;
        }
    private:
        /* 这个静态的成员变量, 后面指向唯一的实例化对象 */
        static sigleton* st;
        sigleton(){};
};

/* 程序一旦启动, 就会创建全局唯一的实例化对象 */
sigleton* sigleton::st = new sigleton;
sigleton* sigleton::GetInstance(){
    return st;
}

int main(){
    sigleton* st = sigleton::GetInstance();
    st->Print();

    sigleton* st1 = new sigleton;
    sigleton st2;

    return 0;
}

懒汉模式:当你第一次使用是才创建唯一实例对象,从而实现延迟加载效果。懒汉模式在第一次使用单例对象是才能完成初始化工作,因此可能存在多线程竞态环境,如若不加锁会导致重复构造、构造不完全。

代码模拟:

#include 

/*
 * 懒汉模式的单例类
 *    并不是程序启动的时候, 就实例化对象
 *    而是用到的时候, 才进行实例化对象
 * */

class sigleton{
    public:
        static sigleton* GetInstance();
        void Print(){
            std::cout << "sigleton print" << std::endl;
        }
    private:
        sigleton(){};
        static sigleton* st;
        static pthread_mutex_t lock_;
};

pthread_mutex_t sigleton::lock_ = PTHREAD_MUTEX_INITIALIZER;
sigleton* sigleton::st = NULL;
sigleton* sigleton::GetInstance(){
    if(st == NULL){
        pthread_mutex_lock(&sigleton::lock_);
        if(st == NULL){
            st = new sigleton;
        }
        pthread_mutex_unlock(&sigleton::lock_);
    }
    return st;
}

int main(){
    sigleton* st = sigleton::GetInstance();
    st->Print();
    std::cout << st << std::endl;


    sigleton* st1 = sigleton::GetInstance();
    st1->Print();
    std::cout << st1 << std::endl;

    return 0;
}

乐观锁&悲观锁

悲观锁:针对某个线程访问临界区修改数据的时候,都会认为可能有其他线程并行修改的情况发生,所以在线程修改数据之前就进行加锁,让多个线程互斥访问。悲观锁包括:互斥锁、读写锁、自旋锁等等。

乐观锁:针对某个线程访问临界区修改数据的时候,乐观的认为只有该线程在修改,大概率不会存在并行的情况,所以修改数据不加锁,但是在修改完毕进行更新的时候进行判断。

自旋锁(busy-waiting类型)和互斥锁(sleep-waiting类型)的区别

【Linux】线程中的线程池与读写锁_第1张图片

你可能感兴趣的:(Linux,linux,c++,c#,服务器)