互斥锁未解锁。

3

我在更新周期中不断从主线程调用getter函数来锁定和解锁变量,同时从另一个线程调用setter函数。我在下面提供了setter和getter代码的定义。

定义

bool _flag;
System::Mutex m_flag;

通话记录

#define LOCK(MUTEX_VAR) MUTEX_VAR.Lock();
#define UNLOCK(MUTEX_VAR) MUTEX_VAR.Unlock();

void LoadingScreen::SetFlag(bool value)
{
    LOCK(m_flag);
    _flag = value;
    UNLOCK(m_flag);
}

bool LoadingScreen::GetFlag()
{
    LOCK(m_flag);
    bool value = _flag;
    UNLOCK(m_flag);

    return value;
}

这个方法有一半的时间表现良好,但是有时候变量在调用SetFlag时会被锁定,因此永远不会被设置,从而干扰了代码的流程。

有没有人能告诉我如何解决这个问题?

编辑:

这是我最终采取的解决方法。这只是一个临时解决方案。如果有更好的答案,请告诉我。

bool _flag;
bool accessingFlag = false;

void LoadingScreen::SetFlag(bool value)
{
    if(!accessingFlag)
    {
        _flag = value;
    }
}

bool LoadingScreen::GetFlag()
{
    accessingFlag = true;
    bool value = _flag;
    accessingFlag = false;

    return value;
}

3
你能展示 LOCK()UNLOCK() 的定义吗? - Andy Prowl
@Adnan Akbar,你能否提供一种替代的方法? - glo
2
这些是唯一锁定互斥量的函数吗?显式解锁的最大危险(而不是使用标准库中的RAII锁类型)是,如果函数提前返回或抛出异常,则很容易将其永久锁定。但是,在您发布的代码中不会发生这种情况;如果这是使用互斥量的所有代码,则可能存在您的非标准System :: Mutex类中的错误。 - Mike Seymour
5
你是否考虑使用std::atomic<bool>?这样就不需要自己进行加锁了。 - stefan
3
去掉 LOCKUNLOCK。首先,编写一个 RAII 锁定器类:struct locker { System::Mutex* m; locker(System::Mutex& m_):m(&m_){m->Lock();} void unlock(){if (m)m->Unlock();} ~locker() {unlock();} };。将调用 LOCK(m_flag); 替换为 locker lock(m_flag);,在函数或其他作用域退出时应删除对 lockUNLOCK,在少数其他情况下,应替换为 lock.unlock();。这应该会减少泄漏到 m_flag 的锁的风险。 - Yakk - Adam Nevraumont
显示剩余15条评论
6个回答

2
你所遇到的问题(正如 user1192878 所提到的)是由于编译器延迟加载和存储造成的。你需要使用内存屏障来实现代码。你可以声明 volatile bool _flag;,但是对于单CPU系统,这并非必需,因为使用编译器内存屏障即可。在多CPU解决方案中需要使用硬件屏障(请参见下面的维基百科链接),以确保所有CPU都能看到本地处理器的内存/缓存。在这种情况下,不需要使用互斥锁和其他互锁机制。它们只会创建死锁,而且是没有必要的。
bool _flag;
#define memory_barrier __asm__ __volatile__ ("" ::: "memory") /* GCC */

void LoadingScreen::SetFlag(bool value)
{
    _flag = value;
    memory_barrier(); /* Ensure write happens immediately, even for in-lines */
}

bool LoadingScreen::GetFlag()
{
   bool value = _flag;
   memory_barrier(); /* Ensure read happens immediately, even for in-lines */
   return value;
}

互斥量仅在同时设置多个值时才需要。您还可以将bool类型更改为sig_atomic_tLLVM原子操作。但是,这有点迂腐,因为bool在大多数实际CPU架构上都可以工作。 Cocoa的并发页面也提供了一些有关执行相同操作的替代API的信息。我认为gcc的内联汇编与Apple的编译器使用的语法相同;但这可能是错误的。

API存在一些限制。 实例GetFlag()返回值后,其他东西可以调用SetFlag()。然后GetFlag()返回值就过时了。如果您有多个写入者,则很容易错过一个SetFlag()。如果高级逻辑容易出现ABA问题,则这可能很重要。但是,所有这些问题都存在/不存在互斥量。 内存屏障仅解决编译器/CPU不会在长时间内缓存SetFlag()并且它将在GetFlag()中重新读取值的问题。声明volatile bool flag通常会导致相同的行为,但会带来额外的副作用,并且无法解决多CPU问题。

std::atomic<bool>根据stefanatomic_set(&accessing_flag, true);通常会在其实现中执行与上述描述相同的操作。如果您的平台支持它们,则可以使用它们。


谢谢。看起来这个方法可行,详细的解释加一分。 - glo
@ruben2020:不是的,“内存屏障”是编译器和/或操作系统特定的。这就是为什么“C++11”指定了“std::atomic”,以便您可以以跨平台的方式执行它。如果您没有这个,维基百科链接中有参考资料,说明如何在每个平台上执行此操作(“C++11”库编写者会为您执行此操作)。也可能某些处理器设计会使“硬件内存屏障”变得不可能;但对于glo感兴趣的手机来说不是这种情况。 - artless noise

2
首先,对于互斥锁的锁定和解锁,您应该使用 RAII。其次,要么您未显示一些其他直接使用 _flag 的代码,要么是您正在使用的互斥体有问题(不太可能)。提供 System::Mutex 的库是什么?

@BillPringlemeir,如果互斥锁是您的问题,它与我的答案有什么关系呢? - Slava
RAII 给予了一个互斥锁是正常的假象。我想我的意思是你的第一句话应该是“如果需要使用互斥锁,你应该使用 RAII...”。在这种情况下,没有理由使用互斥锁。它们保护不了任何东西。它们以某种方式给人一种值已经同步的假象。实际上,像 pthread_cond_wait() 这样的东西可能是初始帖子所需的。然而,在某些情况下,只有原子读/写起作用,在这种情况下,不需要互斥锁。 - artless noise
@BillPringlemeir,你能看到同步使用不当是很好的。你有没有想过当我回答时可用信息要少得多? - Slava
是的,我考虑过了。有两种情况。getter/setter是“正确”的,而更高级别的是无锁的,并且需要内存屏障。第二种情况是更高级别的完全错误,需要类似于pthread_cond_wait()的东西。在任何情况下,RAII都解决不了任何问题;这个答案中没有其他建议,只有问题。 - artless noise

1
代码看起来没问题,如果 System::Mutex 被正确实现了的话。 需要提醒一下:
  1. 正如其他人指出的那样,RAII比宏更好。

  2. 最好将accessingFlag和_flag定义为volatile。

  3. 我认为你得到的临时解决方案如果使用优化编译是不正确的。

    bool LoadingScreen::GetFlag()
    {
      accessingFlag = true;  // 可能会被重新排序或删除
      bool value = _flag;  // 可能会被优化掉
      accessingFlag = false;    // 可能会在设置值之前重新排序
      return value;   // 可能会被优化为直接返回_flag或寄存器
    }
    
    在上面的代码中,优化器可能会做出讨厌的事情。例如,没有什么可以阻止编译器消除对accessingFlag=true的第一次赋值,或者它可能会被重新排序、缓存。例如,从编译器的角度来看,如果是单线程的,对accessingFlag的第一次赋值是无用的,因为true值从未被使用过。

  4. 使用互斥锁来保护单个布尔变量是昂贵的,因为大部分时间都花费在切换操作系统模式上(从内核到用户来回切换)。使用自旋锁可能不错(详细代码取决于目标平台)。应该像这样:

    spinlock_lock(&lock); _flag = value; spinlock_unlock(&lock);

  5. 此外,原子变量在这里也很好用。可能看起来像:
atomic_set(&accessing_flag, true);

0
您提供的第二个代码块可能会在单处理器设置下在读取标志时修改该标志,即使原始代码是正确的,在两个假设下也不会导致死锁:
  1. m_flags锁已正确初始化,并且未被任何其他代码修改。
  2. 锁实现是正确的。
如果您想要一个可移植的锁实现,我建议使用OpenMP: 如何在OpenMP中使用锁? 从您的描述中,似乎您想要忙等待线程处理一些输入。在这种情况下,stefans解决方案(声明标志std :: atomic)可能是最好的。在半理智的x86系统上,您还可以声明标志volatile int。只是不要为不对齐的字段(紧凑结构)这样做。
您可以使用两个锁来避免忙等待。当从主线程等待从属完成时,第一个锁被从属解锁并由主线程锁定。当提供输入时,主线程解锁第二个锁,并由从属锁定等待输入。

0
你考虑过使用CRITICAL_SECTION吗?它只在Windows上可用,所以你会失去一些可移植性,但它是一个有效的用户级别互斥量。

我可以尝试一下,但我想要能在Linux和Mac上使用的东西。 - glo

-1

这是一种我曾经在某处看到过的技巧,但现在无法找到来源了。如果我找到了,我会编辑答案。基本上,作者只会写入,但读者会多次读取设置变量的值,只有当所有副本都一致时,才会使用它。而我已经改变了作者的方式,使其尽可能地保持写入该值,只要它不匹配所期望的值。

bool _flag;

void LoadingScreen::SetFlag(bool value)
{
    do
    {
       _flag = value;
    } while (_flag != value);
}

bool LoadingScreen::GetFlag()
{
    bool value;

    do
    {
        value = _flag;
    } while (value != _flag);

    return value;
}

为什么你要在SetFlag()中循环?如果有多个写入者,这可能会导致与帖子相同的死锁。这是API固有的问题,无法修复。想象一下两个CPU在SetFlag()中不断争夺不同的值。然后在GetFlag()中,我们希望变量不改变多长时间?同样,这是setter/getter API中无法解决的限制。 - artless noise
我觉得给你-1很不好。虽然答案是错误的,但它比其他一些答案更有思考性。我不得不花一点时间来思考你的解决方案。 - artless noise
我认为对于布尔值来说并没有太大的区别,但如果更复杂,例如需要保持一致的两个变量 - 小时和分钟 - 那么编写者和读者都需要确保一致性。虽然看起来我们会进入一个循环,但我认为在多线程单核应用程序中这不太可能发生,因为每次只有一个线程运行并最终获胜。但如果它在多个核心上同时运行,那么可能会出现问题。因此,最好使用RAII、原子变量或其他方法。 - ruben2020
问题是“为什么要循环”?这与“为什么要使用互斥锁”相同吗?如果循环速度很快,您会在退出时得到一个过时的值。也就是说,“value”和“flag”何时允许不相等?当方法返回时它们可能不相等。那么为什么要检查呢?互斥锁也是一样。您暂时将它们正确,但对于调用者来说它们可能是不正确的。因此,接口要么有问题,要么循环/互斥锁不需要并引入了额外的复杂性。 - artless noise

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接