历经整整一个月终于到linux系统知识的最后一篇博客了,在这期间博主承认有偷懒几天~这篇博客写完后,接下来就是C++的高阶数据结构了。等博主把网络编程和MySql学好之后再继续写~今天主要介绍线程池、单例模式、读者写者模型、悲观锁和自旋锁的区别。
目录
我们先类比一下内存池:
我们如果频繁向OS申请小块空间,OS就要在底层做很多动作,比如进程身份状态的变化,OS执行内存处理算法。这些对于用户层来说都是没必要的,但确实是耗时间的!所以引入了内存池的概念,趁着OS好的时候,一次申请一大块空间,这块空间在用户层进行管理,这样当我们再次申请空间时,就不需要向OS去要了,直接在用户层拿就好了,这样效率大大提高。
再来看线程池:
我们发现当多个任务到来时,OS要创建线程,但是临时创建的话效率肯定是比较低的。所以要提前创建好线程,保存在在线程池中。
- #pragma once
-
- #include<iostream>
- #include<pthread.h>
- #include<queue>
- #include<string>
- using namespace std;
-
- namespace ns_threadpool
- {
- const int g_num = 3; //线程池中线程的数目
-
- template<class T>
- class ThreadPool
- {
- private:
- int _num; //线程池中线程的数目
- queue<T> _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<T>* tp = (ThreadPool<T>*)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);
- }
- };
- }
- #pragma once
- #include <iostream>
- #include <pthread.h>
- #include<map>
- #include<functional>
- 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; //+-*/%
- };
- }
- #include "thread_pool.hpp"
- #include "task.hpp"
-
- #include<time.h>
- #include<stdlib.h>
- #include<unistd.h>
-
- using namespace ns_task;
- using namespace ns_threadpool;
-
- int main()
- {
- ThreadPool<Task>* tp = new ThreadPool<Task>(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;
- }
- main:main.cc
- g++ -o $@ $^ -std=c++11 -lpthread
- .PHONY:clean
- clean:
- rm -f main
测试结果:
这时候我们发现,我们不断往任务队列中塞任务,我们在线程池中创建的多个线程不断去处理。
单例模式是一种 "经典的, 常用的, 常考的" 设计模式。
IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重.。为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。
某些类, 只应该具有一个对象(实例), 就称之为单例。
例如一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
洗完的例子:
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。
总结:懒汉方式最核心的思想是 "延时加载",从而能够优化服务器的启动速度。这和我们之前讲过的内存申请和内存使用及写时拷贝的概念类似。
通过Singleton 这个包装类来使用T对象, 则一个进程中只有一个T对象的实例。
注意:
存在一个严重的问题, 线程不安全。第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例。
- #pragma once
-
- #include <iostream>
- #include <pthread.h>
- #include <queue>
- #include <string>
- using namespace std;
-
- namespace ns_threadpool
- {
- const int g_num = 3; //线程池中线程的数目
-
- template <class T>
- class ThreadPool
- {
- private:
- int _num; //线程池中线程的数目
- queue<T> _task_queue; //该成员是一个临界资源
-
- pthread_mutex_t _mtx;
- pthread_cond_t _cond;
-
- static ThreadPool<T>* _ins;
-
- private:
- //构造函数必须得实现,而且设置为私有
- ThreadPool(int num = g_num)
- : _num(num)
- {
- pthread_mutex_init(&_mtx, nullptr);
- pthread_cond_init(&_cond, nullptr);
- }
- //拷贝构造和赋值设置为私有
- ThreadPool(const ThreadPool<T>& tp) = delete;
- ThreadPool<T> operator=(const ThreadPool<T>& tp) = delete;
-
- public:
- static ThreadPool<T>* GetInstance()
- {
- //静态锁不用手动初始化和销毁
- static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
- //当前单例对象还没有被创建
- if(_ins == nullptr) //双判定,减少锁的争用,提高单例的效率
- {
- pthread_mutex_lock(&lock);
- if(_ins == nullptr)
- {
- _ins = new ThreadPool<T>();
- _ins->InitThreadPool(); //创建线程
- cout << "首次加载对象" << endl;
- }
- pthread_mutex_unlock(&lock);
- }
- return _ins;
- }
- //在类中要让线程执行类内成员方法(参数个数匹配问题,只有一个参数),是不可行的!
- //必须让线程执行静态方法
- static void *Rountine(void *args)
- {
- pthread_detach(pthread_self()); //线程分离
- ThreadPool<T> *tp = (ThreadPool<T> *)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<class T>
- ThreadPool<T>* ThreadPool<T>::_ins = nullptr;
- }
- #pragma once
- #include <iostream>
- #include <pthread.h>
- #include<map>
- #include<functional>
- 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; //+-*/%
- };
- }
- #include "thread_pool.hpp"
- #include "task.hpp"
-
- #include<time.h>
- #include<stdlib.h>
- #include<unistd.h>
-
- 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<Task>::GetInstance()->PushTask(t);
-
- //单例模式本身会在任何场景,任何环境下使用
- //GetInstance():被多线程重入,进而导致线程安全问题
- //所以要加锁
- cout << ThreadPool<Task>::GetInstance() << endl;
- sleep(1);
- }
- return 0;
- }
运行结果:
我们发现每次都是一个只有对象,这个对象的线程池里面有多个线程执行任务。
pthread_rwlock_t *restrict rwlock:传入定义的pthread_rwlock_t变量的地址
const pthread_rwlockattr_t *restrict attr:设置属性,我们不关心,传nullptr就好了
pthread_rwlock_t* rwlock:传入变量的地址,进行释放锁资源
以写方式加锁
以读者身份加锁
解锁,读者写者以统一方式解锁。
优先级
读者优先:读者和写者同时到来的时候,我们让读者先进入访问。
写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有的读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入。
注意:读者多,写着少的问题,是存在"饥饿问题",但是,"饥饿问题"是一个中性词。
我们先找一个现实场景:
对于临界资源任务处理时间比较短就不适合使用挂起等待的锁,因为可能还没有线程挂起等待,别的线程就释放锁了,这时候线程还得继续挂起然后立刻被唤醒,这是有一定成本的!
如果对于处理任务比较长的时候,不适合自旋锁,自旋锁不停地循环检测锁的状态,长时间的话消耗CPU资源也是很大的。
所以针对不同的场景,要选用合适的锁
线程如何得知,自己在临界资源中呆多长时间?
线程不知道!!程序员知道!所以是程序员选择锁的使用!
pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址
int pshared:是否进程间共享,我们一般设置成0
pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址,释放锁资源
pthread_spin_lock:传入定义的自旋锁的变量的地址,进行加锁
pthread_spin_lock:传入定义的自旋锁的变量的地址,进行解锁
我们发现自旋锁和互斥锁的使用几乎一模一样,有了之前的基础,我们就可以使用起来自旋锁。
看到这里,给博主点个赞吧~