首页 > 解决方案 > 具有昂贵操作的线程会减慢 UI 线程 - Windows 10、C++

问题描述

问题:我正在处理的 Windows 10 应用程序中有两个线程,一个 UI 线程(在代码中称为渲染线程)和一个后台工作线程(在代码中称为模拟线程)。每隔几秒钟左右,后台线程就必须执行一项非常昂贵的操作,涉及分配大量内存。出于某种原因,当此操作发生时,UI 线程会滞后一秒钟并变得无响应(这在应用程序中被视为在给出相机移动输入时相机没有移动一秒钟)。

也许我误解了线程在 Windows 上的工作方式,但我不知道这是应该发生的事情。我的印象是,您使用单独的 UI 线程正是出于这个原因:在其他线程执行更多时间密集型操作时保持它的响应性。

我尝试过的事情:我已经删除了两个线程之间的所有通信,所以没有互斥锁或类似的东西(除非 Windows 隐含了一些我不知道的东西)。我还尝试将 UI 线程设置为比后台线程更高的优先级。这些都没有帮助。

我注意到了一些事情:虽然 UI 线程滞后了一会儿,但在我的机器上运行的其他应用程序和以往一样响应迅速。繁重的操作似乎只影响这一个过程。此外,如果我减少分配的内存量,它会缓解问题(但是,为了让应用程序按我的意愿工作,它需要能够进行这种分配)。

问题:我的问题有两个。首先,我想了解为什么会发生这种情况,因为这似乎违背了我对多线程应该如何工作的理解。其次,您对如何解决此问题并获得它有任何建议或想法,以便 UI 不会滞后。

缩写代码:注意timeline.h main.cpp中关于epochs的注释

#include "Renderer/Headers/Renderer.h"
#include "Shared/Headers/Timeline.h"
#include "Simulator/Simulator.h"

#include <iostream>
#include <Windows.h>


unsigned int __stdcall renderThread(void* timelinePtr);
unsigned int __stdcall simulateThread(void* timelinePtr);

int main() {
    Timeline timeline;

    HANDLE renderHandle = (HANDLE)_beginthreadex(0, 0, &renderThread, &timeline, 0, 0);
    if (renderHandle == 0) {
        std::cerr << "There was an error creating the render thread" << std::endl;
        return -1;
    }
    SetThreadPriority(renderHandle, THREAD_PRIORITY_HIGHEST);

    HANDLE simulateHandle = (HANDLE)_beginthreadex(0, 0, &simulateThread, &timeline, 0, 0);
    if (simulateHandle == 0) {
        std::cerr << "There was an error creating the simulate thread" << std::endl;
        return -1;
    }
    SetThreadPriority(simulateHandle, THREAD_PRIORITY_IDLE);

    WaitForSingleObject(renderHandle, INFINITE);
    WaitForSingleObject(simulateHandle, INFINITE);
    return 0;
}

unsigned int __stdcall renderThread(void* timelinePtr) {
    Timeline& timeline = *((Timeline*)timelinePtr);

    Renderer renderer = Renderer(timeline);
    renderer.run();
    return 0;
}

unsigned int __stdcall simulateThread(void* timelinePtr) {
    Timeline& timeline = *((Timeline*)timelinePtr);

    Simulator simulator(timeline);
    simulator.run();
    return 0;
}

模拟器.cpp

// abbreviated
void Simulator::run() {
    while (true) {
        // abbreviated
        timeline->push(latestState);
    }
}
// abbreviated

时间线.h

#ifndef TIMELINE_H
#define TIMELINE_H

#include "WorldState.h"
#include <mutex>
#include <vector>

class Timeline {
public:
    Timeline();
    bool tryGetStateAtFrame(int frame, WorldState*& worldState);
    void push(WorldState* worldState);
private:

    // The concept of an Epoch was introduced to help reduce mutex conflicts, but right now since the threads are disconnected, there should be no mutex locks at all on the UI thread. However, every 1024 pushes onto the timeline, a new Epoch must be created. The amount of slowdown largely depends on how much memory the WorldState class takes. If I make WorldState small, there isn't a noticable hiccup, but when it is large, it becomes noticeable.  
    class Epoch {
    public:
        static const int MAX_SIZE = 1024;

        void push(WorldState* worldstate);
        int getSize();
        WorldState* getAt(int index);
    private:
        int size = 0;
        WorldState states[MAX_SIZE];
    };
    Epoch* pushEpoch;

    std::mutex lock;
    std::vector<Epoch*> epochs;
};

#endif // !TIMELINE_H

时间线.cpp

#include "../Headers/Timeline.h"

#include <iostream>

Timeline::Timeline() {
    pushEpoch = new Epoch();
}

bool Timeline::tryGetStateAtFrame(int frame, WorldState*& worldState) {
    if (!lock.try_lock()) {
        return false;
    }
    if (frame >= epochs.size() * Epoch::MAX_SIZE) {
        lock.unlock();
        return false;
    }
    worldState = epochs.at(frame / Epoch::MAX_SIZE)->getAt(frame % Epoch::MAX_SIZE);
    lock.unlock();
    return true;
}

void Timeline::push(WorldState* worldState) {
    pushEpoch->push(worldState);
    if (pushEpoch->getSize() == Epoch::MAX_SIZE) {
        lock.lock();
        epochs.push_back(pushEpoch);
        lock.unlock();
        pushEpoch = new Epoch();
    }
}

void Timeline::Epoch::push(WorldState* worldState) {
    if (this->size == this->MAX_SIZE) {
        throw std::out_of_range("Pushed too many items to Epoch without clearing");
    }
    this->states[this->size] = *worldState;
    this->size++;
}

int Timeline::Epoch::getSize() {
    return this->size;
}

WorldState* Timeline::Epoch::getAt(int index) {
    if (index >= this->size) {
        throw std::out_of_range("Tried accessing nonexistent element of epoch");
    }
    return &(this->states[index]);
}

Renderer.cpp:循环调用 Presenter::update() 和一些 OpenGL 渲染任务。

演示者.cpp

// abbreviated
void Presenter::update() {
    camera->update();
    // timeline->tryGetStateAtFrame(Time::getFrames(), worldState); // Normally this would cause a potential mutex conflict, but for now I have it commented out. This is the only place that anything on the UI thread accesses timeline.
}
// abbreviated

有什么帮助/建议吗?

标签: c++windowsmultithreadinguser-interfaceopengl

解决方案


我最终弄清楚了!

事实证明,newC++ 中的运算符是线程安全的,这意味着一旦开始,它必须在任何其他线程执行任何操作之前完成。为什么这对我来说是个问题?好吧,当一个 Epoch 被初始化时,它必须初始化一个包含 1024 个 WorldStates 的数组,每个 WorldStates 都有 10,000 个 CellStates 需要初始化,每个 CellStates都有一个包含 16 个项的数组需要初始化,所以我们结束了在new操作员返回之前,需要初始化超过 100,000,000 个对象。这花费了足够长的时间,导致 UI 在等待时出现打嗝。

解决方案是创建一个工厂函数,该函数将零碎地构建 Epoch 的各个部分,一次一个构造函数,然后将它们组合在一起并返回一个指向新 epoch 的指针。

时间线.h


    #ifndef TIMELINE_H
    #define TIMELINE_H
    
    #include "WorldState.h"
    #include <mutex>
    #include <vector>
    
    class Timeline {
    public:
        Timeline();
        bool tryGetStateAtFrame(int frame, WorldState*& worldState);
        void push(WorldState* worldState);
    private:
    
        class Epoch {
        public:
            static const int MAX_SIZE = 1024;
            static Epoch* createNew();
    
            void push(WorldState* worldstate);
            int getSize();
            WorldState* getAt(int index);
        private:
            Epoch();
    
            int size = 0;
            WorldState* states[MAX_SIZE];
        };
        Epoch* pushEpoch;
    
        std::mutex lock;
        std::vector<Epoch*> epochs;
    };
    
    #endif // !TIMELINE_H

时间线.cpp


    Timeline::Epoch* Timeline::Epoch::createNew() {
        Epoch* epoch = new Epoch();
        for (unsigned int i = 0; i < MAX_SIZE; i++) {
            epoch->states[i] = new WorldState();
        }
        return epoch;
    }


推荐阅读