双缓冲线程切换等待时间交替为0与30000微秒问题排查
双线程双缓冲控制台引擎的同步问题分析与修复
从你的描述和代码来看,问题出在线程同步逻辑不严谨,导致主线程(绘制线程)和读取线程的执行节奏混乱,出现交替的等待时间。下面我会拆解问题根源,并给出具体的修复方案。
问题根源分析
你的核心逻辑是让绘制线程和读取线程交替工作:绘制完成后等待读取完成,交换缓冲区后再开始下一轮绘制。但原代码的同步逻辑有几个关键缺陷:
- 共享变量未受锁保护:
readingFinished、writingFinished等同步变量的读写没有在互斥锁保护下,会导致数据竞争,出现未定义的状态判断。 - 条件变量使用不规范:没有使用谓词等待(
wait的第二个参数),无法避免虚假唤醒,同时等待条件逻辑混乱。 - 读取线程无等待直接循环:读取线程完成一次输出后,如果
writingFinished为true,会立刻进入下一次读取,没有等待主线程完成绘制,导致主线程进入EndDraw时,有时读取已完成(等待时间0),有时读取正在进行(等待时间30ms左右),形成交替现象。 - memset字节数错误:原代码中
memset(this->pReadPtr, 0, this->pAppBase->screenBufferSize)的第三个参数是元素个数,而memset需要传入字节数,导致缓冲区只被清空一半(WORD是2字节)。
修复方案
我们需要重新设计同步逻辑,让两个线程严格交替执行,并用统一的互斥锁保护所有共享状态。
1. 重构类成员变量
合并冗余的锁和条件变量,定义清晰的同步标记:
class SwapChain : Manager { WORD *pScreenBuffer1, *pScreenBuffer2, *pWritePtr, *pReadPtr; bool isRunning, initialized; std::mutex syncMutex; // 统一保护所有共享状态的互斥锁 std::condition_variable drawDoneCv; // 绘制完成通知读取线程 std::condition_variable readDoneCv; // 读取完成通知绘制线程 bool drawCompleted; // 绘制完成标记 bool readCompleted; // 读取完成标记 DWORD charsWritten; COORD startPosition; int screenBufferWidth; std::thread readThread; // 存储读取线程对象,避免detach void ReadingThread(); void SwapBuffers(); public: void BeginDraw(); void EndDraw(); inline void PutPixel(short xPos, short yPos, WORD color) { this->pWritePtr[(xPos * 2) + yPos * screenBufferWidth] = color; this->pWritePtr[(xPos * 2) + yPos * screenBufferWidth + 1] = color; } void Initialize(); void Run(); void Stop(); SwapChain(WinAppBase * pAppBase); virtual ~SwapChain(); };
2. 初始化同步变量
在构造函数中正确初始化同步标记:
SwapChain::SwapChain(WinAppBase * pAppBase) : Manager(pAppBase) { this->isRunning = false; this->initialized = false; this->pReadPtr = nullptr; this->pScreenBuffer1 = nullptr; this->pScreenBuffer2 = nullptr; this->pWritePtr = nullptr; this->charsWritten = 0; this->startPosition = { 0, 0 }; this->drawCompleted = false; this->readCompleted = true; // 初始允许第一次绘制 this->screenBufferWidth = this->pAppBase->screenBufferInfo.dwSize.X; }
3. 重构读取线程逻辑
让读取线程严格等待绘制完成的通知,完成读取后通知绘制线程:
void SwapChain::ReadingThread() { std::unique_lock<std::mutex> lock(syncMutex); while (isRunning) { // 等待绘制完成,或线程停止 drawDoneCv.wait(lock, [this](){ return drawCompleted || !isRunning; }); if (!isRunning) break; // 输出缓冲区到控制台 WriteConsoleOutputAttribute( pAppBase->consoleCursor, pReadPtr, pAppBase->screenBufferSize, startPosition, &charsWritten ); // 修复memset字节数错误:乘以sizeof(WORD) memset(pReadPtr, 0, pAppBase->screenBufferSize * sizeof(WORD)); // 标记读取完成,通知绘制线程 readCompleted = true; drawCompleted = false; readDoneCv.notify_one(); } }
4. 重构BeginDraw和EndDraw
让BeginDraw等待读取完成后交换缓冲区,EndDraw标记绘制完成并通知读取线程:
void SwapChain::BeginDraw() { std::unique_lock<std::mutex> lock(syncMutex); // 等待读取完成,确保写缓冲区空闲 readDoneCv.wait(lock, [this](){ return readCompleted || !isRunning; }); if (!isRunning) return; // 交换缓冲区:将刚读完的缓冲区作为下一次的写缓冲区 SwapBuffers(); // 重置标记,准备绘制 readCompleted = false; } void SwapChain::EndDraw() { std::unique_lock<std::mutex> lock(syncMutex); if (!isRunning) return; // 标记绘制完成,通知读取线程 drawCompleted = true; drawDoneCv.notify_one(); }
5. 简化SwapBuffers并修复线程生命周期
void SwapChain::SwapBuffers() { std::swap(pReadPtr, pWritePtr); // 用std::swap更简洁安全 } void SwapChain::Run() { if (isRunning || !initialized) return; isRunning = true; readThread = std::thread(&SwapChain::ReadingThread, this); } void SwapChain::Stop() { { std::unique_lock<std::mutex> lock(syncMutex); isRunning = false; // 唤醒所有等待的线程,确保线程能退出 drawDoneCv.notify_one(); readDoneCv.notify_one(); } // 等待线程结束,避免detach导致的资源泄漏 if (readThread.joinable()) { readThread.join(); } }
修复后的执行流程
- 初始化:
pWritePtr指向buffer1,pReadPtr指向buffer2,readCompleted=true。 - 主线程调用
BeginDraw:等待readCompleted,交换缓冲区(第一次交换后pWritePtr还是buffer1,因为初始readCompleted=true),开始绘制。 - 主线程调用
EndDraw:标记drawCompleted=true,通知读取线程。 - 读取线程被唤醒:输出buffer2,清空后标记
readCompleted=true,通知主线程。 - 主线程下一次
BeginDraw:等待readCompleted,交换缓冲区(pWritePtr变为buffer2,pReadPtr变为buffer1),开始绘制。 - 重复上述流程,实现严格的交替执行,等待时间会稳定在读取线程的执行时间(或0,取决于哪端先完成,但不会再出现交替的极端情况)。
内容的提问来源于stack exchange,提问作者Anyfail
相关产品推荐
相关产品推荐

