1.STL中的线程安全:
1.STL容器:默认不安全,因为为了追求极致的性能,引入线程安全机制(如加锁)会显著影响性能。此外,不同容器的加锁策略可能导致不同的性能表现(例如,哈希表的锁表与锁桶),因此需要使用者自己保证线程安全
2.智能指针:unique_ptr的作用范围限定在当前代码块内,不涉及线程安全问题,shared_ptr的特点是多个对象可能共享同一个引用计数变量,存在线程安全问题,所以标准库在实现shared_ptr时,采用了基于原子操作(如CAS)的方式来保证引用计数的操作既高效又原子,从而确保线程安全
2.线程安全的单例模式
1.单例模式的特点:某些类只应具有一个对象(实例),这样的类称为单例
2.比如一把钥匙开一把锁,在服务器开发中,常用于管理大量数据(如上百G)的单例类
3.单例模式的实现方法
1.饿汉模式:类比于吃完饭后立即洗碗,以便下次吃饭时可以直接使用。在程序中,饿汉方式是在类加载时就立即初始化并创建单例对象
2.懒汉模式:类比于吃完饭后暂时不洗碗,等到下次需要用时再洗。懒汉方式的核心是“延时加载”,即直到第一次使用时才创建单例对象,以优化服务器启动速度
示例饿汉模式:
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
只能通过类使用T对象,就可以保证线程中只有一个T对象的实例
示例懒汉模式:
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
调用时,发现不存在,才对指针进行 new
存在问题:线程不安全。在第一次调用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;
}
};
注意:首先是加锁与解锁的位置,正好是new对象之间,双重if判定,避免不必要的锁竞争,使用volatile关键字防止编译器过度优化
全局变量:
1.饿汉模式:一开始就创建
2.懒汉模式:使用指针,调用的时候再new
懒汉模式为什么GetInstance函数要设置成静态?
1.非静态成员函数是可以通过对象访问到静态成员变量的
2.主要是单例对象在实例化出对象前没有对象可以给你借由它访问到那个非静态成员函数,自然也就没办法访问到静态成员变量了
3.所以将GetInstance设置成静态成员函数,即可在没有对象时,通过类名::函数名直接调用
4.在不实例化对象的情况下不能调用该函数,确实懒
4.线程池改成单例模式
1.构造方法一定是要有的,单例版的线程池,就是只获取一个线程池,将可能违背单例的部分私有化,不能被对象调用,就能保证多个对象只有一个了
2.多线程调用单例必须加锁:tp本身就是一份公共资源,如果两个线程同时调用, 可能会创建出两份 ,T 对象的实例。因此我们需要在创建空间时加锁
public:
static ThreadPool<T> *GetInstance()
{
if (nullptr == tp_) // ???
{
pthread_mutex_lock(&lock_);
if (nullptr == tp_)
{
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>();
}
pthread_mutex_unlock(&lock_);
}
return tp_;
}
private:
ThreadPool(int num = defalutnum) : threads_(num)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
static ThreadPool<T> *tp_;
代码解析:
静态成员变量tp_指针,指向一个ThreadPool<T>实例,只有创建整个线程池才能调用该函数,是典型的懒汉模式,lock_是一个互斥锁,使多个线程不能同时创建对象,以及条件变量_cond
static ThreadPool<T> *GetInstance()
: 这是一个静态方法,用于获取ThreadPool<T>
类的唯一实例。它首先检查tp_
是否为nullptr
,如果是,则进入临界区(通过加锁lock_
),再次检查tp_
是否为nullptr
(这是因为在进入临界区之前可能有其他线程已经创建了实例),然后创建ThreadPool<T>
实例并赋值给tp_
。最后,退出临界区(通过解锁lock_
)并返回tp_
5.静态锁
创建
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
优化:即全局如果已经被其他线程创建了,就不必要再线性竞争去创建了。所以 if 判断是否存在后再加锁
if (nullptr == tp_) //加锁
{
pthread_mutex_lock(&lock_);
6.自旋锁
1.常见锁:
1.悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。(平时用的 互斥锁 都是这个)
2.乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作
3.CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试
2.自旋锁
本质:和非阻塞轮询一样,不断询问更新锁的状态
实现:
1.互斥锁的try接口
2.系统接口pthread_spin_t
底层是一个while循环进行不断访问,spin_trylock
失败了就返回,就相当于互斥锁了
初始化与销毁
加锁
解锁
7.读写锁
1.读者写者问题:
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁
写独占,读共享,读锁优先级高
读写锁 pthread_rwlock_t
初始化销毁
读加锁(读者采用)
写加锁(写者采用)
解锁(读者写者采用)
读多写少的情况,默认读者优先,写者容易饥饿
在任意一个时刻,只允许一个写者写入,但是可能允许多个读者读取(写者阻塞)。
写者在写的时候不允许其他写者写也不允许读着读,而读者在读的时侯允许其他读者一起读但是不允许写者写,虽然默认读者优先,但是部分情况下写者优先更加合适
8.读者优先与写者优先
假设在某一时刻读者写者同时到达:
1.读者优先:先读再写
2.写者优先:先写再读,以及在读的线程继续(避免写者饥饿)
设置优先级
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 写者优先,但写者不能递归加锁
*/