1. 前置知识
1.1 __builtin_expect
1.1.1 使用
__builtin_expect
提供给编译器分支预测优化信息,其含义为 exp 大概率为 c,其返回值为 exp 的值;
long __builtin_expect(long exp, long c)
// 下述表明该分支大概率不会执行
if (__builtin_expect(t_cachedTid == 0, 0))
{
func();
}
// C++20 正式将其变为关键字,之前的宏如下
// 两次取非,可以保证最后一定为 bool 值
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
使用这个优化手段时,必须要保证概率差别真大很多(默认要有 90% 的把握),在 muduo 库中用在缓存线程 tid 场景;
1.1.2 原理
// -fprofile-arcs 必须要加上才会在汇编代码中体现出来区分
gcc -fprofile-arcs -O2 myexpect.cpp -o mexp
objdump -S mexp
__builtin_expect
在汇编代码中体现是,把概率更大的分支与之后的代码直接放在一起,概率小的分支需要进行跳转;
if (likely(a == 2))
a++;
else
a--;
如下图红线为概率小的分支,蓝线为概率大的分支;(这样的好处是 CPU 流水线可以大概率顺利执行,从而提高效率)
1.1.3 参考
https://kernelnewbies.org/FAQ/LikelyUnlikely
https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
1.2 __thread
(1)GCC 内置的线程局部存储,类似全局变量,但有所不同;
每个线程都拥有该变量的一份拷贝,且互不干扰。
线程局部存储中的变量将一直存在,直至线程终止,当线程终止时会自动释放这一存储;
只能存储一些小变量(基本为内置类型);
(2)C++11 新引入了 thread_local 关键字,其与之作用相同;
1.3 const 成员函数
(1)一般情况下 const 对象调用 const 成员函数,普通对象调用非 const 成员函数
const 对象不可用调用非 const 成员函数。普通对象可以调用 const 成员函数
(2)const 成员函数中可以修改静态数据成员,不可以修改非静态成员(此处的理解是,从语义方面来讲,静态成员变量类似于全局变量,不属于某个对象,因此不算是对对象进行了修改。)
(3)对于声明时加了mutable(只能修饰非静态非const)的变量,也可以在const成员函数中改变。
1.4 Observer 模式
Observable 维护了一个指针数组,每个元素指向一个 Observer ,调用 notifyObservers
调用各个 Observer 的 update
方法;
1.8 节中 Observer 对象也是由 shared_ptr 管理的;
1.5 MutexLockGuard
MutexLock 封装了一个互斥锁和一个线程 ID 变量,从而可以判断当前线程是否持有该锁;
MutexLockGuard 对上述进一步封装,构造时调用 lock
,析构时调用unlock
2. 线程安全的对象生命期管理
// 内部调用 gettid() ,并缓存了线程 tid;
CurrentThread::tid()
2.1 对象的构造
要做到线程安全,唯一的要求是在构造期间不要泄漏 this 指针;
不要在构造函数中注册任何回调;
不要在构造函数中把 this 传给跨线程的对象;(构造期间对象没有完成初始化,其他线程访问时可能是半成品,造成难以预料的后果)
即使在构造函数的最后一行也不行;(因为该类可能是基类,之后要去执行子类的构造函数)
2.2 对象的析构
(1)一个对象可被多线程观察到,线程 A 调用析构函数,线程 B 调用该对象其他函数,就可能会导致意想不到的后果;
线程 A 可能获取到锁先执行,线程 B 阻塞在锁处;但析构函数会把锁也释放掉;
(2)只要别的线程都访问不到这个对象时,析构才是安全的;
(3)一个函数如果要锁住相同类型的多个对象,为保证始终按相同顺序加锁(避免潜在死锁,例如线程 A 调用 swap(a,b)
,线程 B 调用 swap(b,a)
),可以先比较 mutex 对象的地址,始终先加锁地址较小的 mutex;
2.3 shared_ptr 的使用
使用 shared_ptr 来管理对象
2.3.1 weak_ptr
可以判断对象是否还存活;如果对象存活,可以提升(lock()
)为 shared_ptr,否则,返回一个空的 shared_ptr,提升lock()
行为是线程安全的
2.3.2 enable_shared_from_this
直接从原始指针构造的各 shared_ptr 不会共享引用计数,这就可能会导致重复释放问题;
enable_shared_from_this 使得从 当前被 pt 管理的对象 t 安全的生成其他 shared_ptr 实例(这些都共享 t 的所有权)
2.3.3 swap 技巧
void write()
{
shared_ptr<Foo> newPtr(new Foo); // 注意,对象的创建在临界区之外
{
MutexLockGuard lock(mutex);
globalPtr = newPtr; // write to globalPtr
}
// use newPtr since here,读写newPtr 无须加锁
doit(newPtr);
}
上述代码中原先 globalPtr 指向对象的析构可能会发生在临界区,可以利用 swap
技巧将其推迟到临界区之外;
void write()
{
shared_ptr<Foo> newPtr(new Foo); // 注意,对象的创建在临界区之外
shared_ptr<Foo> newPtr1(newPtr);
{
MutexLockGuard lock(mutex);
swap(newPtr1, globalPtr); // write to globalPtr
}
// use newPtr since here,读写newPtr 无须加锁
doit(newPtr);
}
2.3.4 注意事项
(1)意外延长对象的生命期
boost::bind 会把实参拷贝一份,如果参数是个 shared_ptr,那么对象的生命期就不会短于 boost::function 对象
class Foo
{
void doit();
};
shared_ptr<Foo> pFoo(new Foo);
boost::function<void()> func = boost::bind(&Foo::doit, pFoo); // long life foo 这里 func 持有 shared_ptr 的一份拷贝
(2)析构动作在创建时捕获;
shard_ptr<T> ptr( new T1 );
注意,构造 shard_ptr 时,使用 模板类型推导 保存了 T1 的类型(而非 T 的类型,因为 T 可能是 T1 的基类),从而可以调用其相应析构函数(即使基类的析构函数不是虚函数,详见 STL源码分析:shared_ptr 和 weak_ptr;
shared_ptr<void>
可以持有任何对象,也能保证安全的释放;
(3)如果对象的析构比较耗时,可以使用一个单独的线程专门做析构
可以使用一个 BlockingQueue<shared_ptr<void> >
2.3.5 对象池
只允许出现一个 T
对象,多线程同时访问同一个对象时,其应该被共享;
自然的想法是使用 shared_ptr
管理对象,但引用计数变为 0 时,虽然可以释放对象 T
,但无法减小哈希表的大小;
std::map<string, weak_ptr<T>> mt_;
解决办法是:使用智能指针的定制析构功能,在析构 T
对象时同时清理哈希表;
class StockFactory : boost::noncopyable
{
// 在get() 中,将pStock.reset(new Stock(key)); 改为:
// pStock.reset(new Stock(key),
// boost::bind(&StockFactory::deleteStock, this, _1)); // ***
private:
void deleteStock(Stock* stock)
{
if (stock) {
MutexLockGuard lock(mutex_);
stocks_.erase(stock->key());
}
delete stock; // sorry, I lied
}
.......
};
bind
中使用到了 this
指针这种做法要求 StockFactory
不能先于 Stock
对象析构,否则会 core dump;