文章

采用 std::atomic_flag 实现自旋锁

std::atomic_flag 是 c++ 标准库中的原子标志类,用于实现基本的原子操作。它提供了一种简单的机制来保护共享资源,可以用于实现自旋锁和其他线程同步的机制。std::atomic_flag 只能表示两个状态:设置(set)和清除(clear),并且保证对它的操作是原子的,避免了数据竞争和并发访问的问题。

什么是自旋锁

  • 自旋锁属于 busy-waiting 类型锁,它避免了操作系统进程调度和线程切换开销(用户进程和内核切换的消耗),通常适用在时间极短的情况,所以如果一个线程持有锁的时间比较短,那么就可以使用自旋锁; 操作系统的内核经常使用自旋锁。
  • 但如果长时间上锁,自旋锁会非常耗费性能。线程持有锁时间越长,则持有锁的线程被 OS调度程序中断的风险越大,如果发生中断情况,那么其它线程将保持旋转状态(反复尝试获取锁),而持有锁的线程并不打算释放锁,导致的结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。
  • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其他线程可以获得该自旋锁。实际上许多其他类型的锁在底层使用了自旋锁实现,例如多数互斥锁在试图获取锁的时候会先自旋一小段时间,然后才会休眠。如果在持锁时间很长的场景下使用自旋锁,则会导致CPU在这个线程的时间片用尽之前一直消耗在无意义的忙等上,造成计算资源的浪费。

自旋锁代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <atomic>

class SpinLock
{
private:
  std::atomic_flag flag_;

public:
  SpinLock() : flag_(ATOMIC_FLAG_INIT) {}
  ~SpinLock() = default;
  void lock()
  {
    // 获得锁 (test_and_set为设置当前lock为true,并返回lock设置之前的值)
    while (flag_.test_and_set(std::memory_order_acquire));
  }
  void unlock()
  {
    // 释放锁 (clear为设置lock的值为false)
    flag_.clear(std::memory_order_release);
  }
};

#include <iostream>
#include <thread>

SpinLock slck;

int main()
{
  std::thread t1([&]()
  {
    while (true)
    {
      slck.lock();
      std::cout << "线程1持有锁" << std::endl;
      slck.unlock();
    }
  });

  std::thread t2([&]()
  {
    while (true)
    {
      slck.lock();
      std::cout << "线程2持有锁" << std::endl;
      slck.unlock();
    }
  });
    t1.join();
    t2.join();
    return 0;
}
本文由作者按照 CC BY 4.0 进行授权

© ziqing. 保留部分权利。

纸上得来终觉浅,绝知此事要躬行!