C++ atomic 原子操作

一、原子操作简介

在多线程编程中,通常我们需要多个线程共享数据。但如果不加以控制,多个线程同时访问同一数据,可能会导致数据不一致或竞争条件(race conditions)。为了解决这个问题,我们引入了 原子操作

原子操作 是指在执行时不会被中断、打断或修改的操作。换句话说,原子操作要么完全执行,要么完全不执行,不会在中间被其他线程的操作干扰。这种特性对于多线程环境中的数据访问是至关重要的。

C++11 引入了 <atomic> 库,它提供了对原子操作的支持。通过这个库,我们可以确保在多线程环境中对数据的访问不会导致竞争条件。

二、C++ 原子操作的基本概念

在 C++ 中,原子操作的关键是 原子类型(atomic types)。原子类型是由 std::atomic 模板类提供的,它包装了一个类型,确保对该类型的所有操作(如读取、写入、更新等)都是原子的。

关键概念

  1. 原子类型 (std::atomic):可以包装任何基本类型(如 intboolfloat 等),并保证对这些类型的操作是原子的。
  2. 原子操作:对原子变量的操作是不可分割的,意味着在多线程中不会被打断。
  3. 原子变量:一个变量可以被声明为原子类型(如 std::atomic<int>),它保证在多线程环境下对该变量的操作是安全的。

常用的原子操作

  1. load:读取原子变量的值。
  2. store:将一个值存储到原子变量中。
  3. exchange:将原子变量的值替换为一个新值,并返回旧值。
  4. compare_exchange_weak / compare_exchange_strong:原子地进行条件交换操作。若当前值等于预期值,则交换新值。
  5. fetch_add / fetch_sub:原子地执行加法或减法操作,并返回旧值。

三、std::atomic 类模板

std::atomic 是一个模板类,可以用于包装基础数据类型,确保对该数据类型的所有操作都是原子性的。

基本使用

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;

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

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

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.push_back(std::thread(increment));  // 启动4个线程
    }

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

    cout << "Counter value: " << counter.load() << endl;  // 输出最终结果
    return 0;
}

常用原子操作的详细介绍

1. load() 和 store()

  • load():从原子变量读取值。
  • store():将值存储到原子变量中。
std::atomic<int> a(10);
int value = a.load();   // 获取原子变量a的值
a.store(20);            // 将原子变量a的值设置为20

2. exchange()

exchange 会将原子变量的值更新为给定的值,并返回原先的值。常用来在并发环境下进行“交换”操作。

std::atomic<int> a(5);
int old_value = a.exchange(10); // 将a的值设置为10,并返回旧值5

3. fetch_add() 和 fetch_sub()

  • fetch_add():执行原子加法操作,并返回旧值。
  • fetch_sub():执行原子减法操作,并返回旧值。
std::atomic<int> a(5);
int old_value = a.fetch_add(1);  // 将a加1,并返回旧值
// a的值现在是6

4. compare_exchange_weak() 和 compare_exchange_strong()

这两个函数会比较当前原子变量的值和预期值。如果相同,就将原子变量的值设置为新值。否则,操作失败并返回 false

  • compare_exchange_weak():若失败,可能会重新尝试,性能相对较高。
  • compare_exchange_strong():若失败,会返回 false,并且不再尝试。
std::atomic<int> a(5);
int expected = 5;
if (a.compare_exchange_weak(expected, 10)) {
    // 如果a的值是5,设置为10
    std::cout << "Value changed!" << std::endl;
} else {
    std::cout << "Value not changed!" << std::endl;
}

5. memory_order

std::atomic 的操作可以指定不同的内存顺序(memory ordering),控制不同线程之间的操作顺序。这对于高效并发编程非常重要。常见的内存顺序有:

  • memory_order_relaxed:不保证其他线程与该线程的操作顺序。
  • memory_order_consume:保证后续操作依赖于当前操作。
  • memory_order_acquire:保证所有的读取操作不会在当前操作之前执行。
  • memory_order_release:保证所有的写操作不会在当前操作之后执行。
  • memory_order_acq_rel:同时保证 acquire 和 release。
  • memory_order_seq_cst:最强的内存顺序,保证所有操作的顺序一致。

应用场景

  1. 计数器:
    原子计数器是一个典型的应用场景。多个线程可能会并发修改一个计数器,原子操作可以保证计数器值的一致性。

  2. 锁的实现:
    一些简化版的锁(如自旋锁)可以使用原子操作来减少性能开销。

  3. 无锁数据结构:
    通过原子操作可以设计一些高效的无锁数据结构(如无锁队列、栈等)。

  4. 状态标志:
    原子布尔值常常用来作为状态标志,例如线程是否完成,任务是否已开始等。

好的,以下是两道练习题目,可以帮助你更好地理解和掌握 C++ 中的原子操作和多线程编程。


四、练习题目

练习题 1: 使用 std::atomic 实现线程安全计数器

题目要求

  • 编写一个线程安全的计数器类,使用 std::atomic 来保证多个线程并发访问时的数据一致性。
  • 设计一个类 Counter,其中有一个原子整型成员变量 value
  • 提供 increment(增加)和 get_value(获取当前值)两个方法。
  • 创建多个线程并发地对计数器执行增加操作,最后输出计数器的最终值。

示例

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

using namespace std;

class Counter {
private:
    std::atomic<int> value;

public:
    Counter() : value(0) {}

    void increment() {
        value.fetch_add(1, std::memory_order_relaxed);
    }

    int get_value() const {
        return value.load(std::memory_order_relaxed);
    }
};

void increment_counter(Counter& counter, int times) {
    for (int i = 0; i < times; ++i) {
        counter.increment();
    }
}

int main() {
    Counter counter;

    // 创建多个线程并发增加计数器的值
    vector<thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.push_back(thread(increment_counter, ref(counter), 1000));
    }

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

    cout << "Final counter value: " << counter.get_value() << endl;
    return 0;
}

练习题 2: 使用 compare_exchange_strong 实现自旋锁

题目要求

  • 编写一个简单的自旋锁类,使用 std::atomic<bool> 来实现锁的原子操作。
  • 使用 compare_exchange_strong 来实现 lockunlock 操作。
  • main 函数中启动多个线程,模拟多个线程访问共享资源的场景,确保通过自旋锁控制资源的访问。

示例

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

using namespace std;

class SpinLock {
private:
    std::atomic<bool> flag;

public:
    SpinLock() : flag(false) {}

    void lock() {
        bool expected = false;
        while (!flag.compare_exchange_strong(expected, true)) {
            expected = false;  // 如果失败,重新尝试
        }
    }

    void unlock() {
        flag.store(false, std::memory_order_release);
    }
};

SpinLock spinlock;

void access_shared_resource(int thread_id) {
    spinlock.lock();
    cout << "Thread " << thread_id << " is accessing the shared resource." << endl;
    this_thread::sleep_for(chrono::milliseconds(100));  // 模拟处理过程
    cout << "Thread " << thread_id << " has finished accessing the resource." << endl;
    spinlock.unlock();
}

int main() {
    vector<thread> threads;
    
    // 创建多个线程模拟并发访问共享资源
    for (int i = 0; i < 5; ++i) {
        threads.push_back(thread(access_shared_resource, i));
    }

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

    return 0;
}

总结

  1. 练习题 1 帮助你练习如何使用 std::atomic 实现线程安全的计数器,理解原子操作对共享数据的保护作用。
  2. 练习题 2 让你掌握如何使用 compare_exchange_strong 来实现自旋锁,理解锁机制的基本原理。

五、进一步讨论

为什么不使用原子操作会导致统计混乱:

假设有两个线程同时执行如下代码:

g_count = g_count + 1;

在没有原子操作的情况下,可能会发生以下步骤:

  1. 线程 A 读取 g_count 的值,假设为 5。
  2. 线程 B 也读取 g_count 的值,仍然是 5。
  3. 线程 A 对 g_count 进行加 1 操作,得到新的值 6。
  4. 线程 B 也对 g_count 进行加 1 操作,得到新的值 6。

最终的 g_count 值应该是 7(5 + 1 + 1),但实际上由于没有原子操作,线程 A 和线程 B 的加法操作是并行进行的,导致最终结果错误(g_count 被错误地更新为 6)。

解决方法: 使用原子操作,如 std::atomic 来确保这些操作是 原子的,即每次修改操作都是不可分割的,避免数据竞争。

支持的原子操作:

原子操作一般支持的操作包括:

  • 基本算术操作++--+=-=&=, |=, ^= 等。
  • 这些操作是原子的,也就是说,当多个线程同时对 std::atomic<int> 进行增减或位操作时,编译器会确保每次操作是不可分割的。

示例

#include <iostream>
#include <atomic>
#include <thread>
using namespace std;

atomic<int> g_count = 0;

void mythread1() {
	for (int i = 0; i < 1000000; i++) {
		g_count++;
	}
}

int main() {
	std::thread t1(mythread1);
	std::thread t2(mythread1);
	t1.join();
	t2.join();
	cout << "正常情况下结果应该是2000000次,实际是" << g_count << endl;
}

不支持的操作:

并不是所有的操作都可以直接在原子变量上执行。例如,g_count = g_count + 1; 这种方式 不行,因为它首先会读取 g_count,然后执行加法并将结果写回 g_count,这个过程并不是原子操作。如果多个线程同时进行这种操作,可能会导致竞态条件,结果不正确。

为什么 g_count = g_count + 1; 不行:

  • g_count = g_count + 1; 这个表达式分为两步:

    1. 读取 g_count 的值。
    2. 将计算出的新值写回 g_count

    如果在这两步之间,另一个线程也读取并修改了 g_count 的值,最终可能导致结果错误。

  • 原子操作(如 fetch_addfetch_sub 等)可以保证这类操作是 不可分割 的,避免中间被打断。通过原子操作,线程会依次执行加法或减法操作,确保每次修改都正确地反映到内存中。

对自己进行操作:

一些原子类型(如 std::atomic<int>)只支持对自身进行修改的操作。这意味着,如果要执行某些复杂的表达式(例如 g_count = g_count + 1;),需要使用更复杂的原子操作(如 fetch_add),而不能直接使用加法或赋值操作符。

示例:

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

// 错误:不支持直接执行 g_count = g_count + 1;
g_count = g_count + 1;  // 编译错误

// 正确:使用原子操作
g_count.fetch_add(1, std::memory_order_relaxed);  // 原子增加1
g_count++;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值