目录
1.线程安全
1.1 线程安全的单例模式
1.2 饿汉与懒汉实现方式:
实操
2.锁
3.读者写者问题
实操
4.网络基础
4.1 初始协议
书单:
1.线程安全
STL中的容器和智能指针的线程安全性整理如下:
STL容器线程安全性:
- 状态:STL中的容器默认不是线程安全的。
- 原因:STL的设计目标是追求极致的性能。引入线程安全机制(如加锁)会显著影响性能。此外,不同容器的加锁策略可能导致不同的性能表现(例如,哈希表的锁表与锁桶)。
- 解决方案:在多线程环境中使用STL容器时,需要调用者自行确保线程安全。
智能指针线程安全性:
- unique_ptr:由于其作用范围限定在当前代码块内,不涉及线程安全问题。
- shared_ptr:由于多个对象可能共享同一个引用计数变量,存在线程安全问题。
- 解决方案:标准库在实现shared_ptr时,采用了基于原子操作(如CAS)的方式来保证引用计数的操作既高效又原子,从而确保线程安全。
1.1 线程安全的单例模式
什么是单例模式:
单例模式是一种“经典的、常用的、常考的”设计模式。它是设计模式的一种,旨在为某些常见场景提供标准化的解决方案。
单例模式的特点:
- 某些类只应具有一个对象(实例),这样的类称为单例。
- 例如,一个男性只能有一个妻子。在服务器开发中,常用于管理大量数据(如上百G)的单例类。
1.2 饿汉与懒汉实现方式:
- 饿汉方式:类比于吃完饭后立即洗碗,以便下次吃饭时可以直接使用。在程序中,饿汉方式是在类加载时就立即初始化并创建单例对象。
- 懒汉方式:类比于吃完饭后暂时不洗碗,等到下次需要用时再洗。懒汉方式的核心是“延时加载”,即直到第一次使用时才创建单例对象,以优化服务器启动速度。
饿汉方式实现单例模式:
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
只要通过Singleton
类来使用T
对象,就能保证进程中只有一个T
对象的实例。
懒汉方式实现单例模式:
调用时,发现不存在,才对指针进行 new
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;
}
};
注意事项:
- 加锁解锁的位置。
- 双重
if
判定,避免不必要的锁竞争。 - 使用
volatile
关键字防止过度优化。
例如针对 全局变量 ,其生命周期随进程。
- 饿汉:一开始就创建
- 懒汉:使用指针,调用的时候再 new
懒汉模式中,为何GetInstance要设置成静态?
- 正常来说非静态成员函数是可以通过对象访问到静态成员变量的,这个没有问题
- 主要是单例对象在实例化出对象前没有对象可以给你借由它访问到那个非静态成员函数,自然也就没办法访问到静态成员变量了
- 所以将GetInstance设置成静态成员函数,即可在没有对象时,通过类名::函数名直接调用
需要的时候才创建,懒汉模式
实操
将线程池改成单例,懒汉模式
构造方法一定是要有的,单例版的线程池,就是只获取一个线程池,将可能违背单例的部分私有化,不能被对象调用,就能保证多个对象只有一个了
多线程调用单例呢?需要加锁
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_;
优化多线程的判断情况:
多线程创建单例👆画图思考
- 创建静态锁
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
- 优化:即全局如果已经被其他线程创建了,就不必要再线性竞争去创建了。所以 if 判断是否存在后再加锁
if (nullptr == tp_) //加锁
{
pthread_mutex_lock(&lock_);
2.自旋锁
其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。(平时用的 互斥锁 都是这个)
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁,公平锁,非公平锁?
自旋锁
信号非阻塞轮询处的故事续接
张三约李四出去吃饭,李四说等一下,张三这是注意到旁边有一家网吧
张三是去网吧,还是在楼下等李四取决于什么?等待时间长短
这和我们线程等待共享资源是一样的?取决于线程需要等待临界区(李四)的时间长短
- 长:挂起等待锁(eg.互斥)
- 短:自旋锁
举例:
在楼下等待一会打个电话,一会打个电话,不断地询问好了吗,叫做自旋
我们之前使用到的条件变量的队列等待,是一种挂起的操作
如何实现自旋
- 互斥锁的 try 接口
- 系统接口
pthread_spin_t
初始化销毁
加锁
解锁
理解:
- 底层帮我们封装了
while
循环,实现不断地询问 spin_trylock
失败了就返回,就相当于互斥锁了
3.读者写者问题
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
在生产消费模型上变形了一点点
也遵循 321 原则
- 3 种关系:写写(互斥),写读(互斥,同步),读读(共享看,没什么关系)
- 2 种角色:读者 s,写者 s,线程承担
- 1 个交易场所:数据交换的地点
生产消费和读写的区别?
- 消费者是拿走,互斥的
- 读读是共享看的
实操
读写锁 pthread_rwlock_t
初始化销毁
读加锁
未来读的角色和写的角色是两种角色,读者采用读加锁的方式
写加锁
解锁
无论是读者还是写者最后不想用了都采用这个方案进行解锁
理论理解:读多写少的情况--默认读者优先(写者容易形成饥饿。
现在就有个问题读写者原理很清楚了,但是读写锁是怎么做到给读加锁写加锁的呢?
不过目前我们知道了,在任意一个时刻,只允许一个写者写入,但是可能允许多个读者读取(写者阻塞)。
写者在写的时候不允许其他写者写也不允许读着读,而读者在读的时候允许其他读者一起读但不允许写着写。
因为我们的读写锁是一个结构体所以它的内部可能包含了对应的读锁,写锁,还有读计数器。然后再进行对应的操作时使用特定的一套方案进行相关的操作。
在某些并发编程场景中,读写锁(reader-writer lock)被用来允许多个读者(不会修改数据的线程)同时访问共享资源,但在写者(可能会修改数据的线程)访问时,则需要独占访问。读写锁通常默认采用读者优先的策略,但有时候写者优先的策略更加合适。
场景描述:
- 假设有10个读者和1个写者。
- 前5个读者已经读取了数据,而第6个和第7个读者正在路上。
- 在某个时刻,一个读者和一个写者同时到达。
- 在传统的读者优先策略下,读者会先获得访问权限。
- 在写者优先的策略下,已经获得访问权限的读者可以继续读取,但新到达的读者需要等待,直到写者完成写入操作。
写者优先策略:
- 写者优先意味着一旦写者请求访问,新到达的读者必须等待,直到所有写操作完成。
- 已经在读取的读者可以继续,但新的读者必须等待。
- 这种策略可以避免写者饥饿,即写者长时间等待的情况。
设置读写优先
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 写者优先,但写者不能递归加锁
*/
4.网络基础
- 网络的本质:传递数据
- 系统的本质:处理数据
收集,检测,决策的三个人,要基于数据进行协作
局域网 LAN: 计算机数量更多了, 通过交换机和路由器连接在一起;
广域网 WAN: 将远隔千里的计算机都连在一起;
所谓 "局域网" 和 "广域网" 只是一个相对的概念. 比如, 我们有 "天朝特色" 的广域网, 也 可以看做一个比较大的局域网
- 我国在网络服务技术处于世界领先地位
- 每个国家都有自己的网络运营商。例如中国的电信,移动,联通,欧洲的诺基亚...
- 计算机是人的工具,人要协同工作,注定了网络的产生是必然的.
4.1 初始协议
- "协议" 是一种约定.
- 电报响一声、两声、三声都代表不同的含义。而这个含义不用解释,双方早就已经有了共识了。-------->这就是我们的约定,而这种约定就是协议。而这种协议的约定是为了减少通信成本!
主机是对称的,都例如存在如下的问题:
- 如何处理发来的数据--https/http/ftp/smtp...
- 长距离传输的数据丢失问题--tcp 协议
- 如何定位的主机问题--ip 协议
- 你怎么保证你的数据能准确的到达下一个设备--数据链路层
所有网络的问题:本质都是传输距离变长,所需要的设备就会变多
在要传输的数据之上,还多了一些数据,就像我们的快递盒和快递单一样,这就是协议报头,其表现形式就是结构体对象
下篇文章将继续讲解,两台设备如何实现对定义的 同一对象 实现传输认识~
书单:
从下篇文章开始我们将要从正式系统横跨到网络的学习了,因此有些书我们就可以读起来了。
操作系统
原理:
- 《操作系统精髓与设计原理》、《现代操作系统》
Linux原理方面的书:
- 《Linux内核设计与实现》–陈莉君、《深入理解Linux内核》(选读–不作为重点)
Linux编程方面的书:
- 《Linux高性能服务器编程》、《Unix环境高级编程》
体系结构:
- 《深入理解计算机系统》
对于系统学过前面的知识建议读书顺序:先编程,后原理