自旋锁 | PHM's world

LOADING

歡迎來到烏托邦的世界

自旋锁

2024/5/15 基础

自旋锁

自旋锁是一种用于多线程编程的同步机制,其特点是当一个线程试图获取锁而锁已经被其他线程持有时,这个线程会不断地循环检查锁的状态(即“自旋”),直到获取到锁为止。自旋锁避免了线程在等待锁时被操作系统挂起,从而减少了上下文切换的开销,但也因此会占用CPU资源。自旋锁在短期锁定的情况下是高效的,但在锁定时间较长的情况下可能导致性能下降。

示例:

#include <atomic>
#include <thread>

class SpinLock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        // 自旋直到成功获取锁
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 在等待的过程中可以选择加入一些操作,
            // 比如让出CPU资源给其他线程
            std::this_thread::yield();
        }
    }

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

// 示例使用
SpinLock spinLock;

void critical_section() {
    spinLock.lock();
    // 在临界区进行操作
    // ...
    spinLock.unlock();
}

int main() {
    std::thread t1(critical_section);
    std::thread t2(critical_section);

    t1.join();
    t2.join();

    return 0;
}

上述例子中SpinLock 类使用了 std::atomic_flag 来实现自旋锁。lock 方法通过 test_and_set 方法不断尝试设置标志位,当其他线程释放锁时(即 flag.clear),标志位会被清除,此时 test_and_set 将返回 false,当前线程成功获取锁。unlock 方法则通过 clear 方法释放锁。

自旋锁适用于锁持有时间非常短的情况,因为自旋会占用CPU资源。如果锁持有时间较长,使用互斥量(如 std::mutex)更合适。

以下情况使用自旋锁合适:

1.临界区代码执行时间非常短:自旋锁最适合用于锁定时间极短的临界区。例如,临界区内只是进行一些简单的变量更新或快速检查。因为自旋锁在等待锁释放期间会一直占用CPU,如果临界区代码执行时间较长,自旋锁会导致CPU资源浪费,降低系统整体性能。

2.低锁争用的场景:在多线程程序中,如果锁的争用较低,即大多数情况下锁是可用的,自旋锁的性能会比较好。因为在这种情况下,线程很少需要自旋等待,大部分时间可以直接获取锁。

3.线程数量较少的情况:当程序中的线程数量较少时,自旋锁的效率会更高,因为线程间的竞争较少,线程在自旋时对CPU资源的争夺不会太激烈。

4.嵌入式系统或实时系统:在某些嵌入式系统或实时系统中,线程上下文切换的开销非常高,而自旋锁可以避免这种开销,从而在某些特定场景中提高系统的实时性和响应速度。

5.没有操作系统内核支持的环境:在某些裸机编程或者没有操作系统内核支持的环境中,自旋锁是唯一的选择,没有其他同步机制可以使用。

比如:
全局计数器,多个线程需要对这个计数器进行并发的递增操作。由于递增操作非常短暂,自旋锁在这个场景下是一个合适的选择。

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

// 自旋锁类
class SpinLock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            std::this_thread::yield(); // 让出CPU资源
        }
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

// 全局计数器和自旋锁
int counter = 0;
SpinLock spinLock;

void increment_counter(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        spinLock.lock();
        ++counter;  // 临界区
        spinLock.unlock();
    }
}

int main() {
    const int num_threads = 10;
    const int iterations = 1000;

    // 创建并启动线程
    std::vector<std::thread> threads;
    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back(increment_counter, iterations);
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    // 输出结果
    std::cout << "Final counter value: " << counter << std::endl;

    return 0;
}

需要注意的是,自旋锁通常不提供公平性(即等待时间长的线程不一定先获取锁),这可能导致某些线程长时间得不到锁,自旋锁可能带来线程饥饿的问题。

线程饥饿:(Thread Starvation)是指某些线程长时间无法获得所需的资源(如锁),导致这些线程无法正常执行或完成任务的现象。这通常是由于资源被其他线程长期占用或优先级机制不合理所导致的。在多线程编程中,线程饥饿可能会导致系统性能下降甚至死锁等严重问题。

但依旧要使用自旋锁的时候,可以使用线程队列来解决,例如:

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <queue>

class FairSpinLock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
    std::mutex mtx;
    std::condition_variable cv;
    std::queue<std::thread::id> waitQueue;

public:
    void lock() {
        std::thread::id this_id = std::this_thread::get_id();
        {
            std::lock_guard<std::mutex> lock(mtx);
            waitQueue.push(this_id);
        }

        while (true) {
            if (waitQueue.front() == this_id && !flag.test_and_set(std::memory_order_acquire)) {
                std::lock_guard<std::mutex> lock(mtx);
                waitQueue.pop();
                return;
            }
            std::this_thread::yield();  // 让出CPU资源
        }
    }

    void unlock() {
        flag.clear(std::memory_order_release);
        std::unique_lock<std::mutex> lock(mtx);
        cv.notify_all();
    }
};

int counter = 0;
FairSpinLock fairSpinLock;

void increment_counter() {
    for (int i = 0; i < 100; ++i) {
        fairSpinLock.lock();
        ++counter;
        std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 模拟长时间的临界区
        fairSpinLock.unlock();
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_counter);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}