You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

双缓冲线程切换等待时间交替为0与30000微秒问题排查

双线程双缓冲控制台引擎的同步问题分析与修复

从你的描述和代码来看,问题出在线程同步逻辑不严谨,导致主线程(绘制线程)和读取线程的执行节奏混乱,出现交替的等待时间。下面我会拆解问题根源,并给出具体的修复方案。

问题根源分析

你的核心逻辑是让绘制线程和读取线程交替工作:绘制完成后等待读取完成,交换缓冲区后再开始下一轮绘制。但原代码的同步逻辑有几个关键缺陷:

  1. 共享变量未受锁保护:readingFinished、writingFinished等同步变量的读写没有在互斥锁保护下,会导致数据竞争,出现未定义的状态判断。
  2. 条件变量使用不规范:没有使用谓词等待(wait的第二个参数),无法避免虚假唤醒,同时等待条件逻辑混乱。
  3. 读取线程无等待直接循环:读取线程完成一次输出后,如果writingFinished为true,会立刻进入下一次读取,没有等待主线程完成绘制,导致主线程进入EndDraw时,有时读取已完成(等待时间0),有时读取正在进行(等待时间30ms左右),形成交替现象。
  4. 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();
    }
}

修复后的执行流程

  1. 初始化:pWritePtr指向buffer1,pReadPtr指向buffer2,readCompleted=true。
  2. 主线程调用BeginDraw:等待readCompleted,交换缓冲区(第一次交换后pWritePtr还是buffer1,因为初始readCompleted=true),开始绘制。
  3. 主线程调用EndDraw:标记drawCompleted=true,通知读取线程。
  4. 读取线程被唤醒:输出buffer2,清空后标记readCompleted=true,通知主线程。
  5. 主线程下一次BeginDraw:等待readCompleted,交换缓冲区(pWritePtr变为buffer2,pReadPtr变为buffer1),开始绘制。
  6. 重复上述流程,实现严格的交替执行,等待时间会稳定在读取线程的执行时间(或0,取决于哪端先完成,但不会再出现交替的极端情况)。

内容的提问来源于stack exchange,提问作者Anyfail

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 10:04:29