目录
在bind中采用值绑定时,会创建值的副本,进行绑定后可能会发生以下后果:
今天找BUG+调试真的整得抑郁了快,原本以为简单得不得了的,绝对不可能出错的东西,就这么困扰了我大半天。现在是不到凌晨1点,代码也不想敲了,就写篇“反思录”记录博主今天踩到的坑。且听博主细细道来......
故事源于使用std::bind进行参数绑定,需要绑定的函数如下,由于是测试代码,比较简单,但是很能反映问题!!!
这里博主对socket做了封装,socket中包含一个当前套接字的文件描述符。Poller类则是对epoll的封装,包含了epoll的一系列操作。
在这里,我们明确一下listen套接字EPOLLIN事件就绪的条件:首先服务端设置了监听套接字,此时还没调用accept函数,当客户端发起连接时,由于上层并没有调用accept接受连接,所以此时客户端的连接会被维持在下层的TCP全连接队列当中。而accept的作用就是将连接提取上来,并为其设置一个struct file文件(struct file结构体中有个void*指针指向描述套接字的结构体,这也就是为什么“一切皆文件”)指向套接字,由此返回一个描述连接的文件描述符。可见,对于listen套接字EPOLLIN事件的就绪条件就是:全连接队列中有连接!
相信只要大家写过epoll和Reactor模式就大致了解下面这段代码是在做什么,其实很简单:创建一个服务端套接字(监听套接字),将监听套接字的文件描述符挂载到epoll中,并设置其EPOLLIN事件回调为调用accept接收新连接,就是如此。。。简单。。。
void acceptor(Socket &sock, Poller *epoller)
{
int newfd = sock.Accept();
std::cout << newfd << std::endl;
// ......
}
int main()
{
Poller epoller;
Socket s;
s.CreateServer(8889); // 创建监听套接字
Channel channel(&epoller, s.Fd());
channel.EnableRead();
// 方式1 : 使用std::ref 正确
channel.SetReadCallback(std::bind(acceptor, std::ref(s), &epoller));
// 方式2 : 使用值绑定,bind创建了一个s的副本,错误
// channel.SetReadCallback(std::bind(acceptor, s, &epoller)); // 进行回调函数绑定
while(1)
{
std::vector<Channel*> actives;
epoller.Poll(&actives);
for(auto t : actives){
std::cout << t->Fd() << std::endl;
t->HandleEvent();
}
sleep(1);
std::cout << "sleep" << std::endl;
}
return 0;
}
方式 2 乍一看是不是没什么问题,参数都绑定好了,可偏偏它就是有问题!当使用方式2后,当客户端对服务端进行连接时发现服务端竟然拒绝连接!说明什么?说明监听套接字文件描述符的EPOLLIN回调函数根本就没有调用,也就是服务端压根就没启动accept函数!
哎呀,那你让客户端怎么连接嘛。。。一定是accept的问题吗,难道就不能是listen套接字的监听状态没有设置成功的原因吗?博主当前绑定的端口是8889,通过netstat命令查看了一下,端口8889上的listen套接字好像还真没被设置成功。。。
这下我也蒙了 ,但是设置监听套接字的代码部分一定是没问题的,这点博主测试过了。唯一的问题,可能还是bind进行值绑定的连锁反应。因为当把bind给注释掉之后,端口8889的监听套接字的监听状态又成功设置了。
后来博主分别作了如下改变:1、将acceptor的第一个参数改为int fd,进行套接字文件描述符的绑定,成功了; 2、将acceptor的第一个参数改为Socket * 指针类型,进行地址的绑定,也成功了3、最后就是方式1,在bind中使用std::ref()告诉bind我要进行引用绑定,也成功了。。。
这下客户端也能连接上了,服务端也能接收到连接了,Reactor也能正常工作了。虽然还是不知道为什么,但这次的教训让我注意到了以后绑定引用的时候最好使用std::ref。
那为何需要绑定的参数是引用时,在上述代码中使用值绑定会出现问题呢?
其实这个问题跟bind并没有太大的关系,但也有一点小关系,主要还是Socket类的析构函数,在析构函数中我调用了close关闭连接。bind函数本质是创建了一个_Bind的类,对于传入的回调函数和其参数进行保存,而如果是传值传参的话,那么其实就是生成了一份Socket对象的拷贝,但由于执行函数时离开了main函数的作用域,这份拷贝会在后续调用传参中就会被释放掉,从而使得触发Socket类的析构函数,但函数内部调用close将连接释放了,监听套接字被关闭了,所以客户端连接会失败。
所以在对自定义类对象进行绑定时,我们最好使用指针、shared_ptr,或者std::ref/std::cref来修饰绑定对象。如此,便可使被绑定对象的生命周期与预期一致,且不会出现直接拷贝对象副本的情况。
在bind中采用值绑定时,会创建值的副本,进行绑定后可能会发生以下后果:
1. 值的不变性
- 现象:回调函数中使用的值是绑定时的值,而不是调用时的值。
- 示例:
#include <iostream> #include <functional> void foo(int x) { std::cout << "x: " << x << std::endl; } int main() { int a = 5; auto func = std::bind(foo, a); // 绑定值 a = 10; // 修改 a func(); // 输出 x: 5,而不是 10 return 0; }
- 结果:即使
a
在绑定时被修改为 10,回调函数中仍然输出 5,因为std::bind
复制了a
的值。
2. 对象的深拷贝与浅拷贝问题
- 现象:如果被绑定的对象包含资源(如指针、文件描述符等),复制对象可能会导致资源的深拷贝或浅拷贝问题。
- 深拷贝:如果对象实现了深拷贝(即复制资源而不是共享资源),可能会有不必要的资源占用或性能开销。
- 浅拷贝:如果对象没有实现深拷贝,可能会导致共享资源时的竞态条件或双重释放问题。
- 示例:
class Resource { public: Resource() : data(new int(0)) {} ~Resource() { delete data; } // 假设没有实现深拷贝 Resource(const Resource &other) : data(other.data) {} Resource &operator=(const Resource &other) { data = other.data; return *this; } int *data; }; void bar(Resource r) { *r.data = 42; // 修改资源 } int main() { Resource r1; auto func = std::bind(bar, r1); // 绑定值 func(); // 修改 r1 的资源 std::cout << *r1.data << std::endl; // 这里可能会输出 42,也可能导致未定义行为 return 0; }
- 结果:如果
Resource
类没有正确实现深拷贝,r1
和回调函数中的副本会共享同一个资源,导致未定义行为。
3. 回调时访问了已被销毁的对象
- 现象:如果被绑定的对象的生命周期在回调函数执行前结束,副本对象仍然存在,但可能访问到无效的状态或资源。
- 示例:
#include <iostream> #include <functional> void foo(int x) { std::cout << "x: " << x << std::endl; } int main() { int *a = new int(5); auto func = std::bind(foo, *a); // 绑定值 delete a; // 删除 a func(); // 访问已被释放的内存,未定义行为 return 0; }
- 结果:回调函数中尝试访问已释放的内存,导致未定义行为,可能崩溃或输出错误值。
4. 无法修改原始对象
- 现象:如果需要在回调函数中修改原始对象的状态,但绑定的是副本,则无法实现。
- 示例:
#include <iostream> #include <functional> void increment(int &x) { ++x; } int main() { int a = 5; auto func = std::bind(increment, a); // 绑定值 func(); std::cout << a << std::endl; // 输出 5,而不是 6 return 0; }
- 结果:
a
的值没有被修改,因为回调函数修改的是副本。
5. 性能开销
- 现象:复制对象可能会导致不必要的性能开销,尤其是当对象较大或包含复杂资源时。
- 示例:
#include <vector> #include <functional> void process(const std::vector<int> &v) { // 处理向量 } int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; auto func = std::bind(process, vec); // 绑定值 // 如果有大量数据,复制操作会带来性能开销 func(); return 0; }
- 结果:如果
vec
包含大量数据,复制操作会带来额外的内存和时间开销。
6. 解决方法
- 使用引用绑定:通过
std::ref
或std::cref
来绑定引用,而不是复制值。#include <iostream> #include <functional> void increment(int &x) { ++x; } int main() { int a = 5; auto func = std::bind(increment, std::ref(a)); // 绑定引用 func(); std::cout << a << std::endl; // 输出 6 return 0; }
总结
- 值绑定会复制参数的值,导致回调函数中使用的是副本,可能无法访问原始对象的最新状态。
- 引用绑定通过
std::ref
或std::cref
包装参数,可以避免对象的复制,确保回调函数中使用的是原始对象。