记一次回调函数为空引起的🦋风暴
尝试在“新建连接后,把连接交给SubReactor负责”这一环节写一个新的调度算法,而非取余后随机调度,过程中遇到了以下问题。
1.epoll wait
解决方案:根据返回的信号判断是否重新调用。有的错误并不会影响程序运行,开发时虽然都要抛出异常,但其实可以等等再重连。
代码修改为:
std::vector<Channel*> Epoll::poll(int timeout){
std::vector<Channel*> activeChannels;
int nfds;
while (true) {
nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
if (nfds == -1) {
if (errno == EINTR) {
// 系统调用被信号中断,重新调用 epoll_wait
continue;
}
errif(true, "epoll wait error");
break;
}
break;
}
}
2.直接用std::function<void()>的话要用bind,且没有返回值。
这和回调函数无关,不过涉及到C++11的特性,要记录一下。
解决方案:用完美转发和智能指针。
//不能放在cpp文件,C++编译器不支持模版的分离编译
template<class F, class... Args>
auto ThreadPool::add(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared< std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(tasks_mtx);
// don't allow enqueueing after stopping the pool
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
cv.notify_one();
return res;
}
3.调试中途不断make clean,再不断地make,gdb ./server 要是run不了,还得用netstat去杀死占用端口的进程,太麻烦了
解决方案:搞一个简单的bash脚本,我就快乐了。
5.围绕着std::bad_funciton_callback出现的一堆问题
前置背景:原本的调度算法是随机数取模,随机挑一个SubReactor来处理该连接。想说要有点创新,写了个大小上限为100的队列,储存每个EventLoop处理每个channel的操作时间,同时维护总时间和队列大小,以求出每个EventLoop的平均处理时间。综合平均处理时间+现在连接的channel个数,简单加权得出此时的繁忙度。更新时调用回调函数,在Server类里更新自己的优先级(这里用了一个set来存储权重、对应的EventLoop指针)。在Server每次分配连接时,就取现在繁忙度最小的。
实际操作起来后..
抛出异常 -> gdb调试,锁定了是updateCallback( )回调函数 -> 检查代码逻辑。发现该声明的声明了,该set的set了,该绑定的绑定了 -> 从涉及到updateCallback( )的逻辑部分来看没什么漏的 -> 查找资料/面向ai寻求帮助:可能是多线程同时访问,或者回调函数为空判一下非空 -> 排列组合测试了一下:
互斥锁+判非空:正常运行
无互斥锁+判非空:正常运行
互斥锁+不判非空:bad_funciton_callback
无互斥锁+不判非空:bad_funciton_callback
总结:不判非空都寄了。
-> 所以函数不能为空,把过程变量_cb、updateCallback( )打印出来确实是非空的,用print _cb.operator bool( )也是true,此时没懂为什么会是空的。
以及:到底回调函数是一直为空,还是说有时是有时不是?
如果一直是空的,说明set不会被改变,因为只有在回调时才会往对set进行删改。但代码又能跑起来,那进一步说明其实只有一个SubReactor在工作。写到这里突然发现自己初始化时没往set里面放东西,理论上在建立连接时会有问题,会先运行 auto leastSubReactor = *(eventLoopSet.begin());,这里的begin可能指的乱七八糟。所以先修这个bug。
(其实这里很奇怪,理论上肯定是先有connection连接,连接又需要有EventLoop*,但此时set理论上又是空的,居然能连接上?还跑了几千条?猜测channel里不完全是connection,可能有其它的事件(比如acceptor注册到了epoll上),所以在连接前先触发了update,才这样有新的connection来的时候,set里面才有东西。如果是这样的话,初始化set时往里面塞东西,排列组合的测试结果可能和刚才一样。)
但这只是一个猜想,既然初始化有问题,可能其它也有Bug。先把set的相关代码看了一遍,资料说要么函数为空,要么并发控制有问题,突然发现set是共享资源且在删改时没加锁,那就加个mutex。目前为止,改了2处地方:set操作时有互斥,set初始化成先放一个SubReactor[0]进去。再测一下看看:
判断非空:并发压力大;epoll add error: Bad file descriptor;同时没有其它报错,进程就异常结束了?考虑把连接数目减少,10个20个时能正常退出,再大一点就不行。
不判空:同样的结果。
说明就是set没有写好,导致了回调函数出错,虽然回调函数的绑定是没问题,但具体实现有问题。猜测可能多个线程同时访问set,导致了玄学错误。之前一直注重回调函数本身的问题,有点狭隘。现在好了,不为空但是扛不住压力,新的调度算法表现还不如随机。如果真的每次选最优的,线程个数又只有8,set操作的O(log)复杂度可以几乎不管。虽然不一定更优,但起码不能这么差。于是用gdb调试,打个断点输出每次EventLoop的地址,发现是同一个?并且还是会时不时报这个错:
Thread 2 "server" hit Breakpoint 2, EventLoop::loop (this=0x555555797150)
at src/EventLoop.cpp:50
updateCallback(oldAvgHandleTIme, oldChannelCount, avgHandleTime, channelCount, this);
(gdb) next
terminate called after throwing an instance of 'std::bad_function_call'
what(): bad_function_call
现在知道不能只盯着回调函数为空了,考虑重新优化下set的写法。指定为SubReactor[0]理论上会使得set里一直只有这个值(虽然根据刚才的猜测,哪怕没初始化,也应该会修改set,既然没修改,说明刚才的猜测还是有问题,但现在无从考证了),与其一直面向Bug编程不如把代码写得健壮点。
初始化改成默认为SubReactor[0]-SubReactor[7]轮番启动,并且这块代码得加锁;updateCb改成实时更新,并且判非空。
这时调度能实现了,但抛出了2个报错:
问题1:[Inferior 1 (process 26181) exited with code 01] 没用其它报错信息就退出
问题2: 如果不判非空,还是会出问题。
在多重问题一起出现时,有可能不是并行的,而是一个导致了另一个。我觉得问题1很有可能是问题2导致的,所以继续解决问题2。针对2:既然set里能有东西,说明回调函数是被调用了的,但不是所有的都能用,所以是有时为空有时不为空?目前没找到原因,调度也能跑起来,就先判着。重新考虑回调函数为空的几个可能:1.函数未被正确初始化(排除) 2.函数的生命周期,比如调用的东西其实已经无效了,被销毁了。但我似乎(?)并没有在update里删什么东西。
刚才的测试一直都是一上来就上压力,然后就崩,崩完也找不到问题,把测试程序的参数改成建立连接后等待10s再回显,发现了新的报错:
leastReactor: (nil)
Thread 1 "server" received signal SIGSEGV, Segmentation fault.
0x000055555556e856 in EventLoop::updateChannel (this=0x0, ch=0x5555557b44c0) at src/EventLoop.cpp:72
72 ep->updateChannel(ch);
(gdb) bt
#0 0x000055555556e856 in EventLoop::updateChannel (this=0x0, ch=0x5555557b44c0) at src/EventLoop.cpp:72
#1 0x000055555556d159 in Channel::enableRead (this=0x5555557b44c0) at src/Channel.cpp:35
#2 0x000055555556bef7 in Connection::Connection (this=0x5555557b4470, _loop=0x0, _sock=0x5555557b4220)
at src/Connection.cpp:18
#3 0x00005555555584fb in Server::newConnection (this=0x555555798140, sock=0x5555557b4220) at src/Server.cpp:69
#4 0x00005555555642cd in std::__invoke_impl<void, void (Server::*&)(Socket*), Server*&, Socket*> (__f=
@0x555555798360: (void (Server::*)(Server * const, Socket *)) 0x55555555824e <Server::newConnection(Socket*)>,
__t=@0x555555798370: 0x555555798140) at /usr/include/c++/7/bits/invoke.h:73
#5 0x0000555555563556 in std::__invoke<void (Server::*&)(Socket*), Server*&, Socket*> (__fn=
这个报错的本质是有个EventLoop的指针为0,并且"leastReactor: (nil)"也印证了这点。问题在于前面的输出信息里leastReactor其实是有数据的,结合现在测试行为的不同,突然想到:如果没有新的触发事件,也就不会触发回调,也就不会往set里面塞东西,当初始化的8个都用完后,我认为此时set里会有值了,就调用了 (*(eventLoopSet.begin())).eventLoop,但这是❌的!顺便数了一下,发现果然在第9个连接时出问题。
此时的错误代码:
EventLoop* leastSubReactor=nullptr;
{
std::lock_guard<std::mutex> lock(bookMutex);
for(int i=0;i<8;i++){
if(book[subReactors[i]]) continue;
book[subReactors[i]]=1;
leastSubReactor=subReactors[i];
break;
}
if(leastSubReactor==nullptr)
leastSubReactor = (*(eventLoopSet.begin())).eventLoop;
}
修改成:
if(leastSubReactor==nullptr){
if(eventLoopSet.empty()){
leastSubReactor = subReactors[sock->getFd()%8];
}
else leastSubReactor = (*(eventLoopSet.begin())).eventLoop;
}
修改完后能接受10000条连接同时在了,遇到的新问题是打印出来set里的信息没怎么被改动。如果成功插入就不会没改动,所以是不是没有成功插入?逐个排查。
在insert的语句里加一个:
auto result = eventLoopSet.insert(newInfo);
if (result.second) {
puts( "Insertion into eventLoopSet was successful." );
}
发现又是能成功插入的,说明回调函数有被调用。但这时已经没打印set信息了,说明我打印的地方不对,刚才只在建立连接时打印,当连接全部建立完毕,就不会再有新的打印信息。应该在每次更新set时都打出来看看,于是又在回调函数里每当对set有更新,就打印set里所有元素的信息。
发现后面也会有打印信息了,但是依然没变动?
① 有可能是函数实现的问题,打印oldinfo和newinfo,看看是不是值有变动,但我代码有问题
② 还是说值本身就是没变动。值本身没变动可能是:
1.调度算法设计有问题(包括求平均处理时间、求连接个数的代码可能有问题;或者说这种机制本身就不靠谱,这种普通的压力测试下处理时间可能大差不差;或者说实时回调就不合理)
2.回调函数唤起的时机太少了,去除了非空后,更新不及时。这个好办,调用回调前输出新旧信息就好。
测试结果:值本身就没变动。
Old Info: Weight = 0.90, Avg Handle Time = 0.17, Channel Count = 1, EventLoop Address = 0x5555557991b0
New Info: Weight = 0.50, Avg Handle Time = 0.17, Channel Count = 1,, EventLoop Address = 0x5555557991b0
那可能是回调函数更新得太实时,这样频繁的更新意义并不大,于是加入简单的标准(这里标准也很简陋,各个合适的值需要做更多测试):
bool handleTimeChanged = (oldAvgHandleTIme > 0) &&
((avgHandleTime / oldAvgHandleTIme > 2.0) ||
(avgHandleTime / oldAvgHandleTIme < 0.5));
bool channelCountChanged = (oldChannelCount > 0) &&
((oldChannelCount / channelCount > 2.0) ||
(oldChannelCount / channelCount < 0.5));
这两个条件有一个满足时,才考虑更新set,发现值终于开始变动了!
但发现此时的set里多了很多元素,远不止8个。同一个eventLoop存储了很多不同的weight,猜测是很多个状态先后访问,都给加进去了。再次更新代码,删除的时候遍历,只要指针的值==evetLoop,就删除。
最后实现了1000条连接同时在线,能正常调度、正常更新set的玩具算法。。
总结:
- 实战时和算法题关注的重点不一样,多线程编程会有很多问题,哪里没考虑好并发控制,动不动就崩。
- 很多个问题一起出现时,可能是相互影响的,之间不是并行关系。要一点逻辑判断+经验,才能知道先解决什么。
- 与其面向Bug编程,不如写的时候就多考虑清楚,把代码写健壮点。算法题的代码不需要考虑太多复杂的情况,但用户的输入可能什么都有,并且这段代码的行为很可能被另一段影响,这才是真实的情况。
- gdb真是个伟大的发明。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了