无锁线程安全的方法

C++
Author

0warning0error

Published

June 29, 2024

在多线程编程中,确保线程安全是保证程序正确性和稳定性的关键。以下是一些常见的线程安全方法及其应用场景。

原子值(Atomic Values)

原子值是简单类型的对齐读取和写入通常是原子的。例如,int32int64 类型的变量。

常见应用场景

  • 双buffer切换时的指针赋值。
  • 共享内存中简单原子类型的变量修改。

优势

  • 性能高,且实现简单。

弊端

  • 无锁且实现简单,但仅限于较为简单的数据类型。

示例代码:使用 std::atomic 进行原子操作

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

std::atomic<int> counter(0); // 定义原子变量

void increment() {
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed); // 原子增加
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    
    std::cout << "Final counter value: " << counter.load() << std::endl; // 输出最终计数
    return 0;
}

简单原子变量(Atomic Variable)

通过编译器、语言等实现原子的 CPU 指令。

  • 原子类型的自增自减和立即赋值等,不强调调用后只要求最终结果的正确。

优势

  • 性能高,且实现简单。

弊端

  • 可能存在竞争较严重的时刻,自旋可能非常浪费 CPU。

示例代码:使用 std::atomic 进行自增操作

#include <iostream>
#include <atomic>

std::atomic<int> value(0);

void add_value() {
    for (int i = 0; i < 100; ++i) {
        value.fetch_add(1, std::memory_order_relaxed); // 原子自增
    }
}

int main() {
    std::thread t1(add_value);
    std::thread t2(add_value);
    t1.join();
    t2.join();
    
    std::cout << "Final value: " << value.load() << std::endl;
    return 0;
}

CAS(Compare-And-Swap)

CAS 是一种常见的原子操作,通常用于实现内存屏障,确保线程安全。

常见应用场景

  • 内存屏障中的关键操作。

优势

  • 性能高,且实现简单。

弊端

  • 对执行的先后顺序有较严格的要求。

示例代码:使用 std::atomic_compare_exchange_strong 实现 CAS

#include <iostream>
#include <atomic>

std::atomic<int> data(0);

void cas_test() {
    int expected = 0;
    int desired = 1;
    if (data.compare_exchange_strong(expected, desired)) {
        std::cout << "CAS succeeded, data set to: " << data.load() << std::endl;
    } else {
        std::cout << "CAS failed, data is still: " << data.load() << std::endl;
    }
}

int main() {
    std::thread t1(cas_test);
    std::thread t2(cas_test);
    t1.join();
    t2.join();
    return 0;
}

双buffer(Double Buffering)

简介

双buffer是一种在内存中保持两个缓冲区的技术,通常用于减少延迟。

常见应用场景

  • 双buffer切换时的指针赋值。

优势

  • 性能高,且实现简单。

弊端

  • 浪费内存空间。

示例代码:使用双缓冲技术

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

std::vector<int> buffer1(1000), buffer2(1000); // 两个缓冲区
std::atomic<bool> switch_buffer(false); // 缓冲区切换标志

void fill_buffer(std::vector<int>& buffer) {
    for (int i = 0; i < 1000; ++i) {
        buffer[i] = i;
    }
}

void process_buffer() {
    while (true) {
        if (!switch_buffer.load()) {
            fill_buffer(buffer1);
            switch_buffer.store(true);
        } else {
            fill_buffer(buffer2);
            switch_buffer.store(false);
        }
    }
}

int main() {
    std::thread t1(process_buffer);
    t1.join();
    return 0;
}

延迟删除双buffer(Double Buffering with Deferred Deletion)

简介

在更新后短期内使用双buffer,随后删除旧版本,并通过指针赋值的原子操作切换到新数据。

常见应用场景

  • 更新频繁的数据。

优势

  • 性能高,无需加锁。

弊端

  • 更新频率有限制。

示例代码:使用延迟删除

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

std::vector<int> buffer1(1000), buffer2(1000);
std::atomic<bool> use_buffer1(true); // 使用哪个缓冲区

void fill_buffer(std::vector<int>& buffer) {
    for (int i = 0; i < 1000; ++i) {
        buffer[i] = i;
    }
}

void process_buffer() {
    while (true) {
        if (use_buffer1.load()) {
            fill_buffer(buffer1);
            use_buffer1.store(false); // 切换到buffer2
        } else {
            fill_buffer(buffer2);
            use_buffer1.store(true); // 切换到buffer1
        }
    }
}

int main() {
    std::thread t1(process_buffer);
    t1.join();
    return 0;
}