首页 > 解决方案 > 改进多线程代码设计以防止竞争条件

问题描述

我遇到了一个问题,我不确定是否可以通过我想要解决的方式来解决。我对比赛条件有疑问。

我有一个项目作为 C++ dll(主引擎)运行。然后我有第二个 C# 进程,它使用 C++/CLI 与主引擎(编辑器)进行通信。

编辑器将引擎窗口作为子窗口托管。这样做的结果是子窗口异步接收输入消息(请参阅 参考资料RiProcessMouseMessage())。通常这只发生在我打电话时window->PollEvents();

main engine loop {
    RiProcessMouseMessage(); // <- Called by the default windows message poll function from the child window
    
    foreach(inputDevice)
        inputDevice->UpdateState();

    otherCode->UseCurrentInput();
}

主编辑器循环是我无法控制的 WPF 循环。基本上它是这样做的:

main editor loop {
    RiProcessMouseMessage(); // <- This one is called by the editor (parent) window, but is using the message loop of the (child) engine window
}

RawInput 处理器被引擎称为同步,被编辑器称为异步

void Win32RawInput::RiProcessMouseMessage(const RAWMOUSE& rmouse, HWND hWnd) {
    MouseState& state = Input::mouse._GetGatherState();

    // Check Mouse Position Relative Motion
    if (rmouse.usFlags == MOUSE_MOVE_RELATIVE) {
        vec2f delta((float)rmouse.lLastX, (float)rmouse.lLastY);
        delta *= MOUSE_SCALE;
        state.movement += delta;

        POINT p;
        GetCursorPos(&p);
        state.cursorPosGlobal = vec2i(p.x, p.y);

        ScreenToClient(hWnd, &p);
        state.cursorPos = vec2i(p.x, p.y);
    }

    // Check Mouse Wheel Relative Motion
    if (rmouse.usButtonFlags & RI_MOUSE_WHEEL)
        state.scrollMovement.y += ((float)(short)rmouse.usButtonData) / WHEEL_DELTA;

    if (rmouse.usButtonFlags & RI_MOUSE_HWHEEL)
        state.scrollMovement.x += ((float)(short)rmouse.usButtonData) / WHEEL_DELTA;

    // Store Mouse Button States
    for (int i = 0; i < 5; i++) {
        if (rmouse.usButtonFlags & maskDown_[i]) {
            state.mouseButtonState[i].pressed = true;
            state.mouseButtonState[i].changedThisFrame = true;
        } else if (rmouse.usButtonFlags & maskUp_[i]) {
            state.mouseButtonState[i].pressed = false;
            state.mouseButtonState[i].changedThisFrame = true;
        }
    }
}

UpdateState()仅由引擎调用。它基本上将 RawInput 交换为当前使用的输入。这是为了防止在帧循环中间(又名 during otherCode->UseCurrentInput();)进行输入更新

void UpdateState() {
    currentState = gatherState; // Copy gather state to current
    Reset(gatherState);         // Reset the old buffer so the next time the buffer it's used it's all good
    
   // Use current state to check stuff
   // For the rest of this frame currentState should be used
}

MouseState& _GetGatherState() { return gatherState; }

void Reset(MouseState& state) { // Might need a lock around gatherState :(
    state.movement = vec2f::zero;
    state.scrollMovement = vec2f::zero;

    for (int i = 0; i < 5; ++i)
        state.mouseButtonState[i].changedThisFrame = false;
}

如您所见,当在主引擎循环中RiProcessMouseMessage()调用 while时,会发生竞争条件。Reset()如果不清楚:该Reset()函数需要将状态重置回其帧默认数据,以便每帧正确读取数据。

现在我非常清楚我可以通过mutex在gatherState 更新周围添加一个来轻松解决这个问题,但如果可能的话我想避免这种情况。基本上我在问是否可以重新设计此代码以无锁?

标签: c#c++multithreadingwinapirace-condition

解决方案


如果两端都更改缓冲区,您正在询问无锁,这是不太可能的。但是,如果您要求锁定已优化且几乎是即时的,那么您可以使用 FIFO 逻辑。您可以使用 .net 的 ConcurrentQueue "https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1?view=net-5.0" 编写更新并从中轮询更新队列。

如果你真的摆脱了锁,那么你可以检查无锁循环数组,也就是无锁环形缓冲区,如果你想更深入地研究硬件级别以了解这背后的逻辑,那么你可以查看https://electronics。 stackexchange.com/questions/317415/how-to-allow-thread-and-interrupt-safe-writing-of-incoming-usart-data-on-freerto所以你也会对低级并发有一个想法; 有限制,当一端只写入而另一端只在已知间隔/边界内读取时,无锁环形缓冲区可以工作,可以检查提出的类似问题: 循环无锁缓冲区

Boost 有众所周知的无锁实现:https ://www.boost.org/doc/libs/1_65_1/doc/html/lockfree.html


推荐阅读