1.使用智能指针进行C++
资源管理
(1). 使用智能指针:shared_ptr<T>
每个shared_ptr<T>
拥有一个其所管理的底层对象指针,及该对象的引用计数.在shared_ptr<T>
对象拷贝构造/赋值时,会对参与运算的对象的底层对象引用计数进行更新,正确反映对其引用的shared_ptr<T>
对象个数.引用计数变为0
时,底层对象被销毁.
注意点:
a. 一旦使用shared_ptr<T>
作为动态分配对象的生命期管理手段,就应在new T()
外的其他所有地方,全部使用shared_ptr<T>
作为对象访问的手段,不要使用对象原始指针.
b. 在使用shared_ptr<T>
调T
的成员函数内部,
如有基于调用的shared_ptr<T>
对象产生shared_ptr<T>
的需求,应该:
b.1. T
公共继承std::enable_shared_from_this<T>
b.2. 在需要基于调用shared_ptr<T>
对象产生shared_ptr<T>
处,使用shared_from_this()
.
shared_from_this()
将返回一个shared_ptr<T>
对象,该对象和调用基于的shared_ptr<T>
,拥有相同的底层对象及引用计数对象.
(2). 使用智能指针:weak_ptr<T>
一个weak_ptr<T>
只能基于一个shared_ptr<T>
对象来得到.
weak_ptr<T>
对象拥有与基于的shared_ptr<T>
对象,一样的底层对象及引用计数对象.
但特色是:
a. weak_ptr<T>
本身不能访问底层对象,如要通过weak_ptr<T>
访问底层对象,先通过lock
成员函数,返回shared_ptr<T>
,再通过返回的shared_ptr<T>
进行对底层对象的访问.
b. weak_ptr<T>
虽然拥有与基于的shared_ptr<T>
对象
一样的底层对象,引用计数对象,但基于shared_ptr<T>
得到weak_ptr<T>
对象过程中,不会导致引用计数对象+1
.
在一些你需要记录shared_ptr<T>
拥有的底层对象,以便后续访问.但同时,不想因此而导致底层对象因为你的记录,而无法销毁的场合,weak_ptr<T>
非常适合.weak_ptr<T>
后续使用时,可检查其包含的底层对象是否还有效,有效,则访问.无效,则不能访问.记录底层对象但不干涉对象的生命期管理是weak_ptr<T>
存在的意义.
(3). 使用智能指针:unique_ptr<T>
unique_ptr<T>
存在的意义在于,unique_ptr<T>
所关联的底层对象只能被一个unique_ptr<T>
对象所拥有.不存在,shared_ptr<T>
的多个shared_ptr<T>
对象共享同一底层对象,及引用计数对象因此大于1
的情况.
2.基于std::function
和std::bind
的回调
std::bind
接收回调函数,允许预先设置回调参数.返回std::function
.
std::function
的返回值和std::bind
绑定的函数的返回值类型一致.std::function
的参数列表由std::bind
绑定的函数的参数列表中,未预先设置的参数构成.
std::function
对象可向普通的类对象一样,执行拷贝构造/拷贝赋值.若std::function
通过std::bind
得到,且std::bind
绑定了实参,实参会随着std::function
的拷贝而被拷贝.
3.多线程编程
(1). 线程特定数据
a. 传统方式
pthread_once
让一个回调仅在首次执行到pthread_once
时被调用一次.在回调中通过pthread_key_create
得到一个供所有线程使用的key
,每个线程通过此key
结合pthread_getspecific/pthread_setspecific
,来获取或设置线程特定数据.
b. 新方式
__thread
修饰全局变量.
被修饰的变量,每个线程有自己独立的一份.每个线程通过同一变量名访问全局变量,实际访问的是线程各自的版本,多个线程间版本互不干扰.
__thread
对修饰对象要求:
b.1. 用于修饰POD
类型,不能修饰class
类型
b.2. 可用于修饰全局变量,函数内静态变量,不能修饰函数的局部变量或class
的普通成员变量
b.2. __thread
变量的初始化只能用编译期常量
(2). 获得线程id
#include <sys/syscall.h>
#define gettid() syscall(__NR_gettid)
或
pid_t gettid()
{
return static_cast<pid_t>(::syscall(SYS_gettid));
}
主线程id
和进程id
一致.多个线程id
彼此互不相同.
(3). 线程间互斥
通过线程间共享的互斥锁对象,每个线程对共享数据访问时,对共享的互斥锁对象上锁,访问结束,释放锁.可保证多线程间对共享数据的有序访问.
(4). 线程间同步
互斥锁+条件变量,通过多个线程共享的一个互斥锁+条件变量对象.每个线程对共享数据访问按以下模式.
// 等待者
加锁
while(操作条件检测不满足) {
释放锁+阻塞等待在条件变量
}
操作
释放锁
// 唤醒者
加锁
操作
借助条件变量,执行唤醒
释放锁
a. 阻塞在条件变量等待的线程,唤醒继续时,先获取互斥锁,再继续.
唤醒继续时,无法立即加锁,需阻塞等待,获取成功后,再继续.
b. 执行唤醒时,若此时无人等待.则啥也不做.后续产生的等待,需依赖后续的唤醒操作来唤醒.
4.Linux
下NonBlock I/O+I/O Multiplex
新一代的I/O Multiplex
机制:使用epoll
替代select/poll
a. 相对于select
和poll
来说,epoll
更加灵活,没有描述符限制。
b. epoll
的wait
直接返回发生的事件集合,避免select/poll
下对值-结果参数的低效轮询.
c. epoll
在注册描述符时,允许绑定自定义数据.
在返回事件时,自定义数据也返回.给予事件处理提供语境支持,利用语境,可增强事件处理功能/灵活性.
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
对epoll_ctl
:
第一个参数是epoll_create()
的返回值,第二个参数表示动作.用三个宏来表示:
EPOLL_CTL_ADD
:注册新的fd
到epfd
中;
EPOLL_CTL_MOD
:修改已经注册的fd
的监听事件;
EPOLL_CTL_DEL
:从epfd
中删除一个fd
;
第三个参数是需要监听的fd
,第四个参数是告诉内核需要监听什么事,
struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events
可以是以下几个宏的集合:
EPOLLIN
:表示对应的文件描述符可以读;
EPOLLOUT
:表示对应的文件描述符可以写;
EPOLLPRI
:表示对应的文件描述符有紧急的数据可读
EPOLLERR
:表示对应的文件描述符发生错误;
EPOLLHUP
:表示对应的文件描述符被挂断;快速获取对端关闭通知.
EPOLLET
:
将EPOLL
设为边缘触发(Edge Triggered
)模式, 这是相对于水平触发(Level Triggered
)来说的。
水平触发条件满足,每次执行epoll_wait
,对监控了可读的套接字,只要其接收缓存区存在数据或其他引起可读通知事件到达,就产生可读事件.对监控了可写的套接字,只要其发送缓存区存在多余空间可供继续写入或其他引起可写通知事件达到,就产生可写事件.
边沿触发下,每次执行epoll_wait
,对监控了可读的套接字,只在其接收缓存区从空变为非空后的首个epoll_wait
中会产生一次可读事件.对监控了可写的套接字,只在其发送缓存区从满变为非满后的首个epoll_wait
中会产生一次可写事件.
这样,事件通知次数少了.但每次可读事件通知时,要求我们必须持续读,直到返回EAGIN
(接收缓存区读取完毕).或遭遇错误.才能结束本次事件处理.否则,后续epoll_wait
中我们永远无法再次收到新的可读事件通知了.
同样的,每次可写事件通知时,要求我们必须持续执行异步发送写入.直到返回EAGIN
(表示发送缓存区满了).或遭遇错误.或连接对象的发送缓存区中数据已被全部写入后才可结束.此时,还会要求每次通过连接对象执行发送时,写直接通过send
写.在写入完毕,操作错误,或返回EAGIN
时,才可停止.返回EAGIN
后,剩余未立即发送数据,将在后续可写事件中通过异步写入方式完成写入处理.
EPOLLONESHOT
:
只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里.否则,就是注册一次描述符持续有效.后续可以对已经注册的描述符执行修改/删除,但无法执行添加.
对epoll_wait
,参数events
用来从内核得到事件的集合,maxevents
告之内核这个events
有多大,参数timeout
是超时时间.(毫秒,0
会立即返回,-1
将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0
表示已超时。
5.Muduo
对epoll
的恰到好处的使用
a. epoll
中注册的描述符都是非阻塞的.
这样有效避免,虚假唤醒下,事件处理时,阻塞在一处.而导致后续事件处理/事件监听因此而迟迟无法响应问题.通过搭配非阻塞描述符,可以始终保证每个事件的处理,在一个极短时间内完成.从而保持epoll
高响应时效性.
b. 多线程环境下,对epoll
的使用全部集中在一个线程.有效避免了,多线程同时使用epoll
各种可能的混乱.
这需要利用:
b.1. std::bind,std::function
回调函数及回调参数保存机制.
b.2. 在C++
成员函数调C
的API
,调用时,this
指向对象构成调用时语境.
为了实现高实时性的响应,利用了
a. epoll
处理的均是非阻塞描述符
b. epoll
监听的描述符中有一个专门用于及时唤醒
会在epoll
所在this
对象待处理回调集合非空时,被设置为可读.从而导致,若epoll
本来处于wait
时,立即由于事件存在而返回.
Muduo
如何实现这点:
a. 一个事件循环对象a
启动事件循环便独占一个线程t.
循环执行:
epoll
事件等待
事件处理
回调函数集合处理
b. 多个线程可获得事件循环a
的指针
获得a
指针的线程可以修改a
的回调函数集合,这样在a
的事件循环执行回调函数集合时,通过在线程t执行回调,而从而去与epoll
交互
c. 在事件处理中,由于epoll
事件允许绑定一个用户自定义数据
利用自定义数据也可实现,事件处理时,与epoll
的交互.
上述实现多线程下,对指定事件循环a
的epoll
始终是a
所在的线程,在与其交互.
在C++
成员函数中执行C
的API
调用的优势:
成员函数总是可以获得this
,this
指向一个对象,对象本身可存储成员变量
对象及其成员变量构成了我们调C
的API
时候的一个语境.这样在API
返回,或调用前传参的时候,我们可以基于调用发生时候的语境(this
对象内容).来获取我们需要的关联信息,作对应处理.
6.eventfd
eventfd
包含一个由内核维护的64
位无符号整型计数器,创建eventfd
时会返回一个文件描述符,进程可对这个文件描述符进行read/write
来读取/改变计数器的值,从而实现进程间通信。
int eventfd(unsigned int, int);
参数1
:创建eventfd
时它所对应的64
位计数器的初始值;
参数2
:eventfd
文件描述符的标志EFD_CLOEXEC/EFD_NONBLOCK/EFD_SEMAPHORE
。
EFD_CLOEXEC
,表示返回的eventfd
文件描述符在fork
后exec
其他程序时会自动关闭
EFD_NONBLOCK
,设置返回的eventfd
非阻塞;
EFD_SEMAPHORE
,表示将eventfd
作为一个信号量来使用。
a. 读eventfd
read
函数会从eventfd
对应的64
位计数器中,读取一个8
字节的整型变量;
read
函数设置的接收buf
的大小不能低于8
个字节,否则read
函数会出错,errno
为EINVAL
;
read
函数返回的值是按小端字节序的;
如果eventfd
设置了EFD_SEMAPHORE
,那么每次read
就会返回1
,并且让eventfd
对应的计数器减一;
如果eventfd
没有设置EFD_SEMAPHORE
,那么每次read
就会直接返回计数器中的数值,read
之后计数器就会置0
。
不管是哪一种,当计数器为0
时,如果继续read
,那么read
就会阻塞(如果eventfd
没有设置EFD_NONBLOCK
)或者返回EAGAIN
错误(如果eventfd
设置了EFD_NONBLOCK
)。
b. 写eventfd
在没有设置EFD_SEMAPHORE
的情况下,write
函数会将发送buf
中的数据写入到eventfd
对应的计数器中,最大只能写入0xffffffffffffffff
,否则返回EINVAL
错误;
在设置了EFD_SEMAPHORE
的情况下,write
函数相当于是向计数器中进行“添加”,比如说计数器中的值原本是2
,如果write
了一个3
,那么计数器中的值就变成了5
。如果某一次write
后,计数器中的值超过了0xfffffffffffffffe
.那么write
就会阻塞,直到另一个进程/线程从eventfd
中进行了read
(如果write
没有设置EFD_NONBLOCK
),或者返回EAGAIN
错误(如果write
设置了EFD_NONBLOCK
)。
7.万能对象
boost::any
使用举例
#include <iostream>
#include <list>
#include <boost/any.hpp>
typedef std::list<boost::any> list_any;
void fill_list(list_any& la)
{
la.push_back(10);
la.push_back(std::string("glemontree"));
}
void show_list(list_any& la)
{
list_any::iterator it;
boost::any anyone;
for (it = la.begin; it!= la.end(); ++it)
{
anyone = *it;
// C++中,typeid用于返回指针或引用所指对象的实际类型,
typeid是操作符,不是函数
if (anyone.type() == typeid(int))
{
std::cout << boost::any_cast<int>(*it) << std::endl;
}
else if (anyone.type() == typeid(std::string))
{
std::cout << boost::any_cast<std::string>(*it).c_str() << std::endl;
}
}
}
shared_ptr<void>
,一个shared_ptr<void>
可以基于任意T
下的shared_ptr<T>
对象,执行拷贝构造,拷贝赋值.
拷贝构造/拷贝赋值发生时,依然可以,正确处理引用计数更新,指向同一底层对象
shared_ptr<void>
析构时,可以正确处理引用计数更新,及引用计数为0
时,对底层对象的析构处理.
8.用描述符事件模型处理定时器
timerfd
是Linux
为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,因此可以配合select/poll/epoll
等使用。
int timerfd_create(int clockid, int flags);
clockid
:
CLOCK_REALTIME
,系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0
开始计时,中间时刻如果系统时间被用户改成其他,则对应的时间相应改变.
CLOCK_MONOTONIC
,从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
flags
:
TFD_NONBLOCK
(非阻塞模式),TFD_CLOEXEC
(表示当程序执行exec
函数时本fd
将被系统自动关闭,表示不传递)
struct timespec
{
time_t tv_sec;
long tv_nsec;
};
struct itimerspec
{
struct timespec it_interval;
struct timespec it_value;
};
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
设置新的超时时间,并开始计时,能够启动和停止定时器;
fd
: 参数fd
是timerfd_create
函数返回的文件句柄
flags
:为1
代表设置的是绝对时间(TFD_TIMER_ABSTIME
表示绝对定时器);为0
代表相对时间。
new_value
: 参数new_value
指定定时器的超时时间以及超时间隔时间
old_value
: 如果old_value
不为NULL
,old_vlaue
返回之前定时器设置的超时时间,it_interval
不为0
则表示是周期性定时器。it_value
和it_interval
都为0
表示停止定时器.
int timerfd_gettime(int fd, struct itimerspec *curr_value);
timerfd_gettime()
函数获取距离下次超时剩余的时间,curr_value.it_value
字段表示距离下次超时的时间,如果该值为0
,表示计时器已经解除,该字段表示的值永远是一个相对值,无论TFD_TIMER_ABSTIME
是否被设置.curr_value.it_interval
定时器间隔时间.
uint64_t exp = 0;
read(fd, &exp, sizeof(uint64_t));
可以用read
函数读取计时器的超时次数,该值是一个8
字节无符号的长整型