将线程锁定很长时间

Keeping a thread locked a long time

本文关键字:长时间 锁定 线程      更新时间:2023-10-16

编辑2:要澄清的是,我的问题是,我很难写代码来阻止和恢复线程来做一些工作(在这种情况下,读取一些文件)。

编辑3:添加了void Loader::RequestToLoadRegion(Region&Region)的函数定义。

我想在我的游戏中创建一个基本的流媒体系统。我需要一个线程来读取游戏文件,这些文件大部分时间都会被屏蔽,并不时唤醒。以下是我目前的设计方法。

每当玩家接近未加载在RAM中的世界区域时,游戏向(唯一的)Loader对象发送加载该区域的请求(Loader类表示流系统)。每个区域都有一个与之相关的文件列表,这些文件将被读取以创建游戏数据并加载到RAM中。

加载分两步完成:

  • 步骤1:读取所有文件并将其内容推送到队列中
  • 步骤2:处理存储在此队列中的原始文件数据

Loader对象使用Reader对象读取所有文件并将其内容推送到上述队列中。在Loader的构造函数中,将构造Reader对象,该对象将启动一个用于读取文件的线程。然而,这个读取线程大部分时间都会被阻塞,并且只有当Loader收到加载新区域的请求时才会被Loader唤醒。

在每帧开始时,Loader将得到控制并检查其当前状态,无论是否加载。如果它处于加载状态,那么它将尝试访问受互斥锁保护的队列。如果它从队列中获得一个项目,它将对其进行处理,并将控制权交还给游戏。队列中的下一个项目将在下一帧上进行处理,依此类推,直到区域加载完成。

我知道如何使用互斥实现这一部分。然而,我遇到了一些麻烦,要阻止读取线程,并在加载请求到达时唤醒它。我已经学会了如何将std::condition_variable用于基本程序,我目前的解决方案如下:

  • 在创建Loader时,它会锁定一个互斥对象,以便读取线程保持阻塞状态
  • 当游戏向Loader发送请求时,Loader会将Reader的标志(m_canRead)设置为true,并使用条件变量调用notify_one()来唤醒读取线程
  • 当读取线程要读取文件时,它会调用m_loader.OnStartReading(),以便loader锁定m_canReadMutex互斥对象。然后控制权回到读取文件的读取器。

    我在这个设计中遇到的最大问题是,我了解到,通常会在短时间内锁定互斥体,并且通常会将其与std::lock_guard一起使用,这样,如果发生异常,互斥体就会被解锁。

// This is the function ran by the reading thread
// m_readingThread = std::thread(&Reader::Run, this);
void Reader::Run() {
while (true) {
{
std::unique_lock<std::mutex> ul(m_canReadMutex);
m_canReadCondVar.wait(ul, [this](){ return m_quit || m_canRead; });
// Determine the cause of the wake up
if (m_quit) { // Does the game wants to quit? We break out of
break;      // the loop to reach the end of the Run() function
}             // that way we can join() the reading thread.
// If control reaches this line, it means the thread was awaken by
// a load request and can read. The closing bracket "will run the destructtor"
// of the std::unique_lock to unlock the mutex.
}
{ // When control reaches this scope, it means m_canRead is true.
m_loader.OnStartReading(); // will lock the mutex m_canReadMutex
ReadFiles();
}
}// end-while (true)
}// end-function void Run()
void Reader::ReadFiles() {
/* Read all the files and push data into the queue */
}
void Loader::OnStartReading() {
m_canReadMutex.lock();
}
// This function unlocks the mutex and wake up the reading thread.
void Loader::RequestToLoadRegion(Region &region) {
/* Get the list of files associated to the region */
m_canReadMutex.unlock();
m_reader.m_canRead = true; // Set the flag so the predicate in the reader's wait() evaluates to true
m_canReadCondVar.notify_one(); // Wake up the reader thread
}

std::unique_lockstd::lock_guard非常相似:它在构造函数中获取锁,并在析构函数中释放锁。不同之处在于,它还提供了额外的功能,其中之一是您可以将其与std::condition_variable::wait一起使用。

当您调用wait时,库会将线程标记为等待,然后解锁互斥锁和块。当条件变量解除锁定时(由于对notify_onenotify_all的调用或错误调用),它会在对wait的调用返回之前重新锁定互斥对象。因此,互斥锁在wait调用之前和之后都被锁定,但在wait调用期间不被锁定。

更新:

OnStartReading直接锁定互斥,没有相应的解锁。这真的很不寻常:这意味着互斥锁将永远被锁定,从这一点开始。

我希望onStartReading使用std::lock_guard来锁定互斥锁,以确保它在之后被解锁。如果您需要在整个ReadFiles中锁定它,那么OnStartReading将需要使用unique_lock并返回它,或者ReadFiles移动到OnStartReading中。

进一步更新:

添加的代码越多,情况看起来就越糟糕:(

不能在一个线程中锁定互斥对象,在另一个线程中将其解锁

一般来说,您应该永远不要直接在互斥对象上调用lockunlock。如果可以,只需使用lock_guard,并将锁包含在单个作用域中。如果需要将锁与条件变量wait一起使用,请使用unique_lock。如果必须在一个作用域中锁定,在另一个作用区中解锁,请使用unique_lock并将其作为参数作为返回值传递。

m_canReadCondVar.wait的调用将不会返回,直到m_canRead被设置,或者m_quit被设置,它可以获取互斥锁。如果另一个线程持有互斥锁(暂时的,因为您应该只持有锁很短的时间),那么等待就无法返回。

RequestToLoadRegion是错误的。它应该设置标志,然后解锁互斥,而不是相反,否则m_canRead上会出现数据竞争和未定义的行为。然而,这里的互斥解锁无论如何都不能与OnStartReading中的锁配对,除非它们在同一线程中。

您的代码很好。这是我等待值的标准代码:

void
WaitCondition::wait() {
unique_lock<std::mutex> lock(myMutex);
while (!value) {
condVar.wait(lock);
}
}

等待condVar实际上会释放锁,直到等待完成。所以你很好。

(根据Raymond的建议编辑。)