文章目录
- 4. 线程池
- 5. 单例模式
- 5.1 饿汉模式
- 5.2 懒汉模式
- 6. STL、智能指针和线程安全
- 6.1 STL中的容器是否是线程安全的
- 6.2 智能指针是否是线程安全的
- 6.3 其他常见的各种锁
- 7. 读者写者模型
- 7.1 基本概念
- 7.2 读写锁
- 7.3 基本操作
- 7.4 优先级
4. 线程池
介绍
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这避免了在处理短时间任务时创建与销毁线程的代价,线程池不仅能够保证内核的充分利用,还能防止过分调度,可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量
应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短,WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的,因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数,但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误
示例
- 创建固定数量线程池,循环从任务队列中获取任务对象
- 获取到任务对象后,执行任务对象中的任务接口
Task.hpp
#pragma
#include <iostream>
namespace ns_task
{
class Task
{
private:
int _x;
int _y;
char _op;
public:
Task()
{
}
Task(int x, int y, char op) : _x(x), _y(y), _op(op)
{
}
~Task()
{
}
public:
int run()
{
int res = 0;
switch (this->_op)
{
case '+':
res = this->_x + this->_y;
break;
case '-':
res = this->_x - this->_y;
break;
case '*':
res = this->_x * this->_y;
break;
case '/':
res = this->_x / this->_y;
break;
case '%':
res = this->_x % this->_y;
break;
default:
std::cout << "Calculation error..." << std::endl;
break;
}
std::cout << "The calculation result is " << res << std::endl;
return res;
}
};
}
ThreadPool.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
namespace ns_threadpool
{
const int default_num = 5;
template <class T>
class ThreadPool
{
private:
int _num;
std::queue<T> _task_queue;
pthread_mutex_t _mtx;
pthread_cond_t _cond;
public:
ThreadPool(int num = default_num) : _num(num)
{
pthread_mutex_init(&this->_mtx, nullptr);
pthread_cond_init(&this->_cond, nullptr);
}
~ThreadPool()
{
pthread_mutex_destroy(&this->_mtx);
pthread_cond_destroy(&this->_cond);
}
void lock()
{
pthread_mutex_lock(&this->_mtx);
}
void unlock()
{
pthread_mutex_unlock(&this->_mtx);
}
bool isEmpty()
{
return this->_task_queue.empty();
}
void wait()
{
pthread_cond_wait(&this->_cond, &this->_mtx);
}
void wake()
{
pthread_cond_signal(&this->_cond);
}
public:
// 线程无法直接执行类内的方法,因为类中的方法参数列表会隐含一个this指针,需要定义为静态成员函数
static void *runtime(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *_this = (ThreadPool<T> *)args;
while (true)
{
_this->lock();
while (_this->isEmpty())
{
_this->wait();
}
T *task = new T();
_this->popTask(task);
_this->unlock();
task->run();
}
}
void initThreadPool()
{
pthread_t tid;
for (int i = 0; i < this->_num; i++)
{
pthread_create(&tid, nullptr, runtime, (void *)this);
}
}
void pushTask(const T &in)
{
lock();
this->_task_queue.push(in);
unlock();
this->wake();
}
void popTask(T *out)
{
*out = this->_task_queue.front();
this->_task_queue.pop();
}
};
}
Main.cc
#include "Task.hpp"
#include "ThreadPool.hpp"
#include <ctime>
using namespace ns_threadpool;
using namespace ns_task;
int main()
{
srand((long long)time(nullptr));
ThreadPool<Task> *tp = new ThreadPool<Task>();
tp->initThreadPool();
while (true)
{
Task t(rand() % 20 + 1, rand() % 10 + 1, "+-*/%"[rand() % 5]);
tp->pushTask(t);
sleep(1);
}
return 0;
}
5. 单例模式
某些类,只应该具有一个对象(实例),就称之为单例
- 语义上只有一个
- 该对象内部存在大量的空间,保存了大量的数据,如果允许该对象存在多份,或者允许发生各种拷贝,内存中会存在很多冗余数据
5.1 饿汉模式
在加载对象时候,对象就会创建实例
template <typename T>
class Singleton
{
static T data;
public:
static T* GetInstance()
{
return &data;
}
};
5.2 懒汉模式
懒汉方式最核心的思想是"延时加载",从而能够优化服务器的启动速度
template <typename T>
class Singleton
{
static T* inst;
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
但这样的懒汉模式存在一个严重的问题——线程不安全,第一次调用GetInstance的时候,如果多个线程同时调用,可能会创建出多份T对象的实例,这里第一次调用时的T对象也是临界资源,如果后续再次调用,就没有问题
线程安全的懒汉方式实现的线程池
ThreadPool.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
namespace ns_threadpool
{
const int default_num = 5;
template <class T>
class ThreadPool
{
private:
int _num;
std::queue<T> _task_queue;
pthread_mutex_t _mtx;
pthread_cond_t _cond;
static ThreadPool<T> *instance;
private:
ThreadPool(int num = default_num) : _num(num)
{
pthread_mutex_init(&this->_mtx, nullptr);
pthread_cond_init(&this->_cond, nullptr);
}
ThreadPool(const ThreadPool<T> &tp) = delete;
ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;
public:
static ThreadPool<T> *getInstance()
{
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
if (instance == nullptr)
{
pthread_mutex_lock(&mtx);
if (instance == nullptr)
{
instance = new ThreadPool<T>();
instance->initThreadPool();
}
pthread_mutex_unlock(&mtx);
}
return instance;
}
~ThreadPool()
{
pthread_mutex_destroy(&this->_mtx);
pthread_cond_destroy(&this->_cond);
}
void lock()
{
pthread_mutex_lock(&this->_mtx);
}
void unlock()
{
pthread_mutex_unlock(&this->_mtx);
}
bool isEmpty()
{
return this->_task_queue.empty();
}
void wait()
{
pthread_cond_wait(&this->_cond, &this->_mtx);
}
void wake()
{
pthread_cond_signal(&this->_cond);
}
public:
// 线程无法直接执行类内的方法,因为类中的方法参数列表会隐含一个this指针,需要定义为静态成员函数
static void *runtime(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *_this = (ThreadPool<T> *)args;
while (true)
{
_this->lock();
while (_this->isEmpty())
{
_this->wait();
}
T *task = new T();
_this->popTask(task);
_this->unlock();
task->run();
}
}
void initThreadPool()
{
pthread_t tid;
for (int i = 0; i < this->_num; i++)
{
pthread_create(&tid, nullptr, runtime, (void *)this);
}
}
void pushTask(const T &in)
{
lock();
this->_task_queue.push(in);
unlock();
this->wake();
}
void popTask(T *out)
{
*out = this->_task_queue.front();
this->_task_queue.pop();
}
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
}
6. STL、智能指针和线程安全
6.1 STL中的容器是否是线程安全的
不是
原因是:STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响,而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶),因此 STL 默认不是线程安全,如果需要在多线程环境下使用,往往需要调用者自行保证线程安全
6.2 智能指针是否是线程安全的
对于unique_ptr
,由于只是在当前代码块范围内生效,因此不涉及线程安全问题
对于shared_ptr
,多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr
能够高效、原子的操作引用计数
6.3 其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,被阻塞挂起
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作
- 版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等,如果相等则用新值更新,若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试
- 自旋锁:线程会反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种忙等待,一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁
- 公平锁:多个线程按照申请锁的顺序来获取锁
- 非公平锁:多个线程获取的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁在高并发的情况下,有可能会造成优先级后传或者饥饿想象
考虑线程访问临界资源的时长问题,因为将线程挂起等待是有成本的
- 如果花费的时间非常短,就比较适合自旋锁
- 如果花费的时间比较长,就比较适合挂起等待锁
自旋锁
-
初始化
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
-
销毁
int pthread_spin_destroy(pthread_spinlock_t *lock);
-
加锁与解锁
int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock);
7. 读者写者模型
7.1 基本概念
读者写者模型
- 对数据,大部分的操作是读取,少量的操作是写入
- 判断依据是,进行数据读取(消费)的一端,是否会将数据取走,如果不取走,就可以考虑读者写者模型
321原则
-
三种关系
-
读者和读者:没有关系
生产者消费者模型 vs 读者写者模型
不一样的原因:读者不会取走资源,而消费者会拿走数据
-
写者和写者:互斥、同步
-
读者和写者:互斥关系
-
-
两种角色:读者和写者,由线程承担
-
一个交易场所:一段缓冲区(自己申请的,或者STL容器)
7.2 读写锁
在编写多线程的时候,有一种情况是十分常见的,那就是有些公共数据修改的机会比较少,相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长,给这种代码段加锁,会极大地降低我们程序的效率,使用读写锁可以专门处理这种多读少写的情况
注意:写独占,读共享,读锁优先级高
7.3 基本操作
-
初始化
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有三种选择
PTHREAD_RWLOCK_PREFER_READER_NP
:默认设置,读者优先,可能会导致饥饿情况PTHREAD_RWLOCK_PREFER_WRITER_NP
:写者优先,目前有 BUG,导致表现行为和PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP
:写者优先,但写者不能递归加锁
- pref有三种选择
7.4 优先级
读者优先:读者和写者同时到来的时候,让读者先进入访问
写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有读者,都不要再进入临界区访问了,等临界区中没有读者的时候,让写者先写入