【问题标题】:Does std::mutex favor the thread that owns it?std::mutex 是否偏爱拥有它的线程?
【发布时间】:2019-08-28 13:52:54
【问题描述】:

我试图了解spinlock mutex 的工作原理, 所以我写了一个简单的代码(如下所示)来测量指令的交错 受自旋锁(或 std::) 互斥锁保护的不同线程。

令人惊讶的是,它显示(至​​少在 gcc 中)std::mutex(与自旋锁互斥锁相反) 似乎有利于拥有它的线程,导致非常小的指令交错(最多 5%), 除非有问题的指令非常快(比如增加一个计数器)。 在这种情况下,我们甚至可以得到 50%。自旋锁互斥体提供至少 80%(通常超过 90%)。

这是众所周知的事实吗?或者我下面的代码可能有错误?

我的意思是,我知道一个经验法则,即互斥体应始终锁定最短时间。 但我确信确实如此,因为我们想减少线程的序列化, 而不是因为 std::mutex 偏爱拥有线程...

代码如下:

#include<atomic>
#include<thread>
#include<iostream>
#include<chrono>
#include<mutex>


class SpinLockMutex{
    std::atomic_flag m_flag = ATOMIC_FLAG_INIT;
public:
    void lock()   { while( m_flag.test_and_set(std::memory_order_acquire) )  /*do nothing*/ ; }
    void unlock() { m_flag.clear(std::memory_order_release) ; }
};//class SpinLockMutex


// ******************************************
// // // std::mutex vs SpinLockMutex 
//SpinLockMutex globalMutex;
std::mutex globalMutex;
// ******************************************


// This class helps to start threads at the same time : 
class Starter{
    mutable std::mutex m_m;
    bool m_ready = false;
public:
    bool isReady() const {  std::lock_guard<std::mutex> guard(m_m); 
                return m_ready; 
                 }  

    void start() { std::this_thread::sleep_for(std::chrono::seconds(3)); 
                       std::lock_guard<std::mutex> guard(m_m); 
               m_ready = true; 
             }  
};//class Starter


constexpr std::size_t   LOOP_SIZE                = 100;
std::size_t             previous_thread_repeated = 0;
Starter                 starter;


void mainFcnForThread ()
{   
    static std::thread::id  previous_thread_id = std::this_thread::get_id();

    while(!starter.isReady()) 
        ; //do nothing

    for(std::size_t i = 0; i!=LOOP_SIZE ; ++i){
        globalMutex.lock();
        if(previous_thread_id == std::this_thread::get_id() ) {
            ++previous_thread_repeated;
            std::this_thread::sleep_for(std::chrono::microseconds(100));
        }
        previous_thread_id = std::this_thread::get_id();            
        globalMutex.unlock();
    }
}//void mainFcnForThread


int main()
{

    std::thread t1(mainFcnForThread);
    std::thread t2(mainFcnForThread);

    starter.start();
    t1.join();
    t2.join();  

    std::cout << double(previous_thread_repeated)/(2*LOOP_SIZE) << '\n';

    return 0;
}

【问题讨论】:

  • 请注意sleep_for(t) 至少会休眠t,但没有上限。
  • 操作系统可能已经取消了这些线程的优先级,这些线程都在等待 starter 大约 3 秒。更公平的测试可能是使用适当的信号,例如两个线程的条件变量来发出信号,然后启动器可以立即释放它们。无论哪种方式,涉及跨越 10 微秒的 100 次迭代的测试听起来都不够。如果您运行测试一分钟,您会得到相同的结果吗?
  • 通常它是首选的,因为它仍然在运行,不像其他正在等待/休眠的线程。
  • 锁是否公平在一定程度上取决于操作系统,Linux 和 Windows 都随着时间而改变。
  • 仅供参考:您的 for 循环几乎 100% 地保持互斥锁锁定。如果您的目标是演示互斥锁的属性,这可能是一个有用的技巧,但在任何实际程序中它几乎总是一个坏主意。最好的多线程程序被设计成不需要经常使用互斥锁,并且不会让任何互斥锁锁定超过程序总运行时间的一小部分。

标签: c++ multithreading


【解决方案1】:

Mutex 对公平性做出零保证。

解锁互斥锁不会暂停当前线程。尝试锁定互斥锁并不是说“等等,其他人已经等了更长的时间,他们应该尝试一下”。

阻塞互斥锁有时会使您的线程进入休眠状态。

解锁互斥锁后,您就不再是“拥有线程”了。你可能是一个正在运行的线程。并且互斥锁可以(并且显然确实)有利于运行线程而不是挂起的线程。

可以在 C++ 同步原语之上实现“公平”,但它不是免费的,C++ 的目的不是让你为你不要求的任何东西付费。

【讨论】:

  • 这是有道理的。非常感谢。您的句子“等等,其他人已经等了更长的时间……”让我想知道一些完全不相关的事情。 std::shared_mutex 是否优先考虑写入器线程,以便它们不等待(可能大量)读取器完成?例如,QReadWriteLock 就是 std::shared_mutex 的 Qt 对应物。
  • @Adrian 我不知道,但这听起来像是一个合理的问题。
  • 您可能还指出,在多CPU主机上,等待OP的SpinLockMutex的线程很有可能同时是一个正在运行的线程,并且它很有可能获胜与任何其他需要互斥锁但尚未进入lock() 方法的线程竞争。这可能是SpinLockMutexstd::mutex 执行如此不同的原因:等待std::mutex() 任何相当长的时间的线程都将被移出运行队列。
  • @Solomon Slow 这也有道理:) 感谢两位 cmets。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-12-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多