Muduo 网络库笔记

27 Sep 2022 | net, notes, wip

线程安全的对象生命期管理

对象在构造期间不要泄露this指针,即:

因此应该采用二段式构造。

shared_ptr/weak_ptr的计数在主流平台上是原子操作,线程安全级别与内建类型、string和STL容器一样,即:

如果智能指针是对象x的数据成员,而它的模板参数T是个incomplete类型,那么x的析构函数不能是默认的或内联的,必须在.cpp文件里边显式定义,否则会有编译错或运行错。

shared_ptr技术与陷阱

另一个出错的地方是,std::bind会把实参拷贝一份,如果参数是个shared_ptr,那么对象的生命期就不会短于function对象:

class Foo { void doit(); };
shared_ptr<Foo> pFoo(new Foo);
function<void()> func = bind(&Foo::doit, pFoo);

线程同步精要

条件变量使用方式,对于wait端:

  1. 与mutex一起使用,该布尔表达式的读写需受此mutex保护
  2. 把mutex上锁的时候才能调用wait()
  3. 把判断布尔条件和wait()放到while循环中,防止虚假唤醒
muduo::MutexLock mutex;
muduo::Condition cond(mutex);
std::deque<int> queue;
int dequeue() {
    MutexLockGuard lock(mutex);
    while (queue.empty()) 
        cond.wait();
    int top = queue.front();
    queue.pop_front();
    return top;
}

void enqueue(int x) {
    MutexLockGuard lock(mutex);
    queue.push_back(x);
    cond.notify();
}

使用shared_ptr实现copy on write

typedef std::vector<Foo> FooList;
typedef std::shared_ptr<FooList> FooListPtr;
MutexLock mutex;
FooListPtr g_foos;
void traverse() {
    FooListPtr foos;
    {
        MutexLockGuard lock(mutex);
        foos = g_foos;
    }
    for (auto it = foos->begin(); it != foos->end(); ++it) {
        it->doit();
    }
}
void post(const Foo& f) {
    MutexLockGuard lock(mutex);
    if (!g_foos.unique()) 
        g_foos.reset(new FooList(*g_foos));
    g_foos->push_back(f);
}

多线程服务器的适用场合和常用编程模型

Reactor模式,基于事件驱动的编程模型有本质的缺点,要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑。

必须使用单线程的场合:

  1. 程序可能会fork fork一般不能再多线程程序中调用,因为Linux的fork只克隆当前线程的thread of control,不克隆其他线程。fork之后,除了当前线程之外,其他线程都消失了,这样可能造成死锁。唯一安全的做法是fork之后立即调用exec执行另一个程序。
  2. 限制程序的CPU占用率 一个单线程程序即使busy-wait,也只占用多核服务器的1个core。因此对于一些辅助性的程序,比如监控其他进程状态,做成单线程的能够避免过分抢夺系统的计算资源。

是否选择多线程的场合 值得看看

Linux能同时启动几个线程?对于32位机器,一个进程的地址空间是4G,其中用户态能访问3G左右,而一个线程的默认栈大小是10M,因此一个进程大约最多能同时启动300个线程。

C++多线程系统编程精要

__thread只能修饰POD类型,不能修饰class类型,因为无法自动调用构造和析构函数。__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量。另外,__thread变量的初始化只能用编译期常量。

多线程程序中,不要使用signal。在服务器程序中,除了需要忽略SIGPIPE,其他都用默认语义。没有别的替代方法下,比如说需要处理SIGCHLD信号,把异步信号转换为同步的文件描述符事件。传统的做法是在signal handler里往一个特定的pipe写一个字节,在主程序中从这个pipe读取,从而纳入统一的IO事件处理框架。现代Linux的做法是采用signalfd把信号直接转换成文件描述符事件。

muduo网络库简介

  1. 主动关闭连接,如何保证对方已经收到全部数据?如果应用层有缓冲(这在非阻塞网络编程中是必需的),如何保证先发送完缓冲区中的数据,再断开连接
  2. 主动发起连接,对方主动拒绝,如何定期(带back-off地)重试?
  3. 非阻塞网络编程应该用边沿触发还是电平触发?电平触发什么时候关注EPOLLOUT,会不会造成busy-loop?如果边沿触发,如何防止漏读造成饥饿?epoll一定比poll快吗?
  4. 非阻塞网络编程,为什么要使用应用层发送缓冲区?接受缓冲区? 如果OS发送缓冲区空间不够,你等待OS缓冲区可用就会阻塞当前进程,因为不知道对方什么时候读取数据。因此很自然地就想到把数据缓存起来,等到socket可写再发送数据。而且有应用层缓冲区,能防止发送数据的顺序被打乱。 接受缓冲区是因为一次读到的数据可能不够一个完整的数据包。
  5. 如何设计缓冲区?既能减少系统调用,一次读的数据越多越划算,又能减少内存占用。
  6. 发送缓冲区的接受方处理缓慢,如何避免数据堆积,造成内存暴涨?如何做应用层的流量控制?
  7. 如何设计定时器,并使之与网络IO共用一个线程,以避免锁。

数独服务器并发模型的选择 值得看看 TODO


Older · View Archive (37)

编译器设计

Newer

ants 源码分析