文章目录
- 一、POSIX信号量
- 1.1 相关概念和接口
- 1.2 基于环形队列的生产者消费者模型
- 二、线程池
- 2.1 概念
- 2.2 代码实现
- 三、线程安全的单例模式
- 3.1 概念
- 3.2 懒汉模式的线程池
- 四、读写者问题
- 4.1 概念
- 4.2 相关接口
- 五、自旋锁
- 5.1 自旋锁VS悲观锁
- 5.2 常用接口
一、POSIX信号量
1.1 相关概念和接口
信号量本质是一个计数器,用于描述临界资源的大小。将临界资源分成不同份,让多个线程实现并发执行,这也是多线程预定资源的手段。
每个线程想要访问临界资源,都先得申请到信号量资源,只要申请到,都一定会有属于该线程的一份资源。
初始化信号量
#include <semaphore.h>
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()
1.2 基于环形队列的生产者消费者模型
环形队列在数据结构里面已经涉及到了。由于环形队列(n个空间)的状态有n+1种无法用n个空间表示,一般有两种解决办法:
- 使用一个额外变量充当计数器,表示当前的状态
- 少使用一个空间,这样就只有n种状态了
而我们的信号量就可以充当这样的计数器。
ring_cp.cc
#include "RingQueue.hpp"
using namespace ns_ring_queue;
#include <unistd.h>
#include <time.h>
void* producter(void* args)
{
RingQueue<int>* rq = (RingQueue<int>*)args;
while (true)
{
int t = rand()%10 + 1;
rq->Push(t);
std::cout << "producter send task: " << t << std::endl;
}
}
void* consumer(void* args)
{
RingQueue<int>* rq = (RingQueue<int>*)args;
while (true)
{
int c = 0;
rq->Pop(&c);
std::cout << "线程开始消费" << c << std::endl;
sleep(1);
}
}
int main()
{
srand((unsigned long)time(nullptr));
const int N = 5;
pthread_t master;
pthread_t worker[N];
RingQueue<int> *rq = new RingQueue<int>;
pthread_create(&master, nullptr, producter, (void*)rq);
for (int i = 0; i < N; i ++ )
pthread_create(&worker[i], nullptr, consumer, (void*)rq);
pthread_join(master,nullptr);
for (int i = 0; i < N; i ++ )
pthread_join(worker[i], nullptr);
return 0;
}
RingQueue.hpp
#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
namespace ns_ring_queue
{
const int g_default_cap = 5;
template <class T>
class RingQueue
{
private:
std::vector<T> ring_queue_;
int cap_; // 环形队列的大小
sem_t blank_sem_; // 生产者只关心空位置的数量
sem_t data_sem_; // 消费者只关心数据的数量
int c_step_; // 消费者当前消费的位置
int p_step_; // 生产者生产存放的下标
// 互斥量,用于同类型直接的互斥加锁
pthread_mutex_t c_mtx_;
pthread_mutex_t p_mtx_;
public:
RingQueue(int cap = g_default_cap) : cap_(cap), ring_queue_(cap)
{
sem_init(&blank_sem_, 0, cap);
sem_init(&data_sem_, 0, 0);
c_step_ = p_step_ = 0;
pthread_mutex_init(&p_mtx_, nullptr);
pthread_mutex_init(&c_mtx_, nullptr);
}
~RingQueue()
{
pthread_mutex_destroy(&p_mtx_);
pthread_mutex_destroy(&c_mtx_);
sem_destroy(&blank_sem_);
sem_destroy(&data_sem_);
}
public:
void Push(const T &in)
{
// 等待信号量,信号量减1
sem_wait(&blank_sem_); // P()
// 同类互斥,加锁
pthread_mutex_lock(&p_mtx_);
// 空位置生产
ring_queue_[p_step_] = in;
p_step_++;
p_step_ %= cap_;
pthread_mutex_unlock(&p_mtx_);
// 发布信号量,信号量加1
sem_post(&data_sem_); // V()
}
void Pop(T *out)
{
// 等待信号量,信号量减1
sem_wait(&data_sem_); // P()
// 同类互斥,加锁
pthread_mutex_lock(&c_mtx_);
// 空位置生产
*out = ring_queue_[c_step_];
c_step_++;
c_step_ %= cap_;
pthread_mutex_unlock(&c_mtx_);
// 发布信号量,信号量加1
sem_post(&blank_sem_); // V()
}
};
}
二、线程池
2.1 概念
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
总的来说为了提高效率,提前准备好线程(等到用的时候再申请比较慢,类比内存池),用来随时处理任务。
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
线程池的种类:
- 创建固定数量线程池,循环从任务队列中获取任务对象,
- 获取到任务对象后,执行任务对象中的任务接口
2.2 代码实现
main.cc
#include "thread_pool.hpp"
using namespace ns_threadpool;
int main()
{
ThreadPool<int> *tp = new ThreadPool<int>;
while (true)
{
sleep(1);
int t = rand()%10 + 1;
std::cout << "send task: " << t << std::endl;
tp->PushTask(t);
}
}
thread_pool.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
namespace ns_threadpool
{
const int g_num = 5;
template <class T>
class ThreadPool
{
private:
std::queue<T> task_queue_;
int num_; // 线程池线程的数量
pthread_mutex_t mtx_;
pthread_cond_t cond_;
private:
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);
//
InitThreadPool();
}
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);
std::cout << "执行任务:" << t << std::endl;
tp->Unlock();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < num_; i++)
{
// 由于创建线程传入的参数是确定参数个数的
// 所以该函数不能是类的成员函数,需要设置为静态函数
pthread_create(&tid, nullptr, Rountine, this);
}
}
void PushTask(const T &in)
{
Lock();
task_queue_.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out = task_queue_.front();
task_queue_.pop();
}
~ThreadPool()
{
pthread_cond_destroy(&cond_);
pthread_mutex_destroy(&mtx_);
}
};
}
三、线程安全的单例模式
3.1 概念
单例模式是一种 “经典的, 常用的, 常考的” 设计模式.
单例模式的特点 :某些类, 只应该具有一个对象(实例), 就称之为单例.
饿汉实现方式和懒汉实现方式
[洗完的例子]
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度.
饿汉方式实现单例模式
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
template <typename T>
T Singleton::data = T();
懒汉方式实现单例模式
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.
但是后续再次调用, 就没有问题了.
3.2 懒汉模式的线程池
#include "thread_pool.hpp"
using namespace ns_threadpool;
int main()
{
while (true)
{
sleep(1);
int t = rand()%10 + 1;
std::cout << "send task: " << t << std::endl;
ThreadPool<int>::GetInstance()->PushTask(t);
//单例本身会在任何场景,任何环境下被调用
//GetInstance():被多线程重入,进而导致线程安全的问题
//std::cout << ThreadPool<int>::GetInstance() << std::endl;
}
}
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
namespace ns_threadpool
{
const int g_num = 5;
template <class T>
class ThreadPool
{
private:
std::queue<T> task_queue_;
int num_; // 线程池线程的数量
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);
InitThreadPool();
}
ThreadPool(const ThreadPool<T> &tp) = delete;
//赋值语句
ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;
private:
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:
// 获取实例,也必须为静态函数才能获取
static ThreadPool<T>* GetInstance()
{
// 为了防止多个线程同时调用,需要加锁保护
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 检查当前对象是否被创建
if (ins == nullptr)
{
// 双重判定,减少竞争锁的消耗
pthread_mutex_lock(&mutex);
if (ins == nullptr)
{
ins = new ThreadPool<T>();
std::cout << "首次加载对象" << std::endl;
}
pthread_mutex_unlock(&mutex);
}
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);
std::cout << "执行任务:" << t << std::endl;
tp->Unlock();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < num_; i++)
{
// 由于创建线程传入的参数是确定参数个数的
// 所以该函数不能是类的成员函数,需要设置为静态函数
pthread_create(&tid, nullptr, Rountine, this);
}
}
void PushTask(const T &in)
{
Lock();
task_queue_.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out = task_queue_.front();
task_queue_.pop();
}
~ThreadPool()
{
pthread_cond_destroy(&cond_);
pthread_mutex_destroy(&mtx_);
}
};
// 类外初始化静态成员变量
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
注意事项:
- 加锁解锁的位置
- 双重 if 判定, 避免不必要的锁竞争
- volatile关键字防止过度优化(ins)
四、读写者问题
4.1 概念
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
同样的对读写者模型有
- 一个缓冲区
- 两种角色(读者、写者)
- 三种关系
- 读者和读者:没有关系
- 写者和读者:互斥、同步
- 写者和写者:互斥
读写者模型VS生产者消费者模型
本质:读者不会拿走数据,而消费者会拿走数据
什么情况适用读写者模型
- 对数据的大部分操作是读取,只有少量的操作是写入,也就是读者多于写者
- 判断读者一方是否会把资源拿走
4.2 相关接口
初始化
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);
优先级:
- 读者优先:读者和写者同时到来的时候,我们让读者先进入访问
- 写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有的读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入。
存在读者多,写者少问题,所以,有写饥饿问题
设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/
五、自旋锁
5.1 自旋锁VS悲观锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。前面经常使用的就是悲观锁
自旋锁:条件不满足时不会被挂起,而是反复询问。
使用场景
线程访问临界资源花费时长
- 如果特别长 — 适合挂起等待(悲观锁)
- 时间特别短 — 适合自旋锁(因为很快就能获得锁)
5.2 常用接口
相关操作和互斥锁类似