做任务单用go实现了异步日志,计划c++化练练手。本以为分分钟搞定的事,结果debug到凌晨两点/(ㄒoㄒ)/~~
第一版关键代码如下:
AsyncLog::AsyncLog(size_t bufSize, WriteLogFunc func) : _curBuf(new Buffer(bufSize)) , _nextBuf(new Buffer(bufSize)) , _writeLogFunc(func) , _thread([&]{ this->_WriteLoop(bufSize);})
, _running(true) { ::InitializeConditionVariable(&_cond);
_thread.detach(); } AsyncLog::~AsyncLog()
{ if (_running) Stop(); } void AsyncLog::Append(const void* data, size_t len) { cLock lock(_mutex); _curBuf->append(data, len); if (_curBuf->writableBytes() == 0) { _bufVec.push_back(_curBuf); //FIXME:new可能返回null,不过那会系统已经要跪了吧~ _nextBuf ? (_curBuf = std::move(_nextBuf)) : (_curBuf.reset(new Buffer)); ::WakeConditionVariable(&_cond); } } void AsyncLog::_WriteLoop(size_t bufSize) { BufferPtr spareBuf1(new Buffer(bufSize)); BufferPtr spareBuf2(new Buffer(bufSize)); BufferVec bufToWriteVec; bufToWriteVec.reserve(8); while (_running){ { cLock lock(_mutex); ::SleepConditionVariableCS(&_cond, &_mutex, INFINITE); bufToWriteVec.swap(_bufVec); bufToWriteVec.push_back(_curBuf); _curBuf = std::move(spareBuf1); if (_nextBuf == NULL) _nextBuf = std::move(spareBuf2); } _writeLogFunc(bufToWriteVec);
if (spareBuf1 == NULL){ spareBuf1 = bufToWriteVec[0]; spareBuf1->clear(); } if (spareBuf2 == NULL){ spareBuf2 = bufToWriteVec[1]; spareBuf2->clear(); } bufToWriteVec.clear(); } } void AsyncLog::Stop() { _running = false; ::WakeConditionVariable(&_cond); _thread.join(); //Notice:阻塞,等待子线程执行结束 }
共有五处Bug哟~
- 用std::thread做成员,初始化列表指定子线程启动函数,该函数访问资源时,可能都没初始化好(ctor中调度出去了)
- Stop逻辑是后添加的,_running成员声明在了末尾,其初始化即位于_thread之后,可能_WriteLoop执行时_running仍为false
- 构造函数中,习惯性调_thread.detach(),析构函数中又调了_thread.join()
- 竞态:_WriteLoop中的耗时IO操作_writeLogFunc(bufToWriteVec),可能调度至前台线程,若其继续写入数据,且触发析构或Stop,那多写的数据不会记Log,因为下次loop时_running已是false
- Lambda表达式,用的引用捕获,this指针尚可,bufSize可是局部栈变量,指不定啥时候被戳死 ( ☉_☉)≡☞o────
光构造函数就占了三 (╯‵□′)╯,所以说c++的抽象数据结构,碰上多线程,作死哇。防范ctor、dtor中途调度到别的线程,是门专业技能——详细可参考陈硕的书《Linux多线程服务端编程》前两章(这书前四章干货爆表)
后记:写个代码,把所有该犯的错犯了个遍也是够可以的( ▔___▔)y