1. 需要注意:
scoped_ptr/shared_ptr/weak_ptr都是值语意,要么是栈上对象,或是其他对象的直接数据成员,或是标准容器里的元素。几乎不会有下面这种用法:
shared_ptr
还要注意,如果这几种智能指针是对象x的数据成员,而它的模板参数T是个incomplete类型,那么x的析构函数不能是默认的或内联的,必须在.cpp文件里显示定义,否则会有编译错误或运行错误。
2. 因为要修改引用计数,shared_ptr的拷贝开销比拷贝原始指针要高,但是需要拷贝的时候并不多。多数情况下它可以以const reference方式传递
void save(const shared_ptr& pFoo); // pass by const reference
void validateAccount(const Foo& foo);
bool validate(const shared_ptr& pFoo) // pass by const reference
{
validateAccount(*pFoo);
}
void onMessage(const string& msg) {
shared_ptr pFoo(new Foo(msg));// 只要在最外层持有一个实体,完全不成问题
if (validate(pFoo)) { // 没有拷贝pFoo
save(pFoo); // 没有拷贝pFoo
}
}
3. shared_ptr是管理共享资源的利器,需要注意避免循环引用,通常的做法是owner持有指向child的shared_ptr, child持有指向owner的weak_ptr。
shared_ptr StockFactory::get(const string& key) {
shared_ptr pStock;
MutexLockGuard lock(mutex_);
weak_ptr& wkStock = stocks_[key];
pStock = wkStock.lock();
if (!pStock) {
pStock.reset(new Stock(key));
wkStock = pStock;
}
return pStock;
}
// 注:stocks_[key] 如果key不存在,会默认构造一个,所以会只增不减
解决的办法:利用shared_ptr的定制析构功能。shared_ptr的构造函数可以有一个额外的模板类型参数,传入一个函数指针d,在析构对象时执行d。
template
shared_ptr::shared_ptr(Y* p, D d);
template
void shared_ptr::reset(Y* p, D d);
class StockFactory:boost::noncopyable
{
// 在get()中,将pStock.reset(new Stock(key)); 改为:
// pStock.reset(new Stock(key),
// boost::bind(&StockFactory::deleteStock, this));
private:
void deleteStock(Stock* stock) {
if (stock) {
MutexLockGuard lock(mutex_);
stocks_.erase(stock->key());
}
delete stock;
}
};
不过这里还会有个问题,就是把StockFactory this指针保存在了boost::function里。如果StockFactory先于Stock对象析构,那么会core dump。
class StockFactory : public boost::enable_shared_from_this, boost::noncopyable
{/* ... */};
// 修改StockFactory::get中的pStock.reset
// pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, this, _1));
// to
pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, shared_from_this(), _1));
但是还存在一个问题,StockFactory的生命周期似乎被意外延长了
7. Observer模式的本质问题在于其面向对象的设计。换句话说,正是面向对象(OO)本身造成了Observer的缺点。Observer是基类,这带来了非常强的耦合,强度仅次于友元。这种耦合不仅限制了成员函数的名字,参数,返回值,还限制了成员函数所属的类型(必须是Observer的派生类)。
Observer class是基类,这意味着如果Foo想要观察两种类型的时间(比如时钟和温度),需要使用多继承。这还不是最糟糕的,如果要重复观察同一类型的事件(比如1秒的心跳和30秒一次的自检),就要用到一些伎俩来work around,因为不能从一个Base class继承两次。
现在的语言一般可以绕过Observer模式的限制,比如Java可以使用匿名内部类,Java 8用Closure, C#用delegate,C++用boost::function/boost::bind
8. mutex分为递归(recursive)和非递归(non-recursive)两种,这是POSIX的叫法,另外的名字是可重入(reentrant)与非可重入。它们的区别在于:同一个线程可以重复对recursive mutex加锁,但是不能重复对non-recursive mutex加锁。
9. 注意区分signal与broadcast:
broadcast 通常用于表明状态变化,signal通常用于表示资源可用
10. Singleton的线程安全。通过pthread_once:
template
class Singleton : boost::noncopyable
{
public:
static T& instance() {
pthread_once(&ponce_, &Singleton::init);
return *value_;
}
private:
Singleton();
~Singleton();
static void init() {
value_ = new T();
}
private:
static pthread_once_t ponce_;
static T* value_;
};
// 必须在头文件中定义static变量
template
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
template
T* Singleton::value_ = NULL;
// 调用
Foo& foo = Singleton::instance();
11. sleep(3)不是同步原语
12. 缩小临界区
MapPtr parseData(const string& message); // 解析收到的消息,返回新的MapPtr
void CustomerData::update(const string& message) {
// 解析新数据,在临界区之外
MapPtr newData = parseData(message);
if (newData) {
MutexLockGuard lock(mutex_);
data_.swap(newData); // 不要用data_ = newData;
}
// 旧数据的析构也在临界区外,进一步缩短了临界区
}
// 注:MapPtr是typedef shared_ptr
13. 推荐模式:
推荐的C++多线程服务器编程模式为:one (event) loop per thread + thread pool。
event loop (也叫IO loop)用作IO multiplexing,配合non-blocking IO和定时器
thread pool 用来做计算,具体可以是人物队列或生产者消费者队列
14. 多线程提高的是响应时间,而不是吞吐量
15. POSIX标准列出一份非线程安全的函数的黑名单,在这份黑名单中,system, getenv, putenv, setenv等等函数都是不安全的。
16. 通过gettid() 来获取当前线程的标识
17. 线程正常退出只有一种方式,即自然死亡,从外部强行终止线程的做法和想法都是错的。因为强行终止线程的话(无论是自杀:pthread_exit(), 还是他杀:pthread_cancel()),它都没有机会清理资源。也没有机会释放已经持有的锁,其他线程如果再想对同一个mutex加锁,那么就会立刻死锁。因此我认为不用去研究cancellation point这种“鸡肋”概念。见这里
18. exit(3) 在C++中不是线程安全的。exit()会析构全局对象,_exit(2)不会析构全局对象,但是也不会执行其他任何清理工作,比如flush标准输出。
19. __thread只能用于修士POD类型(Plain Old Data:C++与C兼容的类型),不能修士class类型。
__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量。另外,__thread变量的初始化只能用编译期常量。
__thread变量是每个线程都有一份独立实体,各个线程的变量值互不干扰。
__thread实现非常高效,比pthread_key_t快的多
20. 多线程中,尽可能不要使用fork。唯一安全的做法是在fork之后,立即调用exec()执行另一个程序,彻底隔断子进程与父进程的关系
21. 多线程中,使用signal的第一原则是“不要使用signal”:
(1)不用signal作为IPC的手段
(2)不使用基于signal的定时函数,包括alarm/ualarm/setitimer/timer_create、sleep/usleep等等。
(3)不主动处理异常信号,使用默认语义:结束进程。除了SIGPIPE
(4)通用的部分是在signal handler中往pipe中写字节,纳入到统一IO事件中。现代Linux的做法是采用signalfd(2)把信号直接转换为文件描述符事件。
22. 为什么要限制并发连接数
一方面:我们不希望服务器程序超载
二方面:因为filedescriptor是稀缺资源,如果filedescript耗尽,很棘手
23. Linux的计时函数
获取当前时间:
time(2) / time_t(秒)
ftime(3) / struct timeb(毫秒)
gettimeofday(2) / struct timeval(微秒)
clock_gettime(2) / struct timespec(纳秒)
使用gettimeofday(2)的原因:
(1) time(2)的精度太低,ftime(3)已废弃;clock_gettime(2)精度最高,但是其系统调用的开销比gettimeofday(2)大
(2) 在x86-64平台上,gettimeofday(2)不是系统调用,而是用户态实现的,没有上下文切换和陷入内核的开销
(3) 微秒的精度足够精确
定时函数:
sleep(3)
alarm(2)
usleep(3)
nanosleep(2)
clock_nanosleep(2)
getitimer(2)/setitimer(2)
timer_create(2)/timer_settime(2)/timer_gettime(2)/timer_delete(2)
timerfd_create(2)/timerfd_gettime(2)/timerfd_settime(2)
使用timerfd_*的原因:
(1) sleep(3)/alarm(2)/usleep(3)在实现时有可能用了SIGALARM信号,在多线程程序中处理信号是个想当麻烦的事情,应当尽量避免
(2) nanosleep(2)和clock_nanosleep(2)是线程安全的,但是在非阻塞网络编程中,绝对不能用线程挂起的方式来等待一段时间,这样程序会失去反应
(3) gettimer(2)和timer_create(2)也是用信号来deliver超时,在多线程中也会有麻烦
(4) timerfd_create(2)把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便地融入select(2)/poll(2)框架中
(5) 传统的reactor利用select(2)/poll(2)/epoll(4)的timeout来实现定时功能,但poll(2)和epoll_wait(2)的定时精度这有毫秒,远低于timerfd_settime(2)的定时精度。
24. 为什么TCP keepalive不能代替应用层心跳?
心跳除了说明应用程序还活着(进程还在,网络畅通),更重要的是表明应用程序还能正常工作。而TCP keepalive由操作系统负责探查,即便进程死锁或阻塞,操作系统也会如常收发TCP keepalive消息。对方无法得知这一异常。
心跳协议还有两个实现上的关键点:
(1)要在工作线程发送,不要单独起一个“心跳线程”
(2)与业务消息用同一个连接,不要单独用“心跳连接”
25. Google protobuf中的optional fields可以解决新旧协议的问题。
新版的服务端可以定义一些optional fields,根据请求中这些字段的存在与否来实施不同的行为。