为啥我的文件加载线程没有与主线程并行?

Posted

技术标签:

【中文标题】为啥我的文件加载线程没有与主线程并行?【英文标题】:Why is my file-loading thread not parallelized with the main thread?为什么我的文件加载线程没有与主线程并行? 【发布时间】:2009-04-08 06:36:19 【问题描述】:

我的程序在后台进行文件加载和 memcpy'ing,而屏幕是为了交互更新。这个想法是异步加载程序很快需要的文件,以便在主线程需要它们时可以使用它们。但是,加载/复制似乎不会与主线程并行发生。主线程在加载过程中暂停,通常会等待所有加载(一次最多 8 个)完成,然后再进行主线程的主循环的下一次迭代。

我使用的是 Win32,所以我使用 _beginthread 来创建文件加载/复制线程。

工作线程函数:

void fileLoadThreadFunc(void *arglist)

    while(true)
    
        // s_mutex keeps the list from being updated by the main thread
        s_mutex.lock();  // uses WaitForSingleObject INFINITE
        // s_filesToLoad is a list added to from the main thread
        while (s_filesToLoad.size() == 0)
        
            s_mutex.unlock();
            Sleep(10);
            s_mutex.lock();
        
        loadObj *obj = s_filesToLoad[0];
        s_filesToLoad.erase(s_filesToLoad.begin());
        s_mutex.unlock();

        obj->loadFileAndMemcpy();
    

主线程启动:

_beginThread(fileLoadThreadFunc, 0, NULL);

主线程用来“踢”线程加载文件的类中的代码:

// I used the commented code to see if main thread was ever blocking
// but the PRINT never printed, so it looks like it never was waiting on the worker
//while(!s_mutex.lock(false))
//
//  PRINT(L"blocked! ");
//
s_mutex.lock();
s_filesToLoad.push_back(this);
s_mutex.unlock();

还有一些基于 cmets 的笔记:

工作线程中的 loadFileAndMemcpy() 函数通过 Win32 ReadFile 函数加载 - 这会导致主线程阻塞吗? 我将工作线程的优先级降低到 THREAD_PRIORITY_BELOW_NORMAL 和 THREAD_PRIORITY_LOWEST,这有点帮助,但是当我四处移动鼠标以查看工作线程工作时它移动的速度时,鼠标“跳跃”了一下(没有降低优先级,情况会更糟)。 我在 Core 2 Duo 上运行,所以我不希望看到任何鼠标延迟。 互斥代码似乎不是问题,因为“阻塞!”从未在我上面的测试代码中打印过。 我将睡眠时间提高到 100 毫秒,但就鼠标延迟而言,即使 1000 毫秒似乎也无济于事。 正在加载的数据很小 - 20k .png 图像(但它们是 2048x2048)。它们很小,因为这只是测试数据,图像中只有一种颜色,因此实际数据会大得多。

【问题讨论】:

请分享GUI线程循环的完整代码。 主线程的代码?这是一个完整的游戏引擎 - Torque Game Builder。 只有循环部分。对我来说,这些后台线程与 GUI 线程相比具有相同的优先级。主线程实际上可能正在等待 Mutex。循环部分可能会有所帮助。 我添加了一些测试代码,用于确认主线程没有在互斥体上等待。该 PRINT 从未触发。这在主循环的深处被调用,实际上是由脚本触发的,所以除了这几行之外,我想不出任何与帖子相关的内容。 哦,关于线程优先级,我没有明确设置一个..所以我猜工作线程继承了主线程的优先级?如果是这样,我如何设置相对较低的优先级?线程优先级函数采用绝对值而不是“比主线程的优先级低一点”。 【参考方案1】:

您将必须显示主线程的代码,以指示如何通知它已加载文件。很可能存在阻塞问题。如果您可以在主循环中使用异步 I/O 而不是线程,这确实是一个很好的案例。如果没有别的,你真的需要使用条件或事件。一个触发文件阅读器线程有工作要做,另一个通知主线程文件已加载。

编辑:好的,这是一个游戏,您正在轮询文件是否已作为渲染循环的一部分完成加载。这是我会尝试的:使用 ReadFileEx 启动重叠读取。这不会阻塞。然后在您的主循环中,您可以通过使用具有零超时的等待函数之一来检查读取是否完成。这也不会阻塞。

【讨论】:

主线程并没有真正收到文件加载完成的通知。它们是正在加载的 sprite 图形,如果纹理对象的 bLoaded 标志为 false,则 sprite 对象不会自行渲染。 bLoaded 由加载线程设置。如何不使用线程进行异步加载? 您将使用 ReadFileEx 启动重叠 I/O 操作。查看编辑。【参考方案2】:

不确定您的具体问题,但您真的也应该互斥保护大小调用。

void fileLoadThreadFunc(void *arglist) 
    while (true) 
        s_mutex.lock();
        while (s_filesToLoad.size() == 0) 
            s_mutex.unlock();
            Sleep(10);
            s_mutex.lock();
        
        loadObj *obj = s_filesToLoad[0];
        s_filesToLoad.erase(s_filesToLoad.begin());
        s_mutex.unlock();
        obj->loadFileAndMemcpy();
    

现在,检查您的具体问题,我看不出您提供的代码有什么问题。主线程和文件加载器线程应该很高兴地并行运行如果该互斥锁是它们之间唯一的争用

我之所以这么说是因为可能存在其他争论点,例如在标准库中,您的示例代码没有显示。

【讨论】:

更新了我的 OP,因为从代码中看不出我没有使用互斥锁,因为自定义容器实现只返回一个 int 成员变量。 @Jim,我并不是说 specific 互斥锁会导致主线程阻塞。如果文件 I/O 例程具有全局锁定,则它可能会阻塞其他点。 只是出于兴趣,如果将睡眠调用从 10 毫秒提高到 1000 毫秒会怎样? 此外,我将查看 obj->loadFileAndMemcpy() 中发生的情况,这可能会阻止主线程运行。我问的原因是因为您声明“将经常等待所有负载”(而不是总是),这可能表明那里存在问题。【参考方案3】:

我会以这种方式编写该循环,减少可能会搞砸的锁定解锁:P:

void fileLoadThreadFunc(void *arglist)

    while(true)
    
        loadObj *obj = NULL;

        // protect all access to the vector
        s_mutex.lock();
        if(s_filesToLoad.size() != 0)
        
             obj = s_filesToLoad[0];
             s_filesToLoad.erase(s_filesToLoad.begin());
        
    s_mutex.unlock();

    if( obj != NULL )
            obj->loadFileAndMemcpy();
        else
            Sleep(10);

    

Synchronization 上的 MSDN

【讨论】:

为什么总是休眠 10 毫秒。仅当队列为空时才休眠【参考方案4】:

如果您可以考虑开源选项,Java 有一个阻塞队列 [link] 和 Python [link]。这会将您的代码减少到(此处的队列绑定到 load_fcn,即使用闭包)

def load_fcn():
    while True:
        queue.get().loadFileAndMemcpy()
threading.Thread(target=load_fcn).start()

即使您可能不应该使用它们,python 3.0 线程也有一个 _stop() 函数,python2.0 线程有一个 _Thread__stop 函数。您还可以将“无”值写入队列并签入 load_fcn()。

此外,如果您愿意,可以在 *** 中搜索“[python] gui”和“[subjective] [gui] [java]”。

【讨论】:

【参考方案5】:

根据目前提供的信息,我的猜测是文件加载处理程序中的某些内容正在与您的主循环交互。我不知道所涉及的库,但根据您的描述,文件处理程序执行以下操作:

为 20k 文件加载原始二进制数据 将 20k 解释为 PNG 文件 加载到表示 2048x2048 像素图像的结构中

关于用于实现这些步骤的库,我想到了以下可能性:

可能是未压缩图像数据的内存分配持有主线程执行任何绘图/交互操作所需的锁? 会不会是负责将 PNG 数据转换为像素的调用实际上持有与您的主线程产生不利交互的低级游戏库锁?

获取更多信息的最佳方法是尝试对文件加载器处理程序的活动进行建模,而不使用其中的当前代码...自己编写一个例程,分配正确大小的内存块并执行一些处理将 20k 的源数据转换为目标块大小的结构......然后一次一点地向它添加进一步的逻辑,直到在性能崩溃时缩小范围以找出罪魁祸首。

【讨论】:

【参考方案6】:

我认为您的问题在于访问 FilesToLoad 对象。

在我看来,这个对象在实际处理它时被您的线程锁定(根据您的代码,每 10 毫秒),并且在尝试更新列表时被您的主线程锁定。这可能意味着您的主线程正在等待一段时间才能访问它和/或操作系统解决了可能发生的任何竞争情况。

我会建议你要么启动一个工作线程,只是在你想要的时候加载一个文件,设置一个信号量(甚至是一个布尔值)来显示它何时完成,或者使用 _beginthreadex 并创建一个挂起的每个文件的线程,然后同步它们,以便在每个文件完成时恢复下一行。

如果你想让一个线程在后台永久运行擦除和加载文件,那么你总是可以让它处理它自己的消息队列并使用 Windows 消息传递来回传递数据。这样可以省去很多关于线程锁定和避免竞争条件的烦恼。

【讨论】:

以上是关于为啥我的文件加载线程没有与主线程并行?的主要内容,如果未能解决你的问题,请参考以下文章

将工作线程与主线程同步

android-如何在与主线程不同的线程中运行服务?

Android Handler 异步调用修改界面与主线程

[转]QT子线程与主线程的信号槽通信-亲测可用!

为啥使用了线程池速度没有变化呢python

与主线程相比,在辅助线程上崩溃是不是有优势?