什么是信号量
信号量本质上就是一把计数器
。
是一把衡量衡量临界资源中资源多少的计数器。
我们对临界资源进行整体加锁,就默认了,我们对这个资源是整体使用的。但是实际情况可能是,只有一份公共资源,但是允许访问资源不同区域的情况。
申请信号量的本质:对临界资源中特定小块资源进行 预定
的机制(类似于网上预定电影票)。
通过先申请信号量(计数器),再执行的方式,就可以实现在未来某个线程占有一部分临界资源,与其他线程占用同一临界资源的其他部分不冲突。
信号量底层原理
设 sem_t sem = 10,sem_t是计数器的类型,sem = 10表示这块临界资源可以分为10块小资源。
sem–,申请资源,必须保证操作的原子性,我们用 P
代表这个操作。
sem++,归还资源,必须保证操作的原子性,我们用 V
代表这个操作。
信号量核心操作:PV原语。
POSIX信号量
和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
int sem_destroy(sem_t *sem);
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
数组有
num
格空间,当i
指向num
格的位置的时候,%(模)
上num
,就可以回到起始位置。
生产和消费什么时候能访问同一个位置
环形队列的生产消费问题需要 遵守的规则/完成的核心工作
在环形队列当中,大部分情况下,即满足上述规则,单生产和单消费是可以并发执行的。
我们给生产者和消费者各设定一个计数器,就可以很简单的进行多线程间的同步过程。
双信号量运行原理(伪代码)
对生产者而言:
prodocter_sem = 10 //申请信号量,成功则向下运行,失败则阻塞
comsumer_sem = 0;
P(prodocter_sem); //prodocter_sem--
//从事生产活动 -- 把数据放到队列中
V(comsumer_sem); //comsumer_sem++
对消费者而言:
P(comsumer_sem); //comsumer_sem--
//从事消费活动 -- 把数据从队列中拿出
V(prodocter_sem); //prodocter_sem++
当我们的生产信号量减到0时,再申请空间资源就申请不到了,因此就可以满足生产消费的三个条件。
#include
#include
#include
#include
#include
#define NUM 16
class RingQueue{
private:
std::vector<int> q;
int cap;
sem_t data_sem;
sem_t space_sem;
int consume_step;
int product_step;
public:
RingQueue(int _cap = NUM) :q(_cap), cap(_cap)
{
sem_init(&data_sem, 0, 0);
sem_init(&space_sem, 0, cap);
consume_step = 0;
product_step = 0;
}
void PutData(const int& data)
{
sem_wait(&space_sem); // P
q[consume_step] = data;
consume_step++;
consume_step %= cap;
sem_post(&data_sem); //V
}
void GetData(int& data)
{
sem_wait(&data_sem);
data = q[product_step];
product_step++;
product_step %= cap;
sem_post(&space_sem);
}
~RingQueue()
{
sem_destroy(&data_sem);
sem_destroy(&space_sem);
}
};
void* consumer(void* arg)
{
RingQueue* rqp = (RingQueue*)arg;
int data;
for (; ; ) {
rqp->GetData(data);
std::cout << "Consume data done : " << data << std::endl;
sleep(1);
}
}
//more faster
void* producter(void* arg)
{
RingQueue* rqp = (RingQueue*)arg;
srand((unsigned long)time(NULL));
for (; ; ) {
int data = rand() % 1024;
rqp->PutData(data);
std::cout << "Prodoct data done: " << data << std::endl;
// sleep(1);
}
}
int main()
{
RingQueue rq;
pthread_t c, p;
pthread_create(&c, NULL, consumer, (void*)&rq);
pthread_create(&p, NULL, producter, (void*)&rq);
pthread_join(c, NULL);
pthread_join(p, NULL);
}
/*threadpool.h*/
/* 线程池:
*一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着
监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利
用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
* 线程池的应用场景:
* 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技
术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个
Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
* 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
* 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情
况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,
出现错误.
* 线程池的种类:
* 线程池示例:
* 1. 创建固定数量线程池,循环从任务队列中获取任务对象,
* 2. 获取到任务对象后,执行任务对象中的任务接口
*/
/*threadpool.hpp*/
#ifndef __M_TP_H__
#define __M_TP_H__
#include
#include
#include
#define MAX_THREAD 5
typedef bool (*handler_t)(int);
class ThreadTask
{
private:
int _data;
handler_t _handler;
public:
ThreadTask() :_data(-1), _handler(NULL) {}
ThreadTask(int data, handler_t handler) {
_data = data;
_handler = handler;
}
void SetTask(int data, handler_t handler) {
_data = data;
_handler = handler;
}
void Run() {
_handler(_data);
}
};
class ThreadPool
{
private:
int _thread_max;
int _thread_cur;
bool _tp_quit;
std::queue<ThreadTask*> _task_queue;
pthread_mutex_t _lock;
pthread_cond_t _cond;
private:
void LockQueue() {
pthread_mutex_lock(&_lock);
}
void UnLockQueue() {
pthread_mutex_unlock(&_lock);
}
void WakeUpOne() {
pthread_cond_signal(&_cond);
}
void WakeUpAll() {
pthread_cond_broadcast(&_cond);
}
void ThreadQuit() {
_thread_cur--;
UnLockQueue();
pthread_exit(NULL);
}
void ThreadWait() {
if (_tp_quit) {
ThreadQuit();
}
pthread_cond_wait(&_cond, &_lock);
}
bool IsEmpty() {
return _task_queue.empty();
}
static void* thr_start(void* arg) {
ThreadPool* tp = (ThreadPool*)arg;
while (1) {
tp->LockQueue();
while (tp->IsEmpty()) {
tp->ThreadWait();
}
ThreadTask* tt;
tp->PopTask(&tt);
tp->UnLockQueue();
tt->Run();
delete tt;
}
return NULL;
}
public:
ThreadPool(int max = MAX_THREAD) :_thread_max(max), _thread_cur(max),
_tp_quit(false) {
pthread_mutex_init(&_lock, NULL);
pthread_cond_init(&_cond, NULL);
}
~ThreadPool() {
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
bool PoolInit() {
pthread_t tid;
for (int i = 0; i < _thread_max; i++) {
int ret = pthread_create(&tid, NULL, thr_start, this);
if (ret != 0) {
std::cout << "create pool thread error\n";
return false;
}
}
return true;
}
bool PushTask(ThreadTask* tt) {
LockQueue();
if (_tp_quit) {
UnLockQueue();
return false;
}
_task_queue.push(tt);
WakeUpOne();
UnLockQueue();
return true;
}
bool PopTask(ThreadTask** tt) {
*tt = _task_queue.front();
_task_queue.pop();
return true;
}
bool PoolQuit() {
LockQueue();
_tp_quit = true;
UnLockQueue();
while (_thread_cur > 0) {
WakeUpAll();
usleep(1000);
}
return true;
}
};
#endif
/*main.cpp*/
bool handler(int data)
{
srand(time(NULL));
int n = rand() % 5;
printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n);
sleep(n);
return true;
}
int main()
{
int i;
ThreadPool pool;
pool.PoolInit();
for (i = 0; i < 10; i++) {
ThreadTask* tt = new ThreadTask(i, handler);
pool.PushTask(tt);
}
pool.PoolQuit();
return 0;
}
g++ -std=c++0x test.cpp -o test -pthread -lrt
单例模式是一种 “经典的, 常用的, 常考的” 设计模式。
IT行业这么火,涌入的人很多,俗话说林子大了啥鸟都有。大佬和菜鸡们两极分化的越来越严重,为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是 设计模式。
某些类, 只应该具有一个对象(实例), 就称之为单例.
例如一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.
[洗碗的例子]
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度.
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance()
{
return &data;
}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
上述代码存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.
但是后续再次调用, 就没有问题了.
// 懒汉模式, 线程安全
template <typename T>
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance() {
if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst == NULL) {
inst = new T();
}
lock.unlock();
}
return inst;
}
};
注意事项:
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
对于 unique_ptr
, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr
, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
多线4 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!