muduo知识点

tiny的web server

好好看的github

C++中的RAII机制

RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。

RAII包装文件描述符

Linux文件描述符在程序刚启动时是0,1,2,此后不断加1,会造成串话(单线程中通过全局表,到多线程的话这样做需要加锁,不高效)。
多线程通过RAII解决,用Socket对象包装文件描述符,所有对此文件描述符的读写操作都通过此对象进行,对象的析构函数关闭文件操作符。这样只要对象活着不会有其他Socket对象和他有一样的文件操作符。剩下的就是做好多线程对象的生命期管理。

非阻塞网络:某个tcp连接A收到一个request,程序处理这个request可能需要一段时间,为了避免阻塞处理其他socket,程序记录发来request的TCP连接A,在某个线程池处理请求,处理完成后把reponse返回给tcp连接A。 但是如果中间过程中客户端断开了A,另一个客户端刚好建立了B(恰好等于之前的文件描述符),会出错。因此不应该只记住TCP连接A的文件描述符,而应该持有封装socket连接的TcpConnection对象,保证在处理期间Tcp连接A的文件描述符不会被关闭;或者持有TcpConnection对象的弱引用

RAII与fork()

fork以后子进程几乎继承了父进程的所有状态,包括地址空间和文件描述符,用于管理动态内存的文件描述符的RAII Class都能工作,但是不会继承父进程的内存锁,文件锁,某些定时器。

多线程与fork

多线程与fork协作性很差,一般不能在多线程使用fork.原因是fork只克隆当前线程,不克隆其他线程,不能一下克隆出一个与父进程一样的多线程子程序。fork只克隆当前线程,其他线程都死亡,如果其他线程处于临界区之内,持有某个锁,突然死亡再也没有机会解锁,如果子进程对同一个mutex加锁,会死锁。
唯一安全做法是fork之后立即调用exec()执行另一个程序,隔断子进程与父进程的联系。

Rector模式

muduo p61
non-blocking IO + IO multiplexing
Rector模型中,程序基本结构是一个事件循环、以事件驱动和事件回调实现业务逻辑。

优点:效率高,编写简单,对于IO密集应用是个不错选择。缺点:要求事件回调函数必须是非阻塞的,容易割裂业务逻辑。

Rector相当于是epoll的抽象,epoll过于底层。
每个已经连接的套接字描述符就是一个事件源
每一个套接字接收到数据后的进一步处理操作作为一个事件处理器
我们将需要被处理的事件处理源及其事件处理器注册到一个类似于epoll的事件分离器中。事件分离器负责等待事件发生。一旦某个事件发送,事件分离器就将该事件传递给该事件注册的对应的处理器,最后由处理器负责完成实际的读写工作。这种方式就是Reactor模式的事件处理方式。

在这里插入图片描述

  • 事件源(handle):由操作系统提供,用于识别每一个事件,如Socket描述符、文件描述符等。在服务端系统中用一个整数表示。该事件可能来自外部,如来自客户端的连接请求、数据等。也可能来自内部,如定时器事件。
  • 事件反应器(reactor):定义和应用程序控制事件调度,以及应用程序注册、删除事件处理器和相关描述符相关的接口。它是事件处理器的调度核心,使用事件分离器来等待事件的发生。一旦事件发生,反应器先是分离每个事件,然后调度具体事件的事件处理器中的回调函数处理事件
  • 事件分离器(demultiplexer):是一个有操作系统提供的I/O复用函数,在此我们选用epoll。用来等待一个或多个事件的发生。调用者将会被阻塞,直到分离器分离的描述符集上有事件发生。
  • 事件处理器(even handler):事件处理程序提供了一组接口,每个接口对应了一种类型的事件,供reactor在相应的事件发生时调用,执行相应的事件处理。一般每个具体的事件处理器总是会绑定一个有效的描述符句柄,用来识别事件和服务。
    未完待续

rector分类

参考博客

  • Reactor单线程模型:这个模型和上面的NIO流程很类似,只是将消息相关处理独立到了Handler中去了!
    虽然上面说到NIO一个线程就可以支持所有的IO处理。但是瓶颈也是显而易见的!我们看一个客户端的情况,如果这个客户端多次进行请求,如果在Handler中的处理速度较慢,那么后续的客户端请求都会被积压,导致响应变慢!所以引入了Reactor多线程模型!
  • Reactor多线程模型:Reactor多线程模型就是将Handler中的IO操作和非IO操作分开,操作IO的线程称为IO线程,非IO操作的线程称为工作线程!这样的话,客户端的请求会直接被丢到线程池中,客户端发送请求就不会堵塞!
    但是当用户进一步增加的时候,Reactor会出现瓶颈**!因为Reactor既要处理IO操作请求,又要响应连接请求!为了分担Reactor的负担,所以引入了主从Reactor模型!**
  • 主Reactor用于响应连接请求,从Reactor用于处理IO操作请求!

one loop per thread

p63
在Rector模型下,每个IO线程都有一个event loop(也叫Reactor),用于处理读写和定时事件。
好处:
线程数目固定,可以在程序启动时设置,不会频繁的创建与销毁
可以方便的在线程之间调配负载
IO事件发生的线程是固定的,同一个TCP连接不用考虑并发。
Eventloop代表子线程的主循环,需要哪个线程干活就把timer或IO channel(如tcp连接)注册到哪个线程的loop即可。

线程池

对于没有IO光有计算任务的线程,使用event loop有点浪费,使用blocking queue实现的任务队列

BlockingQueue

进程通信

匿名管道、具名管道、消息队列、共享内存、信号等方式。
TCP sockets和pipe都是文件操作符,但是pipe是单向的,比较受限制,但是有一个经典使用:
写Rector/event loop 时用来异步唤醒select(或者epoll)调用。

运行多个单线程的进程:a. 多次运行单线程的进程, b. 主进程+work进程。
线程间有共享数据,并且共享数据可以修改才可用于多线程程序,不然b也可

多线程退出

安全退出多线程程序并不容易,涉及安全退出其他正在运行的线程,需要精心设计共享对象的析构顺序,防止各个线程在退出时访问已失效的对象。编写长期运行的多线程服务程序时,可以不必追求安全地退出,而是让进程处于拒绝服务状态,然后可以杀死。

多线程与IO

网络IO

如何处理IO:
始终让同一线程操作此socket
多个线程能否同时读写一个socket文件描述符:
不可以
(1)一个线程在阻塞的read/accept,另一个线程close此socket;
(2)只考虑读和写,socket读写不保证完整性,两个线程同时读,有可能同时收到部分数据,无法拼接;写的话只发出半条消息,接收方无法处理;即使给每个线程write加一个锁,还不如直接始终让同一线程操作此socket
多个线程同时处理一个socket会提高效率吗
不会,见上方

磁盘IO

多线程可以加速磁盘io吗?
不会,首先要考虑竞态条件,多线程不能优化;此外,多个线程同时read write同一个磁盘上文件也不一定提速。因为每块磁盘有一个操作队列,多线程读写请求到了内核要排队执行(只有说磁盘缓存绝大多数据多线程才有可能比单线程快)

磁盘io的正确方法:一个文件只有一个进程中的一个线程来读写。
多线程程序遵循原则:每个文件描述符只有一个线程操作,从而解决消息收发的顺序性问题,避免了关闭文件描述符的竞态条件。一个线程可以操作多个文件描述符,但是一个线程不能操作别的线程拥有的文件描述符。

例外:UDP协议本身保证消息的原子性,在适当条件下可以多个线程同时读写同一个UDP

Linux使用定时器timerfd 和 eventfd接口实现进程线程通信

详细说明
Signalfd、timerfd、eventfd三种新的fd使用说明
简短说明
signalfd
传统的处理信号的方式是注册信号处理函数;由于信号是异步发生的,要解决数据的并发访问,可重入问题。signalfd可以将信号抽象为一个文件描述符,当有信号发生时可以对其read,这样可以将信号的监听放到select、poll、epoll等监听队列中。

timerfd
可以实现定时器的功能,将定时器抽象为文件描述符,当定时器到期时可以对其read,这样也可以放到监听队列的主循环中。

eventfd
实现了线程之间事件通知的方式,也可以用于用户态和内核通信。eventfd的缓冲区大小是sizeof(uint64_t);向其write可以递增这个计数器,read操作可以读取,并进行清零;eventfd也可以放到监听队列中,当计数器不是0时,有可读事件发生,可以进行读取。

三种新的fd都可以进行监听,当有事件触发时,有可读事件发生。

epoll水平触发/边沿触发

水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态,没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发.

边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述符.信号驱动式IO就属于边缘触发.

一个管道收到了1kb的数据,epoll会立即返回,此时读了512字节数据,然后再次调用epoll.这时如果是水平触发的,epoll会立即返回,因为有数据准备好了.如果是边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).所以当我们写epoll网络模型时,如果我们用水平触发不用担心数据有没有读完因为下次epoll返回时,没有读完的socket依然会被返回,但是要注意这种模式下的写事件,因为是水平触发,每次socket可写时epoll都会返回,当我们写的数据包过大时,一次写不完,要多次才能写完或者每次socket写都写一个很小的数据包时,每次写都会被epoll检测到,因此长期关注socket写事件会无故cpu消耗过大甚至导致cpu跑满,所以在水平触发模式下我们一般不关注socket可写事件而是通过调用socket write或者send api函数来写socket,说到这我们可以看到这种模式在效率上是没有边缘触发高的,因为每个socket读或者写可能被返回两次甚至多次,所以有时候我们也会用到边缘触发但是这种模式下在读数据的时候一定要注意,因为如果一次可写事件我们没有把数据读完,如果没有读完,在socket没有新的数据可读时epoll就不回返回了,只有在新的数据到来时,我们才能读取到上次没有读完的数据。

多线程同步的锁 死锁

互斥锁先看这个
再看这个
死锁主要发生在有多个依赖锁存在时, 会在一个线程试图以与另一个线程相反顺序锁住互斥量时发生. 如何避免死锁是使用互斥量应该格外注意的东西。
  总体来讲, 有几个不成文的基本原则:
对共享资源操作前一定要获得锁。
完成操作以后一定要释放锁。
尽量短时间地占用锁。
如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
线程错误返回时应该释放它所获得的锁。

定时器

一、定时器概述,应用场景
场景一:keep alive保活机制
成千上万个客户端去连接一台聊天服务器,那么就会存在成千上万个tcp连接。但是这些tcp连接是每时每刻都保持发包收包的活跃状态吗?不是!

某些tcp连接上,可能建立之后,在一天之内就没再发包/收包过,为了把有限的系统资源分配给更活跃的用户使用,我们应该设计一种方案来踢掉空闲连接。

场景二:游戏中,指定时间(或间隔时间)执行某种特定操作

  1. 每日/每周/每月,特定时间执行一次操作

  2. 循环执行的定时器,比如每隔一分钟刷新一次野怪

3.只执行一次的定时器,在60秒后定时器超期,执行操作A

定时器通常包含至少两个成员:一个超时时间(通常采用相对时间或者超时时间)和一个超时时间到达后的一个回调函数。有时候还可能包含回调函数被执行时需要传入的参数
通常定时器结构如下
class stTimer
{
public:
time_t expire;/任务的超时时间,此处使用绝对时间/
void* user_data;/回调函数传入的参数/
void (cb_func)(void arg);/超时回调函数/
};

定时器主流实现方式以及优缺点分析:

list + 线程超时检测

list<stTimer> timerList;
需要开启一个单独的线程来检测list容器中元素是否超时
void thread_func(void* arg)
{
    stTimer* pTimer =  (stTimer*)arg;
    //遍历list容器中全部元素
    //如果当前时间大于容器中的超时时间,则归为超时
    if(currentTime > expire)
    {
        //调用超时回调函数cb_func
    }
}

优点:代码实现简单,适合在定时器数量少(比如100以内)的场景,此时遍历list的开销比较小
缺点:如定时器数量太多,则效率很低。

timerfd+epoll IO复用函数

timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,因此可以配合select/poll/epoll等使用。也就是说当定时器超时的时刻,会触发该timerfd文件描述符的可读事件,而可读事件可以和网络模型相结合来配套使用

int timerfd_create(int clockid, int flags)
timerfd_create()函数创建一个定时器对象,同时返回一个与之关联的文件描述符。
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);

此函数用于设置新的超时时间,并开始计时,能够启动和停止定时器;
int timerfd_gettime(int fd, struct itimerspec *curr_value);
//timerfd_gettime()函数获取距离下次超时剩余的时间

enable_shared_from_this

enable_shared_from_this是一个模板类,定义于头文件,其原型为:

template< class T > class enable_shared_from_this;

std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, … ) ,它们与 pt 共享对象 t 的所有权。
当类A被share_ptr管理,且在类A的成员函数里需要把当前类对象作为参数传给其他函数时,就需要传递一个指向自身的share_ptr。
参考

优先队列

首先要包含头文件#include, 他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队。

优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。

定义:priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。

当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆。

参考

bind function, bind绑定生成目标函数,function相当于函数指针

很好的一个链接

可调用对象

可调用对象有一下几种定义:

是一个函数指针,参考 C++ 函数指针和函数类型;
是一个具有operator()成员函数的类的对象;
可被转换成函数指针的类对象;
一个类成员函数指针;
C++中可调用对象的虽然都有一个比较统一的操作形式,但是定义方法五花八门,这样就导致使用统一的方式保存可调用对象或者传递可调用对象时,会十分繁琐。C++11中提供了std::function和std::bind统一了可调用对象的各种操作。

bind

可将std::bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:

  • 将可调用对象和其参数绑定成一个防函数;
  • 只绑定部分参数,减少可调用对象传入的参数。

std::bind 主要用于绑定生成目标函数,一般用于生成的回调函数

std::bind绑定普通函数

double my_divide (double x, double y) {return x/y;}
auto fn_half = std::bind (my_divide,_1,2);  
std::cout << fn_half(10) << '\n';                        // 5
  • bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针(类成员函数不可以,所以需要)。因此std::bind (my_divide,_1,2)等价于std::bind (&my_divide,_1,2);
  • _1表示占位符,位于中,std::placeholders::_1;
    std::bind绑定一个成员函数
struct Foo {
    void print_sum(int n1, int n2)
    {
        std::cout << n1+n2 << '\n';
    }
    int data = 10;
};
int main() 
{
    Foo foo;
    auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);
    f(5); // 100
}
  • bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
  • 必须显示的指定&Foo::print_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Foo::print_sum前添加&;
  • 使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &foo

function

std::function 是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。
定义格式:std::function<函数类型>。
std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。

std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、bind表达式、函数指针以及其它函数对象。std::function对象是对C++中现有的可调用实体的一种类型安全的包装(我们知道像函数指针这类可调用实体,是类型不安全的)。
最简单的理解就是:
通过std::function对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的封装,形成一个新的可调用的std::function对象;让我们不再纠结那么多的可调用实体。  例如下面代码分别对函数和成员函数保存,bind和成员函数进行保存。 成员函数保存需要加地址。


struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};
 
void print_num(int i)
{
    std::cout << i << '\n';
}
 // store a free function
std::function<void(int)> f_display = print_num;


    // store the result of a call to std::bind
std::function<void()> f_display_31337 = std::bind(print_num, 31337);

 // store a call to a member function
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;

epoll EPOLLONESHOT 事件

参考链接

参考链接

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值