C++ 自旋锁

C++ 自旋锁 (SpinLock) 实现分析

代码概述

#include <iostream> 
#include <atomic> 
#include <thread> 

class SpinLock { 
public: 
    void lock() { 
        while (flag.test_and_set(std::memory_order_acquire)); // 自旋等待,直到成功获取到锁 
    } 

    void unlock() { 
        flag.clear(std::memory_order_release); // 释放锁 
    } 

private: 
    std::atomic_flag flag = ATOMIC_FLAG_INIT; 
}; 

核心组件分析

1. 头文件说明

  • <iostream>: 标准输入输出库(本例未直接使用,但通常并发程序会用到)
  • <atomic>: C++11 原子操作库,提供线程安全的原子类型
  • <thread>: C++11 线程库,用于多线程编程

2. SpinLock 类结构

私有成员

std::atomic_flag flag = ATOMIC_FLAG_INIT;
  • std::atomic_flag: 是 C++ 中最基本的原子类型,代表一个布尔标志位
  • ATOMIC_FLAG_INIT: 宏定义,用于将 atomic_flag 初始化为 clear(false)状态
  • 必须通过初始化器列表或默认构造函数初始化,不能赋值初始化

公共方法

lock() 方法
void lock() {
    while (flag.test_and_set(std::memory_order_acquire)); // 自旋等待,直到成功获取到锁
}
  • test_and_set(): 原子操作,将 flag 设置为 true 并返回操作前的值
  • 自旋等待:当 flag 已被设置(锁被占用),循环继续;当 flag 未被设置(锁可用),设置为 true 并退出循环
  • std::memory_order_acquire: 内存获取序,确保后续读取操作不会被重排到该操作之前
unlock() 方法
void unlock() {
    flag.clear(std::memory_order_release); // 释放锁
}
  • clear(): 原子操作,将 flag 设置为 false
  • std::memory_order_release: 内存释放序,确保之前的写入操作不会被重排到该操作之后

工作原理

  1. 初始化flag 被初始化为 ATOMIC_FLAG_INIT,即 clear 状态(false)
  2. 获取锁
    • 线程调用 lock() 方法
    • test_and_set() 原子地检查 flag 状态并设置为 true
    • 如果返回 false(之前是 clear),表示成功获取锁,退出循环
    • 如果返回 true(之前是 set),表示锁已被占用,继续循环(自旋等待)
  3. 释放锁
    • 线程调用 unlock() 方法
    • clear() 原子地将 flag 设置为 false
    • 其他自旋等待的线程可以获取锁

内存序的重要性

std::memory_order_acquire

  • 确保在获取锁后,对共享数据的读取操作不会被重排到锁获取之前
  • 保证了线程看到的是锁释放前的最新数据

std::memory_order_release

  • 确保在释放锁前,对共享数据的写入操作不会被重排到锁释放之后
  • 保证了线程对共享数据的修改对其他线程可见

自旋锁的优缺点

优点

  • 低延迟:没有上下文切换开销,适合锁持有时间短的场景
  • 简单实现:基于原子操作,实现简单可靠
  • 无死锁风险:只要线程最终会释放锁,就不会死锁

缺点

  • CPU 资源浪费:线程在自旋等待时会占用 CPU 资源
  • 优先级反转:高优先级线程可能被低优先级线程阻塞
  • 不适合锁持有时间长的场景:会导致大量 CPU 资源浪费

使用场景

  • 锁持有时间短:如保护简单的共享变量或短临界区
  • 多核 CPU:在多核环境下,自旋等待不会影响其他核心的线程
  • 低延迟要求:如实时系统或高性能计算

与其他锁的比较

锁类型 优点 缺点
自旋锁 低延迟、无上下文切换 CPU 资源浪费
互斥锁 不浪费 CPU 资源 上下文切换开销大
读写锁 支持并发读操作 实现复杂

代码优化建议

  1. 添加超时机制:避免无限自旋
  2. 指数退避:自旋一段时间后增加等待时间
  3. 结合线程优先级:确保高优先级线程优先获取锁

示例用法

#include <iostream>
#include <thread>
#include <vector>
#include "spinlock.h" // 假设 SpinLock 定义在这个头文件中

SpinLock spinLock;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        spinLock.lock();
        ++counter;
        spinLock.unlock();
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Counter: " << counter << std::endl; // 应该输出 1000000
    return 0;
}

总结

这段代码实现了一个基于 std::atomic_flag 的自旋锁,通过原子操作和内存序保证了多线程环境下的线程安全。自旋锁适合锁持有时间短、低延迟要求的场景,但会消耗 CPU 资源。理解内存序的作用对于正确使用原子操作和实现线程同步至关重要。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容