__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 场景;
// -fprofile-arcs 必须要加上才会在汇编代码中体现出来区分
gcc -fprofile-arcs -O2 myexpect.cpp -o mexp
objdump -S mexp
__builtin_expect
在汇编代码中体现是,把概率更大的分支与之后的代码直接放在一起,概率小的分支需要进行跳转;
if (likely(a == 2))
a++;
else
a--;
如下图红线为概率小的分支,蓝线为概率大的分支;(这样的好处是 CPU 流水线可以大概率顺利执行,从而提高效率)
https://kernelnewbies.org/FAQ/LikelyUnlikely
https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
(1)GCC 内置的线程局部存储,类似全局变量,实现非常高效,存取效率与全局变量也类似,但有所不同;
比 pthread_key_t
快很多,使用了段寄存器
每个线程都拥有该变量的一份拷贝,且互不干扰。
线程局部存储中的变量将一直存在,直至线程终止,当线程终止时会自动释放这一存储;
只能存储一些小变量(POD类型,基本为内置类型);不能修饰 class 类型,因为无法自动调用构造和析构函数;
可用于修饰全局变量、函数内的静态变量,不能修饰函数的局部变量或 class 的普通成员变量;
__thread 变量的初始化只能用编译器常量;
(2)C++11 新引入了 thread_local 关键字,其与之作用相同;
(1)一般情况下 const 对象调用 const 成员函数,普通对象调用非 const 成员函数
const 对象不可用调用非 const 成员函数。普通对象可以调用 const 成员函数
(2)const 成员函数中可以修改静态数据成员,不可以修改非静态成员(此处的理解是,从语义方面来讲,静态成员变量类似于全局变量,不属于某个对象,因此不算是对对象进行了修改。)
(3)对于声明时加了mutable(只能修饰非静态非const)的变量,也可以在const成员函数中改变。
Observable 维护了一个指针数组,每个元素指向一个 Observer ,调用 notifyObservers
调用各个 Observer 的 update
方法;
1.8 节中 Observer 对象也是由 shared_ptr 管理的;
MutexLock 封装了一个互斥锁和一个线程 ID 变量,从而可以判断当前线程是否持有该锁;
MutexLockGuard 对上述进一步封装,构造时调用 lock
,析构时调用unlock
要做到线程安全,唯一的要求是在构造期间不要泄漏 this 指针;
不要在构造函数中注册任何回调;
不要在构造函数中把 this 传给跨线程的对象;(构造期间对象没有完成初始化,其他线程访问时可能是半成品,造成难以预料的后果)
即使在构造函数的最后一行也不行;(因为该类可能是基类,之后要去执行子类的构造函数)
(1)一个对象可被多线程观察到,线程 A 调用析构函数,线程 B 调用该对象其他函数,就可能会导致意想不到的后果;
线程 A 可能获取到锁先执行,线程 B 阻塞在锁处;但析构函数会把锁也释放掉;
(2)只要别的线程都访问不到这个对象时,析构才是安全的;
(3)一个函数如果要锁住相同类型的多个对象,为保证始终按相同顺序加锁(避免潜在死锁,例如线程 A 调用 swap(a,b)
,线程 B 调用 swap(b,a)
),可以先比较 mutex 对象的地址,始终先加锁地址较小的 mutex;
使用 shared_ptr 来管理对象
可以判断对象是否还存活;如果对象存活,可以提升(lock()
)为 shared_ptr,否则,返回一个空的 shared_ptr,提升lock()
行为是线程安全的
直接从原始指针构造的各 shared_ptr 不会共享引用计数,这就可能会导致重复释放问题;
enable_shared_from_this 使得从 当前被 pt 管理的对象 t 安全的生成其他 shared_ptr 实例(这些都共享 t 的所有权)
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);
}
(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
可以持有任何对象,也能保证安全的释放;
(3)如果对象的析构比较耗时,可以使用一个单独的线程专门做析构
可以使用一个 BlockingQueue
(1)只允许出现一个 T
对象,多线程同时访问同一个对象时,其应该被共享;
自然的想法是使用 shared_ptr
管理对象,但引用计数变为 0 时,虽然可以释放对象 T
,但无法减小哈希表的大小;
std::map<string, weak_ptr<T>> mt_;
(2)解决办法是:使用智能指针的定制析构功能,在析构 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;
(3)解决办法是:万能的 shared_ptr
,将 StockFactory
继承enable_shared_from_this
,bind
绑定 shared_from_this()
,可以保证调用 deleteStock
时 StockFactory
还存活;但这种做法延长了 StockFactory
的生命期;
(4)弱回调:如果对象还活着就调用它的成员函数,否则就忽略;
判断对象活着的方法很容易想到是 weak_ptr
;
这种做法的好处是,不会延长对象的生命期;
pStock.reset(new Stock(key),
boost::bind(&StockFactory::weakDeleteCallback,
boost::weak_ptr<StockFactory>(shared_from_this()), _1));
只用非递归的 mutex (即不可重入的 mutex)
在使用迭代器遍历时修改 vector
,有两种做法:把修改推迟,等循环结束再修改;用 copy-on-write;
在调试时,可以加上在函数后 __attribute__((noinline))
,避免防止函数 inline
展开;
(1)broadcast 通常用于表示状态变化,signal 通常用于表示资源可用;
(2)读写锁中,reader lock 是可重入的,writer lock 是不可重入的;为防止 writer lock 饥饿,其通常会阻塞后来的 reader lock ,因此 reader lock 在重入时可能死锁;
(3)assert
在 release build 里是空语句
(4)单例模式下,指令重排序可能会导致 Double-Checked Locking 失效;
正常顺序为:分配空间,使用给定参数构造对象,将对象赋值给变量;
可能的顺序为:分配空间,将该空间地址赋值给指针,使用给定参数构造对象;
此时第 2 步中,其他线程可以判断出指针不为空,但只能看到拥有默认值的对象,可能会导致问题;这在 C++ 中是合法的,可以通过加入显式内存屏障阻止重新排序;
使用了 pthread_once
C++11 可以使用局部静态变量,其初始化是线程安全的;
(5)2.8 节练习题
错误一直接修改了全局变量,可能会导致 traverse
中迭代器失效情况;错误二、三都可能发生丢失更新的情况,即两个线程同时拿到了原先的 oldFoos,同时向里面加入对象;