实现一个类似于 Qt 的高性能互斥体

2024-03-19

我有一个多线程科学应用程序,其中多个计算线程(每个核心一个)必须将其结果存储在公共缓冲区中。这就需要互斥机制。

工作线程只花费一小部分时间写入缓冲区,因此互斥锁在大多数时间都处于解锁状态,并且锁定很有可能立即成功,而无需等待另一个线程解锁。

目前,我已经使用 Qt 的 QMutex 来完成该任务,并且效果很好:互斥体的开销可以忽略不计。

但是,我必须将其仅移植到 c++11/STL。使用 std::mutex 时,性能下降 66%,并且线程大部分时间都在锁定互斥体。

在另一个问题之后,我认为 Qt 使用基于简单原子标志的快速锁定机制,针对互斥锁尚未锁定的情况进行了优化。当发生并发锁定时,会回退到系统互斥体。

我想用STL来实现这个。有没有一种基于 std::atomic 和 std::mutex 的简单方法?我已经深入研究了 Qt 的代码,但它对我的使用来说似乎过于复杂(我不需要锁超时、pimpl、小占用空间等......)。

编辑:我尝试过自旋锁,但这效果不佳,因为:

另一个线程会定期(每隔几秒)锁定互斥体并刷新缓冲区。这需要一些时间,因此所有工作线程此时都会被阻塞。自旋锁使调度变得繁忙,导致刷新速度比使用适当的互斥锁慢 10-100 倍。这是不可接受的

编辑:我已经尝试过这个,但它不起作用(锁定所有线程)

class Mutex
{
public:
    Mutex() : lockCounter(0) { }

    void lock()
    {
        if(lockCounter.fetch_add(1, std::memory_order_acquire)>0)
        {
            std::unique_lock<std::mutex> lock(internalMutex);
            cv.wait(lock);
        }
    }

    void unlock();
    {
        if(lockCounter.fetch_sub(1, std::memory_order_release)>1)
        {
            cv.notify_one();
        }
    }


private:
    std::atomic<int> lockCounter;
    std::mutex internalMutex;
    std::condition_variable cv;
};

Thanks!

编辑:最终解决方案

MikeMB 的快速互斥体运行得很好。

作为最终的解决方案,我做了:

  • 使用带有 try_lock 的简单自旋锁
  • 当一个线程尝试锁定失败时,它们不会等待,而是填充一个队列(不与其他线程共享)并继续
  • 当线程获得锁时,它会使用当前结果以及队列中存储的结果更新缓冲区(它处理其队列)
  • 缓冲区刷新变得更加高效:阻塞部分仅交换两个指针。

一般建议

正如一些评论中提到的,我首先看看您是否可以重构您的程序设计,以使互斥体实现对您的性能不再那么重要。
此外,由于标准 C++ 中的多线程支持相当新,而且有些幼稚,因此有时您只需要依靠特定于平台的机制,例如AfutexLinux 系统上的关键部分或 Windows 上的关键部分或 Qt 等非标准库。
话虽这么说,我可以想到两种可能加速你的程序的实现方法:

Spinlock
如果访问冲突很少发生,并且互斥锁仅保留很短的时间(当然,无论如何我们都应该努力实现这两件事),那么仅使用自旋锁可能是最有效的,因为它不需要任何系统完全调用并且实现起来很简单(取自参考参数 http://en.cppreference.com/w/cpp/atomic/atomic_flag):

class SpinLock {
    std::atomic_flag locked ;
public:
    void lock() {
        while (locked.test_and_set(std::memory_order_acquire)) { 
             std::this_thread::yield(); //<- this is not in the source but might improve performance. 
        }
    }
    void unlock() {
        locked.clear(std::memory_order_release);
    }
};

当然,缺点是等待线程不会保持睡眠状态并窃取处理时间。

检查锁定

这本质上就是您演示的想法:您首先进行快速检查,基于原子交换操作是否确实需要锁定,并使用重型std::mutex仅当它不可避免时。

struct FastMux {
    //Status of the fast mutex
    std::atomic<bool> locked;
    //helper mutex and vc on which threads can wait in case of collision
    std::mutex mux;
    std::condition_variable cv;
    //the maximum number of threads that might be waiting on the cv (conservative estimation)
    std::atomic<int> cntr; 

    FastMux():locked(false), cntr(0){}

    void lock() {
        if (locked.exchange(true)) {
            cntr++;
            {
                std::unique_lock<std::mutex> ul(mux);
                cv.wait(ul, [&]{return !locked.exchange(true); });
            }
            cntr--;
        }
    }
    void unlock() {
        locked = false;
        if (cntr > 0){
            std::lock_guard<std::mutex> ul(mux);
            cv.notify_one();
        }
    }
};

请注意,std::mutex没有被锁定在中间lock() and unlock()但它仅用于处理条件变量。如果互斥锁上存在严重拥塞,这会导致更多的锁定/解锁调用。

您的实施的问题是cv.notify_one();可以在之间调用if(lockCounter.fetch_add(1, std::memory_order_acquire)>0) and cv.wait(lock);所以你的线程可能永远不会醒来。

不过,我没有与您建议的实现的固定版本进行任何性能比较,因此您只需看看什么最适合您。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

实现一个类似于 Qt 的高性能互斥体 的相关文章

随机推荐