死锁及避免
死锁的产生:线程a获得资源x的锁后,去获取资源y的锁,在这个空当,线程b获得了资源y的锁,再去获取x的锁。
此时,显然地,a获取y的时候,陷入等待,b获取x的时候,也陷入等待,两个线程因为锁而死亡,即发生了死锁。
在实际的项目中,死锁是一种较难发现和定位的程序bug,因为程序表面上看起来一切正常,没有崩溃,不会有coredump。
但程序此时的两个线程已经没有在工作了,它们的既定任务也不会被正确地执行,所以无法得到正确的结果。
死锁的定位:如果在程序中使用了多个锁,特别是使用了嵌套锁,而线程又没有按照既定的设计给出结果,可能发生了死锁。
发生死锁的程序,表面看起来进程仍在运行,但线程却一直在sleep,因为它们都在为了获得想要的锁陷入了等待。
通过strace -p进程id,可以跟踪进程的执行情况,看看是不是进程一直处于等待锁。
另一种方式,是使用gdb的strace,可以查看每个线程的调用栈,如果有线程一直处于等待锁的状态,则可能发生了死锁。
死锁的避免:有以下几个建议:
- 优化业务逻辑,尽量避免多线程嵌套使用锁
- 使用try尝试获取锁,如果失败,则释放掉已经获取的锁
- 使用RAII形式的锁,如lock_guard,作用域外自动释放锁,避免忘记释放导致的bug
- 加锁时对metux的地址进行比较,每次从最小地址开始加锁,保证进程内所有线程都按同一顺序使用锁,避免嵌套
- 对加锁业务限制时长,如果超时则释放锁
move语义原理及作用
c++11支持move语义,它的作用不是移到一个值,而是:
用代码表达就是:rvalue = static_cast<T&&>(lvalue)。
强制转化为右值后,它就可以被移动了,所以该命令叫move。
移动操作的支持,可以避免以前只能复制的低效。
但也不能太迷信它,因为一些对象并没有提供移动操作,移动它们会自动退化为复制。
另外,移动并不总是比复制更快,如对象是采用了SSO(小型字符串优化)的string(长度小于15)。
注意,不要对const对象执行move,因为const要确保参数安全不被改变,而move后不能保证这点,所以move会退化为复制。
STL中的内存管理allocator
STL使用的内存管理机制叫做allocator,以vector为例,原型:
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
{
}
std::allocator即为STL默认的标准内存管理器,一般使用时无需重写它,使用默认的即可。
默认的allocator是一个由两级分配器构成的内存管理器:
- 当申请的内存大于128B时,启动第一级分配器,通过malloc直接向系统的堆空间分配
- 申请的内存大小小于128B时,启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户
这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。
关于allocator,主要是以下方法的实现:
template<typename T>
struct Allocator{
T*allocate(size_t size){
return (T*)malloc(sizeof(T)*size);
}
void deallocate(void *p){
free(p);
}
void construct(T*p,const T&val){
new (p) T(val);
}
void construct(T*p,T&&val){
new (p) T(std::move(val));
}
void destroy(T*p){
p->~T();
}
};
更多深入内容得看源码剖析了。
mutex和临界区异同
这个问题有点突兀,因为临界区(Critical Section)属于Windows独有的,Linux上并不支持。
但是从实现原理和机制上也可以一起讨论一下。
- mutex:只有拥有互斥对象的线程才具有访问资源的权限,互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问
- 临界区:在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
区别:
- 临界区是非内核对象,只在用户态进行锁操作,速度快;互斥量是内核对象,在核心态进行锁操作,速度慢
- 临界区通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问
- 临界区只能用于对象在同一进程里线程间的互斥访问;互斥量可以用于对象进程间或线程间的互斥访问
- 对于多线程频繁读而不频繁写的场景,使用shared_mutex是提升性能的更佳选择
服务发现和服务熔断
微服务中的两个基本原理:
- 服务发现:有两种主要的服务发现机制:客户端发现 和 服务端发现
- 客户端发现:客户端负责决定可用服务实例的网络地址并且在集群中对请求负载均衡, 客户端访问服务登记表,也就是一个可用服务的数据库,然后客户端使用一种负载均衡算法选择一个可用的服务实例然后发起请求
- 服务端发现:客户端通过一个负载均衡器向服务发送请求,负载均衡器查询服务注册表并把请求路由到一台可用的服务实例上。和客户端发现一样,服务实例通过服务注册表进行服务的注册和注销
- 常见的框架:Consul、 ZooKeeper以及Etcd
- 服务熔断:当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用(如使用本地调用),如不熔断,可能导致系统雪崩
- 服务降级:在服务器压力剧增的时候,对一些服务有意地不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行
|