互斥锁与自旋锁(用户态)的性能比较

高性能多线程编程中,频繁使用互斥锁会影响程序性能。某些情况下用自旋锁替代互斥锁也许是优化性能的好手段。

下文将简单的分析自旋锁和互斥锁各自的优势

无竞争的互斥锁

在常见的linux和glibc版本中,无竞争使用互斥锁并不会带来额外的系统调用。因为Futex技术,使得在线程能直接获取到锁得情况下无需使用系统调用,直接利用CPU原子指令进行加锁或解锁,故互斥锁在竞争小得场合并不会因为频繁进行系统调用而损耗性能。

#include <iostream>
#include <chrono>
#include <mutex>
#include <list>
#include <cstdint>
​
static constexpr size_t loop_max = 10000000;
static std::list<int> g_list;
​
template <typename LockType>
void test_lock(const char *lock_name)
{
    auto start = std::chrono::steady_clock::now();
    LockType locker;
    for (size_t i = 0; i < loop_max; i++)
    {
        std::lock_guard<LockType> lock_guard(locker);
        g_list.emplace_back(i);
        g_list.pop_back();
    }
    std::int64_t ms = std::chrono::duration_cast<std::chrono::microseconds>
                        (std::chrono::steady_clock::now() - start).count();
    std::cout << "lock_type   " << lock_name << "\n";
    std::cout << "elapsed     " << ms / 1000000.0 << "\n\n";
}
​
int main()
{
    test_lock<std::mutex>("std::mutex");
    return 0;
}
​

对以上代码进行编译测试,由于仅一个线程,故使用互斥锁不会出现竞争和等待,也不会进行系统调用

我们可以用strace命令进行确认,该程序未使用与锁相关的系统调用

有竞争的互斥锁

假设存在两个线程以以下顺序执行代码

Thread1Thread2
申请锁
得到锁
申请锁
释放锁
得到锁
释放锁
  1. Thread1申请锁时无需使用系统调用,用户态加锁
  2. Thread1得到锁
  3. Thread2申请锁时在用户态中发现该锁已经被申请,在锁对象中登记自己需要等待该锁,使用系统调用通知内核等待解锁
  4. Thread1释放锁时发现该锁对象有其他线程需要使用,解锁后使用系统调用通知内核拉起等待的线程
  5. Thread2得到锁
  6. Thread2释放锁,这时该锁不被任何一个线程等待,故无需进行系统调用

结论:linux下互斥锁的加锁和解锁尽可能在用户态完成,如果涉及到等待和唤醒操作,才会进行系统调用

应用层自旋锁实现

C++11提供了原子能力,此处可直接使用std::atomic_flag实现自旋锁。

注意:由于用户态不能关闭调度,使临界区内任务尽快做完。故系统可能会在临界区调度到其他线程,最坏的情况是其他线程不停的尝试获取这个锁,但因为获取到这个锁的线程还在等待下一次调度,导致白白浪费宝贵的CPU时间,故我们可以在lock()函数中插入一条std::this_thread::yield()防止这种情况。

#include <atomic>
#include <thread>
// 自旋锁的实现
class SpinLock {
public:
    SpinLock() = default;
    SpinLock(const SpinLock&) = delete;
    SpinLock& operator=(const SpinLock) = delete;
    void lock() {   // acquire spin lock
        while (flag.test_and_set(std::memory_order_acquire)) {
            std::this_thread::yield();
        }
    }
    void unlock() {   // release spin lock
        flag.clear(std::memory_order_release);
    }
private:
    atomic_flag flag;
};

无竞争下互斥锁和自旋锁性能测试

#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
#include <atomic>
#include <list>
#include <cstdint>
​
// 自旋锁的实现
class SpinLock {
public:
    SpinLock() = default;
    SpinLock(const SpinLock&) = delete;
    SpinLock& operator=(const SpinLock) = delete;
    void lock() {   // acquire spin lock
        while (flag.test_and_set(std::memory_order_acquire)) {
            std::this_thread::yield();
        }
    }
    void unlock() {   // release spin lock
        flag.clear(std::memory_order_release);
    }
private:
    atomic_flag flag;
};
​
static constexpr size_t loop_max = 10000000;
static std::list<int> g_list;
​
template <typename LockType>
void test_lock(const char *lock_name)
{
    auto start = std::chrono::steady_clock::now();
    LockType locker;
    for (size_t i = 0; i < loop_max; i++)
    {
        std::lock_guard<LockType> lock_guard(locker);
        g_list.emplace_back(i);
        g_list.pop_back();
    }
    std::int64_t ms = std::chrono::duration_cast<std::chrono::microseconds>
                        (std::chrono::steady_clock::now() - start).count();
    std::cout << "lock_type   " << lock_name << "\n";
    std::cout << "elapsed     " << ms / 1000000.0 << "\n\n";
}
​
int main()
{
    test_lock<std::mutex>("std::mutex");
    test_lock<SpinLock>("SpinLock");
    return 0;
}
​

运行输出

lock_type   std::mutex
elapsed     0.322497
​
lock_type   SpinLock
elapsed     0.251232

结论:虽然在无竞争情况下,互斥锁和自旋锁都不会进行系统调用,但自旋锁的实现更简单,性能更高

有竞争下互斥锁和自旋锁性能测试

#include <iostream>
#include <list>
#include <vector>
#include <chrono>
#include <thread>
#include <mutex>
#include <atomic>
#include <cstdint>
​
// 自旋锁的实现
class SpinLock {
public:
    SpinLock() = default;
    SpinLock(const SpinLock&) = delete;
    SpinLock& operator=(const SpinLock) = delete;
    void lock() {   // acquire spin lock
        while (flag.test_and_set(std::memory_order_acquire)) {
            std::this_thread::yield();
        }
    }
    void unlock() {   // release spin lock
        flag.clear(std::memory_order_release);
    }
private:
    atomic_flag flag;
};
​
​
static std::list<int> g_list;
static constexpr size_t loop_max = 10000000;
​
template <typename LockType>
void test_thread(size_t id, LockType &lock)
{
    for (size_t i = 0; i < loop_max; i++)
    {
        std::lock_guard<LockType> lock_guard(lock);
        g_list.push_back(id);
        g_list.pop_back();
    }
}
​
template <typename LockType>
void begin_test(size_t thread_number, const char *lock_name)
{
    std::vector<std::thread> threads;
    LockType lock;
​
    auto start = std::chrono::steady_clock::now();
    for (size_t i = 0; i < thread_number; i++)
    {
        threads.emplace_back([i, &lock](){
            test_thread(i, lock);
        });
    }
    for (auto &t : threads)
    {
        t.join();
    }
 
 
    std::int64_t ms = std::chrono::duration_cast<std::chrono::microseconds>
                        (std::chrono::steady_clock::now() - start).count();
    std::cout << "lock_type   " << lock_name << "\n";
    std::cout << "elapsed     " << ms / 1000000.0 << "\n\n";
}
​
int main()
{
    size_t nproc = std::thread::hardware_concurrency();
​
    std::cout << "nproc: " << nproc << "\n\n";
    for (size_t n = 1; n <= 2*nproc; n++)
    {
        std::cout << "=============== " << n << " ===============\n";
        begin_test<std::mutex>(n, "std::mutex");
        begin_test<SpinLock>(n, "SpinLock");
    }
    return 0;
}
​

运行输出

nproc: 4
​
=============== 1 ===============
lock_type   std::mutex
elapsed     0.032644
​
lock_type   SpinLock
elapsed     0.021863
​
=============== 2 ===============
lock_type   std::mutex
elapsed     0.221694
​
lock_type   SpinLock
elapsed     0.053253
​
=============== 3 ===============
lock_type   std::mutex
elapsed     0.242243
​
lock_type   SpinLock
elapsed     0.078929
​
=============== 4 ===============
lock_type   std::mutex
elapsed     0.242026
​
lock_type   SpinLock
elapsed     0.12438
​
=============== 5 ===============
lock_type   std::mutex
elapsed     0.38374
​
lock_type   SpinLock
elapsed     0.154654
​
=============== 6 ===============
lock_type   std::mutex
elapsed     0.446244
​
lock_type   SpinLock
elapsed     0.182802
​
=============== 7 ===============
lock_type   std::mutex
elapsed     0.395281
​
lock_type   SpinLock
elapsed     0.212078
​
=============== 8 ===============
lock_type   std::mutex
elapsed     0.37593
​
lock_type   SpinLock
elapsed     0.264617

结论:自旋锁性能好于互斥锁

为何自旋锁性能好于互斥锁

很多人说互斥锁会频繁进行系统调用,我认为这个说法是不正确的。

在竞争小的情况下,自旋锁和互斥锁均不会过多的进行系统调用。

而竞争大的情况下,自旋锁需要调用sched_yield,互斥锁需要调用futex。

互斥锁更慢的原因时其本身实现更复杂,除了要在用户态进行原子操作外,还要维护一个等待队列,记录哪些线程需要申请锁,以便于实现让权等待。但如果临界区内的任务非常轻量,甚至轻量到比加锁解锁的开销还小的情况下,再去维护这个队列的话,从开销上来看就不那么划算,反而直接使用自旋锁这种简单的机制能获得更高的效率。

怎么选择使用互斥锁还是自旋锁

通常情况下推荐使用互斥锁,在不追求极致性能的情况下,使用互斥锁往往就足够了。

在追求极致性能情况下,若临界区内任务很轻,能够很迅速的完成。在这种情况下可以考虑使用自旋锁,若不满足这个条件,使用自旋锁只会降低程序性能。

评论:

不调yield最大的问题在于用户态没法禁止调度,如果持有锁的线程在临界区内被操作系统剥夺CPU使用,调度给一个尝试拿锁的线程。那尝试拿锁的线程就只能不停的空转,直到耗尽本次调度的整个时间片。

我把yield去掉以后确实自旋锁慢了好几倍,但是我不理解的是,我只开了三个线程,我是8核心16线程的电脑,按理说这三个线程都不应该被切掉啊

很棒的测试,把我一直想做的测试做了。其实我一直在纠结自旋锁里到底要不要使用yield,如果不用,那就是期望自旋锁在空转的一个时间片内获取到锁。加了yield,就是期望在几个时间片内获取到锁,性价比才高,这就要求临界区负担小,而且竞争自旋锁的线程不能太多。如果临界区负担大,或者竞争线程比较多,还不如直接用mutex,把线程放到阻塞队列,等待唤醒,还不影响cpu轮询效率。真是太纠结了,平常使用的时候还是倾向于加了yield的自旋锁,因为任务大多执行很快,没必要使用mutex阻塞线程等待唤醒

  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值