多线程服务端编程 简记
简介
muduo
- 非阻塞IO
- 事件驱动
- one loop per thread
现代C++
- 资源管理
- 事件回调
线程安全的对象生命周期管理
线程安全(函数或类)与可重入(函数或类)
- 线程安全:当被多个并发线程反复调用时,它会一直产生正确的结果。
- 若想确保线程安全,则应对函数内访问的共享变量进行加锁。全局变量、局部静态变量、分配于堆的变量都是共享的
- 可重入:当程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理 函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处 理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的。
- 确保函数可重入:
- 不在函数内部使用静态或全局数据
- 不返回静态或全局数据,所有数据都由函数的调用者提供
- 确保函数可重入:
可重入的函数一定是线程安全的,但反过来不一定成立
C++如何保证线程安全
线程安全的对象创建:
保证在构造期间不要泄漏this指针,目的是防止对象还没初始化完就被另一线程访问。
即一是不要在构造函数中注册任何回调;二是不要在构造函数中把this传给跨线程的对象
线程安全的对象析构:
C++在构造函数中申请资源并初始化,在析构函数中释放资源。在多线程环境下如何保证在执行成员函数期间,对象不会在另一个线程中被析构?
解决类似的竞争条件是C++多线程编程面临的基本问题,可使用shared_ptr一劳永逸地解决这些问题
通过指针来访问对象时,是无法判断当前指针指向的是一个有效对象而不是一个已经析构了的对象。所以仅仅靠原始指针是无法判断的,它不携带任何信息,于是出来了智能指针
- shared_ptr:控制对象的生命周期
- weak_ptr:不控制对象的生命周期,但可以知道对象是否存活
使用shared_ptr还是weak_ptr应取决于是否应该由当前环境来控制对象的生命周期;另外weak_ptr可用来解决循环引用问题,通常父类保存子类的shared_ptr,子类保存父类的weak_ptr
所以多线程中推荐使用shared_ptr来定义对象;C++一个重要特性就是RAII,故在类中,可将类对象使用shared_ptr获取而不是原始指针,这样在析构中就不用delete了
因为unique_ptr同一时刻只能有一个拥有者,即不可能同时被多个线程访问,所以不存在线程安全问题
附:智能指针本身都是值,而不是指针(可以用指针,但这就失去了使用智能指针的本意);智能指针的引用计数是原子操作
线程同步
原则: 首先选用封装的高级构件,如TaskQueue、Producer-Consumer Queue;其次再考虑非递归互斥锁(非可重入)和条件变量或shared_ptr
使用互斥锁时,不使用原生的mutex,即不自己进行lock和unlock,而是使用封装了的lock_guard,即满足RAII的特性
使用条件变量时,必须与互斥锁一起使用,因为对条件的读需要互斥保护,当判断完进行wait的时候,条件变量会将互斥锁解开,wait执行完后会再重新加上锁;一定要用while来进行条件判断,避免虚假唤醒(多线程的调度问题)
使用shared_ptr来管理共享数据(如配置文件等)

相当于加锁,读写时的流程如下(其中资源已经被一个shared_ptr管理,所以无读写时引用计数为1):
- 读:通过一个临时shared_ptr指向资源,通过此临时shared_ptr访问数据,此时计数大于1
- 写:若计数为1,则直接修改;若计数为2,则拷贝一份修改,再swap(代替赋值)到old_ptr
注:shared_ptr计数加减的地方要加锁保护
多线程服务器常用编程模型
其他
C++内存问题
- 野指针(访问时)
- 缓冲区溢出(使用时)
- 重复释放(多次删除)
- 内存泄漏(忘记删除)
- 内存碎片
std::function && std::bind
std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。
std::bind函数可被看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
bind绑定普通函数:
double my_divide (double x, double y) {return x/y;}
// 第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针
auto fn_half = std::bind (my_divide, _1, 2);
fn_half(10); // 5
bind绑定对象的成员函数:
class Foo {
public:
void print_sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
private:
int data = 10;
};
int main()
{
Foo foo;
// bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);
f(5); // 100
}
assert
assert在release build中是空语句
lock_guard && unique_lock
参考链接:https://www.cnblogs.com/xudong-bupt/p/9194394.html
懒汉单例模式
三种方式可实现懒汉式的单例模式:
- static
- DCL双重检测锁定(Double-Checked Locking)
- pthread_once
生产环境的代码中不要用sleep来轮询等待某事件发生
应该向 select、poll或epoll_wait中注册timer,在timer的回调函数里进行事件处理
因为使用sleep时,当前线程处于阻塞状态,此线程没有了处理其他事件的能力,降低了效率。