Linux 线程池&单例模式&读写锁&自旋锁

                 Linux 线程池&单例模式&读写锁&自旋锁_第1张图片

       历经整整一个月终于到linux系统知识的最后一篇博客了,在这期间博主承认有偷懒几天~这篇博客写完后,接下来就是C++的高阶数据结构了。等博主把网络编程和MySql学好之后再继续写~今天主要介绍线程池、单例模式、读者写者模型、悲观锁和自旋锁的区别。

目录

线程池

为什么要有线程池?

代码测试

thread_pool.hpp

task.hpp

main.cc

Makefile

单例模式

什么是单例模式?

什么是设计模式?

单例模式特点

饿汉实现方式和懒汉实现方式

饿汉模式实现单例

懒汉模式方式单例模式

懒汉模式实现线程池版本代码

signal_pool.hpp

task.hpp

main.cc

读者写者模型

基本理论

函数接口 

pthread_rwlock_init

pthread_rwlock_destroy

pthread_rwlock_wrlock

pthread_rwlock_rdlock

phread_rwlock_unlock

如何理解?伪代码

挂起等待的锁 vs 自旋锁

自旋锁函数接口

pthread_spin_init

pthread_spin_destroy

pthread_spin_lock

pthread_spin_unlock


线程池

为什么要有线程池?

我们先类比一下内存池:

Linux 线程池&单例模式&读写锁&自旋锁_第2张图片

       我们如果频繁向OS申请小块空间,OS就要在底层做很多动作,比如进程身份状态的变化,OS执行内存处理算法。这些对于用户层来说都是没必要的,但确实是耗时间的!所以引入了内存池的概念,趁着OS好的时候,一次申请一大块空间,这块空间在用户层进行管理,这样当我们再次申请空间时,就不需要向OS去要了,直接在用户层拿就好了,这样效率大大提高。

再来看线程池:

Linux 线程池&单例模式&读写锁&自旋锁_第3张图片

       我们发现当多个任务到来时,OS要创建线程,但是临时创建的话效率肯定是比较低的。所以要提前创建好线程,保存在在线程池中。

代码测试

thread_pool.hpp

#pragma once

#include
#include
#include
#include
using namespace std;

namespace ns_threadpool
{
    const int g_num = 3; //线程池中线程的数目

    template
    class ThreadPool
    {
    private:
        int _num; //线程池中线程的数目
        queue _task_queue;//该成员是一个临界资源

        pthread_mutex_t _mtx;
        pthread_cond_t _cond;

    public:
        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
        void Wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }
        void WakeUp()
        {
            pthread_cond_signal(&_cond);
        }
        bool IsEmpty()
        {
            return _task_queue.empty();
        }
    public:
        ThreadPool(int num = g_num)
            :_num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }

        //在类中要让线程执行类内成员方法(参数个数匹配问题,只有一个参数),是不可行的!
        //必须让线程执行静态方法
        static void* Rountine(void* args)
        {
            pthread_detach(pthread_self());//线程分离
            ThreadPool* tp = (ThreadPool*)args;

            while(true)
            {
                tp->Lock();
                while(tp->IsEmpty())
                {
                    //任务队列为空,线程该做些什么呢?
                    tp->Wait();
                }
                //到这里,该任务队列中一定有任务了
                T t;
                tp->PopTask(&t);
                tp->Unlock();

                t();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid;
            for(int i = 0; i < _num; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void*)this);
            }
        }

        void PushTask(const T& in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();
            WakeUp();
        }
        //由于queue大小可以动态增长,这里不考虑满了就等待的的情况

         //这里不能加锁,否则就成死锁了,Rountine也有锁,但是这里也是安全的~
        void PopTask(T* out)
        {
            *out = _task_queue.front();//尾插头出
            _task_queue.pop(); //尾插头出
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }
    };
}

task.hpp

#pragma once
#include 
#include 
#include
#include
namespace ns_task
{
    class Task
    {
    public:
        Task()
        {}
        Task(int x, int y, char op)
            : _x(x), _y(y), _op(op)
        {}
        int Run()
        {
            int res = 0;
            switch (_op)
            {
            case '+':
                res = _x + _y;
                break;
            case '-':
                res = _x - _y;
                break;
            case '*':
                res = _x * _y;
                break;
            case '/':
                res = _x / _y;
                break;
            case '%':
                res = _x % _y;
                break;
            default:
                cout << "bug??" << endl;
                break;
            }
            cout << "当前任务正在被: " << pthread_self() << " 处理: ";
            cout << _x << _op << _y << "=" << res << endl;
            return res;
        }
        int operator()()
        {
            return Run();
        }

    private:
        int _x;
        int _y;
        char _op; //+-*/%
    };
}

main.cc

#include "thread_pool.hpp"
#include "task.hpp"

#include
#include
#include

using namespace ns_task;
using namespace ns_threadpool;

int main()
{
    ThreadPool* tp = new ThreadPool(3);
    tp->InitThreadPool();
    srand((unsigned int)time(0));

    while(true)
    {
        //生产任务
        int x = rand()%20 + 1;
        int y = rand()%10 + 1;
        char op = "+-*/%"[rand()%5];
        Task t(x, y, op);

        //放任务
        tp->PushTask(t);
        sleep(1);
    }
    return 0;
}

Makefile

main:main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f main

测试结果:

Linux 线程池&单例模式&读写锁&自旋锁_第4张图片

这时候我们发现,我们不断往任务队列中塞任务,我们在线程池中创建的多个线程不断去处理。 

单例模式

什么是单例模式?

单例模式是一种 "经典的, 常用的, 常考的" 设计模式。

什么是设计模式?

       IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重.。为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。

单例模式特点

某些类, 只应该具有一个对象(实例), 就称之为单例。
例如一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。

饿汉实现方式和懒汉实现方式

洗完的例子:
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。

总结:懒汉方式最核心的思想是 "延时加载",从而能够优化服务器的启动速度。这和我们之前讲过的内存申请和内存使用及写时拷贝的概念类似。

饿汉模式实现单例

Linux 线程池&单例模式&读写锁&自旋锁_第5张图片

懒汉模式方式单例模式

Linux 线程池&单例模式&读写锁&自旋锁_第6张图片

 通过Singleton 这个包装类来使用T对象, 则一个进程中只有一个T对象的实例。

注意:

存在一个严重的问题, 线程不安全。第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例。

懒汉模式实现线程池版本代码

signal_pool.hpp

#pragma once

#include 
#include 
#include 
#include 
using namespace std;

namespace ns_threadpool
{
    const int g_num = 3; //线程池中线程的数目

    template 
    class ThreadPool
    {
    private:
        int _num;             //线程池中线程的数目
        queue _task_queue; //该成员是一个临界资源

        pthread_mutex_t _mtx;
        pthread_cond_t _cond;

        static ThreadPool* _ins;

    private:
        //构造函数必须得实现,而且设置为私有
        ThreadPool(int num = g_num)
            : _num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }
        //拷贝构造和赋值设置为私有
        ThreadPool(const ThreadPool& tp) = delete;
        ThreadPool operator=(const ThreadPool& tp) = delete;

    public:
        static ThreadPool* GetInstance()
        {
            //静态锁不用手动初始化和销毁
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            //当前单例对象还没有被创建
            if(_ins == nullptr) //双判定,减少锁的争用,提高单例的效率
            {
                pthread_mutex_lock(&lock);
                if(_ins == nullptr)
                {
                    _ins = new ThreadPool();
                    _ins->InitThreadPool(); //创建线程
                    cout << "首次加载对象" << endl;
                }
                pthread_mutex_unlock(&lock);
            }
            return _ins;
        }
        //在类中要让线程执行类内成员方法(参数个数匹配问题,只有一个参数),是不可行的!
        //必须让线程执行静态方法
        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self()); //线程分离
            ThreadPool *tp = (ThreadPool *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpty())
                {
                    //任务队列为空,线程该做些什么呢?
                    tp->Wait();
                }
                //到这里,该任务队列中一定有任务了
                T t;
                tp->PopTask(&t);
                tp->Unlock();

                t();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < _num; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this);
            }
        }

        void PushTask(const T& in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();
            WakeUp();
        }
        //由于queue大小可以动态增长,这里不考虑满了就等待的的情况

        //这里不能加锁,否则就成死锁了,Rountine也有锁,但是这里也是安全的~
        void PopTask(T *out)
        {
            *out = _task_queue.front(); //尾插头出
            _task_queue.pop();          //尾插头出
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }

    public:
        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
        void Wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }
        void WakeUp()
        {
            pthread_cond_signal(&_cond);
        }
        bool IsEmpty()
        {
            return _task_queue.empty();
        }
    };

    template
    ThreadPool* ThreadPool::_ins = nullptr;
}

task.hpp

#pragma once
#include 
#include 
#include
#include
namespace ns_task
{
    class Task
    {
    public:
        Task()
        {}
        Task(int x, int y, char op)
            : _x(x), _y(y), _op(op)
        {}
        int Run()
        {
            int res = 0;
            switch (_op)
            {
            case '+':
                res = _x + _y;
                break;
            case '-':
                res = _x - _y;
                break;
            case '*':
                res = _x * _y;
                break;
            case '/':
                res = _x / _y;
                break;
            case '%':
                res = _x % _y;
                break;
            default:
                cout << "bug??" << endl;
                break;
            }
            cout << "当前任务正在被: " << pthread_self() << " 处理: ";
            cout << _x << _op << _y << "=" << res << endl;
            return res;
        }
        int operator()()
        {
            return Run();
        }

    private:
        int _x;
        int _y;
        char _op; //+-*/%
    };
}

main.cc

#include "thread_pool.hpp"
#include "task.hpp"

#include
#include
#include

using namespace ns_task;
using namespace ns_threadpool;

int main()
{
    cout << "当前正在运行我的进程其他代码..." << endl;
    cout << "当前正在运行我的进程其他代码..." << endl;
    cout << "当前正在运行我的进程其他代码..." << endl;
    cout << "当前正在运行我的进程其他代码..." << endl;
    cout << "当前正在运行我的进程其他代码..." << endl;
    sleep(3);

    while(true)
    {
        //生产任务
        int x = rand()%20 + 1;
        int y = rand()%10 + 1;
        char op = "+-*/%"[rand()%5];
        Task t(x, y, op);
        Task t1(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);
        
        ThreadPool::GetInstance()->PushTask(t);
        
        //单例模式本身会在任何场景,任何环境下使用
        //GetInstance():被多线程重入,进而导致线程安全问题
        //所以要加锁
        cout << ThreadPool::GetInstance() << endl;
        sleep(1);
    }
    return 0;
}

运行结果:

Linux 线程池&单例模式&读写锁&自旋锁_第7张图片

我们发现每次都是一个只有对象,这个对象的线程池里面有多个线程执行任务。

读者写者模型

基本理论

Linux 线程池&单例模式&读写锁&自旋锁_第8张图片

函数接口 

pthread_rwlock_init

Linux 线程池&单例模式&读写锁&自旋锁_第9张图片

pthread_rwlock_t *restrict rwlock:传入定义的pthread_rwlock_t变量的地址 

const pthread_rwlockattr_t *restrict attr:设置属性,我们不关心,传nullptr就好了

pthread_rwlock_destroy

Linux 线程池&单例模式&读写锁&自旋锁_第10张图片

pthread_rwlock_t* rwlock:传入变量的地址,进行释放锁资源 

pthread_rwlock_wrlock

Linux 线程池&单例模式&读写锁&自旋锁_第11张图片

以写方式加锁 

pthread_rwlock_rdlock

以读者身份加锁 

phread_rwlock_unlock

解锁,读者写者以统一方式解锁。

如何理解?伪代码

Linux 线程池&单例模式&读写锁&自旋锁_第12张图片

优先级

读者优先:读者和写者同时到来的时候,我们让读者先进入访问。

写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有的读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入。 

注意:读者多,写着少的问题,是存在"饥饿问题",但是,"饥饿问题"是一个中性词。

挂起等待的锁 vs 自旋锁

我们先找一个现实场景:

Linux 线程池&单例模式&读写锁&自旋锁_第13张图片

       对于临界资源任务处理时间比较短就不适合使用挂起等待的锁,因为可能还没有线程挂起等待,别的线程就释放锁了,这时候线程还得继续挂起然后立刻被唤醒,这是有一定成本的! 

       如果对于处理任务比较长的时候,不适合自旋锁,自旋锁不停地循环检测锁的状态,长时间的话消耗CPU资源也是很大的。

所以针对不同的场景,要选用合适的锁

线程如何得知,自己在临界资源中呆多长时间?

线程不知道!!程序员知道!所以是程序员选择锁的使用!

自旋锁函数接口

pthread_spin_init

Linux 线程池&单例模式&读写锁&自旋锁_第14张图片

pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址

int pshared:是否进程间共享,我们一般设置成0

pthread_spin_destroy

 pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址,释放锁资源

pthread_spin_lock

Linux 线程池&单例模式&读写锁&自旋锁_第15张图片

 pthread_spin_lock:传入定义的自旋锁的变量的地址,进行加锁

pthread_spin_unlock

  pthread_spin_lock:传入定义的自旋锁的变量的地址,进行解锁

我们发现自旋锁和互斥锁的使用几乎一模一样,有了之前的基础,我们就可以使用起来自旋锁。

看到这里,给博主点个赞吧~

                        Linux 线程池&单例模式&读写锁&自旋锁_第16张图片

你可能感兴趣的:(Linux,博客,运维,服务器,linux)