使用std::lock (c++11)导致的大量CPU负载

7 浏览
0 Comments

使用std::lock (c++11)导致的大量CPU负载

我最近在实现一个线程/互斥锁管理器的过程中,发现CPU负载达到了75%(4核),而所有四个运行的线程要么处于睡眠状态,要么等待一个互斥锁被解锁。

特定的类太大了,无法在这里完整地发布,但我可以将问题缩小到两个互斥锁死锁安全的获取:

std::unique_lock lock1( mutex1, std::defer_lock );
std::unique_lock lock2( mutex2, std::defer_lock );
std::lock( lock1, lock2 );

该类的另一部分使用了std::condition_variable,在mutex1上使用wait()notify_one(),选择性地执行一些代码。

将简单的更改为:

std::unique_lock lock1( mutex1 );
std::unique_lock lock2( mutex2 );

将CPU使用率降至正常的1-2%。

我不敢相信std::lock()函数会这么低效。这可能是g++ 4.6.3的一个错误吗?

编辑:(示例)

#include 
#include 
#include 
#include 
#include 
#include 
std::mutex mutex1, mutex2;
std::condition_variable cond_var;
bool cond = false;
std::atomicdone{false};
using namespace std::chrono_literals;
void Take_Locks()
    {
    while( !done )
        {
        std::this_thread::sleep_for( 1s );
        std::unique_lock lock1( mutex1, std::defer_lock );
        std::unique_lock lock2( mutex2, std::defer_lock );
        std::lock( lock1, lock2 );
        std::this_thread::sleep_for( 1s );
        lock1.unlock();
        lock2.unlock();
        }
    }
void Conditional_Code()
    {
    std::unique_lock lock1( mutex1, std::defer_lock );
    std::unique_lock lock2( mutex2, std::defer_lock );
    std::lock( lock1, lock2 );
    std::cout << "t4: waiting \n";
    while( !cond )
        cond_var.wait( lock1 );
    std::cout << "t4: condition met \n";
    }
int main()
    {
    std::thread t1( Take_Locks ), t2( Take_Locks ), t3( Take_Locks );
    std::thread t4( Conditional_Code );
    std::cout << "threads started \n";
    std::this_thread::sleep_for( 10s );
    std::unique_lock lock1( mutex1 );
    std::cout << "mutex1 locked \n" ;
    std::this_thread::sleep_for( 5s );
    std::cout << "setting condition/notify \n";
    cond = true;
    cond_var.notify_one();
    std::this_thread::sleep_for( 5s );
    lock1.unlock();
    std::cout << "mutex1 unlocked \n";
    std::this_thread::sleep_for( 6s );
    done = true;
    t4.join(); t3.join(); t2.join(); t1.join();
    }

0
0 Comments

大量使用std::lock (c++11)导致的CPU负载问题的原因是,当互斥锁被其他线程长时间持有时,无法通过等待而不进行自旋来提高效率。在这种情况下,函数无法等待而不进行自旋。

解决这个问题的方法是使用一个高效的开源实现,该实现在高争用场景中经过测试并被证明非常高效。这个实现的链接是:llvm.org/svn/llvm-project/libcxx/trunk/include/mutex。这个实现在失败后进行了yield并尝试锁定/阻塞之前失败的互斥锁,以减少自旋的开销。

现代操作系统在一个线程因为被锁定的互斥锁而被阻塞时,会将其挂起。如果有可用的核心可以运行线程,则我所知道的所有操作系统都不会将准备就绪的线程挂起。

然而,如果有更多的核心而没有准备就绪的线程,yield操作将不起作用。这个实现是糟糕的,并且这并不是实现的问题,而是规范没有给它选择的机会。当两个线程以锁步方式运行,一个线程执行lock(a,b),另一个线程执行lock(b,a)时,它们可能会一直自旋。

为了证明这一点,可以尝试创建一个活锁(livelock)。可以编写合理的代码,与该函数在相同的两个锁上同时运行,会导致荒谬的CPU消耗,至少是普通方式下的两倍。

在没有展示彼此代码的情况下,很难继续这个讨论。建议如何处理?

我很想看到那段代码和解释。

我决定将这个问题作为一个独立的问答主题:stackoverflow.com/questions/18520983。到目前为止,所有的答案都不同意我的观点,但我同意David的观点:按照规范,没有合理的方法来实现这个函数。(在我的世界中,任何形式的忙等待都是“不合理”的...)

正确。轮询锁是没有希望的。浪费CPU,浪费内存带宽和延迟。

0
0 Comments

使用std::lock()函数可能会导致大量的CPU负载,它只能保证“永远不会发生死锁”,但可能会导致活锁问题或性能下降。

如果您可以通过设计确定多个互斥锁的“锁顺序(锁层次结构)”,则最好不要使用通用的std::lock()函数,而是按预定的顺序锁定每个互斥锁。

有关更多详细信息,请参考"Acquiring Multiple Locks Without Deadlock"。

这基本上是我最终采用的方法,直到出现一种高效且确定性的(条件)锁定方法。

0
0 Comments

在我的机器上,以下代码每秒打印出10次,并且几乎不消耗CPU,因为大部分时间线程要么在睡眠,要么在被锁定的互斥锁上阻塞:

#include 
#include 
#include 
#include 
using namespace std::chrono_literals;
std::mutex m1;
std::mutex m2;
void f1()
{
    while (true)
    {
        std::unique_lock l1(m1, std::defer_lock);
        std::unique_lock l2(m2, std::defer_lock);
        std::lock(l1, l2);
        std::cout << "f1 has the two locks\n";
        std::this_thread::sleep_for(100ms);
    }
}
void f2()
{
    while (true)
    {
        std::unique_lock l2(m2, std::defer_lock);
        std::unique_lock l1(m1, std::defer_lock);
        std::lock(l2, l1);
        std::cout << "f2 has the two locks\n";
        std::this_thread::sleep_for(100ms);
    }
}
int main()
{
    std::thread t1(f1);
    std::thread t2(f2);
    t1.join();
    t2.join();
}

输出结果:

f1 has the two locks
f2 has the two locks
f1 has the two locks
...

我在OS X上运行这个程序,活动监视器应用程序显示该进程使用了0.1%的CPU。这台机器是一台Intel Core i5(4核)。

如果这个程序在您的平台上使用了过多的CPU,请尝试改为调用`::lock()`,其中定义如下:

template 
void lock(L0& l0, L1& l1)
{
    while (true)
    {
        {
            std::unique_lock u0(l0);
            if (l1.try_lock())
            {
                u0.release();
                break;
            }
        }
        std::this_thread::yield();
        {
            std::unique_lock u1(l1);
            if (l0.try_lock())
            {
                u1.release();
                break;
            }
        }
        std::this_thread::yield();
    }
}

我很想知道这对您有没有任何影响,谢谢。

经过很长时间的等待,我写了一篇关于这个主题的初稿。这篇论文比较了4种不同的完成这个任务的方法。它包含了您可以复制和粘贴到自己的代码中并进行测试的软件(请回报您的发现!):

[http://howardhinnant.github.io/dining_philosophers.html](http://howardhinnant.github.io/dining_philosophers.html)

在我的机器上,这个例子大部分时间以不交替的方式打印出一系列的f1/f2 "...has the two locks"消息。(gcc 4.6.3)

行为可能因操作系统或C++实现的不同而异。我在Linux上使用libstdc++时看到了您描述的行为,但在OS X上使用libc++时看到了交替的f1/f2。

在OS X上,我看到大部分是交替的f1/f2,但并不完全是这样。偶尔会打破这个模式。如果您缩短睡眠时间,这种情况会更加频繁。无论如何,f1/f2锁定的确切顺序在这里并不是一个相关的问题。主要的问题是对David Schwartz的断言“`std::lock`不能以高效的方式实现”提出异议。也就是说,我强烈反驳:“没有办法让函数等待而不旋转”。并提供这个实验作为反驳的证据,并邀请任何人贴出旋转的代码。

有趣的是,OS X似乎拥有更“先进”的实现。

我注意到,当我在VS2012下运行这个程序时,CPU使用率很高;在4核机器上为25%。如果我改变它不使用延迟锁定(并以一致的顺序获取锁),那么CPU使用率几乎为零。我试图阅读`std::lock`的实现以找出原因,但它太混乱了。

我在Linux上确实让这段代码旋转,通过在`main()`中锁定一个互斥锁。但是只保持两个线程时,我做了以下更改:

1)在`f1()`和`f2()`中的`lock()`之前添加一个短暂的睡眠来交替顺序(参见我的第一个评论)。

2)一个线程中只锁定一个互斥锁,而在另一个线程中锁定两个互斥锁。

3)由于我的CPU监视器的刷新率较低,我将锁定单个互斥锁后的睡眠持续时间增加到500毫秒。现在,代码产生高达30%的CPU负载峰值。

我使用`std::thread`以保持最大的可移植性。在您使用VS2012的经验之后,我对决定重新编写我的代码并避免延迟锁定感到非常高兴。

使用更新后的`::lock()`函数将CPU使用率降到了可接受的值。

我知道,基本上这是您在第一个评论中发布的链接中的实现。但由于缺乏理解,我无法进行测试。

请考虑这段代码捐赠给公共领域。对于非libc++的实现,可能需要进行性能错误报告。

根据原样,这段代码在返回时释放一个锁。当调用返回时,它们应该都保持锁定状态,不是吗?

实际上,`std::unique_lock::release()`函数会解除与互斥锁的关联,而不会解锁它。(否则,当互斥锁仍然关联时,unique_lock将超出范围,它的析构函数将解锁互斥锁。)

好的,明白了。我仍然不确定它是否适用于其他情况,但对于两个互斥锁来说,这是一个很好的实现。(一个糟糕规范的好实现)

如果您想要了解如何在N(N>2)个互斥锁上有效地实现此操作,请参阅开源代码libc++([libcxx.llvm.org](http://libcxx.llvm.org))。该规范在不需要特殊努力的情况下可以高效实现。它已经在N == 8的情况下进行了性能测试(结果良好)。如果您需要同时锁定更多的互斥锁,请告诉我您的用例,我很愿意了解。

小问题:为什么在获得第二个锁之后释放第一个锁?难道餐厅哲学家不需要两个叉子才能进餐吗?

`unique_lock::release()`函数释放锁的所有权,而不是解锁它。所以局部变量`u0`负责锁定`l0`,如果`l1.try_lock()`抛出异常或返回false,`u0`负责解锁`l0`。如果`l1.try_lock()`成功,现在`l0`和`l1`都被锁定。任务完成。现在只剩下通知`u0`它不再负责`l0`的问题。调用方现在负责`l0`和`l1`。

哦,对了。我犯了个愚蠢的错误。谢谢。

在我的Ubuntu 14.04 64位上,使用gcc 4.9.2,一开始有很多f2(只有它们),然后几分钟后我只得到了f1。它们之间没有任何的交替。

这是使用我回答中的`lock`实现还是gcc的`std::lock`实现?如果是后者,请尝试前者。

使用这个代码的版本,结果基本上和我上面描述的一样(一些其他的随机顺序,但大部分是很多的2和很多的1,变化非常少)。

如果有可能,您可以把代码发布在某个地方吗?我无法想象您希望它的样子是什么样。

[codepad.org/igAI2mSb](http://codepad.org/igAI2mSb)。这个想法是看看您所看到的行为是来自试图锁定两个互斥锁的算法,还是来自不受此实验控制的线程调度程序。

这段代码的行为与我上面描述的一样。感谢您进行这个测试。对我来说,这似乎表明,在您的系统上,任何使用两个锁定的算法都无法改变您所看到的行为。

0