目录
- `📚STL,智能指针和线程安全 `
- `📕STL中的容器是否是线程安全的?`
- `💡智能指针是否是线程安全的?`
- `🍁shared_ptr、unique_ptr和weak_ptr`
- `🍑线程安全的智能指针atomic_shared_ptr`
- `🌙线程安全的单例模式 `
- `什么是单例模式 `
- `什么是设计模式`
- `单例模式的特点 `
- `⭐饿汉实现方式和懒汉实现方式`
- `🦅饿汉方式实现单例模式 `
- `🦌懒汉方式实现单例模式`
- `🐎懒汉方式实现单例模式(线程安全版本)`
- 🐛其他常见的各种锁
- `什么是自旋?`
- `📚读者写者问题`
- `🔒读写锁 `
- 🐟系统中读写锁接口
- 🦈读者写者优先问题:
📚STL,智能指针和线程安全
📕STL中的容器是否是线程安全的?
-
不是.
-
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。
-
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
-
因此 STL 默认不是线程安全.
如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
💡智能指针是否是线程安全的?
🍁shared_ptr、unique_ptr和weak_ptr
在C++中,常见的智能指针包括shared_ptr
、unique_ptr
和weak_ptr
等
-
shared_ptr:
shared_ptr使用引用计数机制
来管理对象的生命周期。虽然它的复制构造函数和赋值操作是原子操作(这保证了智能指针本身的拷贝在多线程下是安全的),但引用计数的增加和减少通常不是原子的
。这意味着,如果多个线程同时尝试增加或减少同一个shared_ptr的引用计数,而没有适当的同步机制,就可能导致数据竞争和不一致的问题。因此,在多线程环境中,直接使用shared_ptr
进行共享对象的访问和修改通常不是线程安全的
。如果需要在多线程中使用shared_ptr,应该使用互斥锁或其他同步机制来保护对共享资源的访问。
-
unique_ptr:
- unique_ptr保证同一时间只有一个所有者拥有对象的所有权。由于它不允许复制(除了移动构造和移动赋值之外),因此
它不会遇到与shared_ptr相同的线程安全问题
。然而,如果尝试将unique_ptr传递给不同的线程,或者在多线程中修改它的所有权,仍然需要适当的同步措施来确保线程安全。
- unique_ptr保证同一时间只有一个所有者拥有对象的所有权。由于它不允许复制(除了移动构造和移动赋值之外),因此
-
weak_ptr:
- weak_ptr是一个对shared_ptr所管理的对象存在性的弱引用,它不会增加对象的引用计数。因此,
weak_ptr的线程安全性问题通常与它所引用的shared_ptr的线程安全性相关
。在多线程环境中,如果weak_ptr和shared_ptr一起使用,并且没有适当的同步机制,同样可能导致问题。
- weak_ptr是一个对shared_ptr所管理的对象存在性的弱引用,它不会增加对象的引用计数。因此,
🍑线程安全的智能指针atomic_shared_ptr
为了解决多线程环境中智能指针的线程安全问题,C++14引入了atomic_shared_ptr
。这是一个线程安全的智能指针,它使用原子操作来管理引用计数,从而避免了数据竞争的问题。然而,需要注意的是,即使使用了atomic_shared_ptr,对智能指针所指向对象的访问仍然可能需要额外的同步机制,特别是当这些访问涉及到修改对象状态时。
🌙线程安全的单例模式
什么是单例模式
- 单例模式是一种 “经典的, 常用的, 常考的” 设计模式。
什么是设计模式
针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。
单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例。
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
⭐饿汉实现方式和懒汉实现方式
[洗完的例子]
- 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
- 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
饿汉方式优化将来使用的效率,以空间换时间。程序加载的时候就创建对象。
🦅饿汉方式实现单例模式
- 只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.
template <typename T>
class Singleton
{
public:
static T* GetInstance()
{
return &data;
}
private:
static T data;
};
🦌懒汉方式实现单例模式
懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度.运行时创建对象。
template <typename T>
class Singleton {
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
private:
static T* 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;
}
};
🐛其他常见的各种锁
悲观锁:
在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。乐观锁:
每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。CAS操作
:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。自旋锁
,公平锁,非公平锁?重复非阻塞的申请锁,称为自选,非阻塞的读取文件这个过程叫非阻塞
。
什么是自旋?
-
如果一个线程申请锁成功后执行的代码很耗时(比如代码里面面有大量的io操作等),推荐线程在申请锁的时候等待挂起,如果不耗时的话,推荐让其他线程
重复非阻塞的申请锁
,也就是自旋
。 -
自旋锁:用法和其他锁一样,不赘述。
📚读者写者问题
🔒读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
读写锁的伪代码:
🐟系统中读写锁接口
-
初始化
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);
🦈读者写者优先问题:
默认的是写者优先
写者优先:
当读者正在访问资源的时候,写者申请访问,这时候过后再有读者申请锁的时候,会失败,类似于会放到在一个等待队列中等待,会等当前读者读完后,写者再访问资源,访问完后这些读者才会申请锁访问资源(也就是说,如果读者挡在访问资源的时候,这时写者要访问资源,后续来的读者都会等待,等目前正在访问资源的读者退出后,读者访问完资源退出后,这些等待的读者才可以申请锁,访问资源。)
读者优先:与上面的相反。