【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用

简介: 【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用

1. 引言

并发编程中,数据竞争(Data Race)是一个常见的问题。为了解决这个问题,C++11引入了一个新的库类型:std::atomic(原子类型)。这个类型提供了一种方式来保证对某些数据类型的操作是原子的,即这些操作在执行过程中不会被其他线程中断。

在英语口语交流中,我们通常会这样描述std::atomic: “The std::atomic type in C++ provides a way to perform atomic operations on certain data types, which means these operations cannot be interrupted by other threads during their execution.” (C++中的std::atomic类型提供了一种在某些数据类型上执行原子操作的方式,这意味着这些操作在执行过程中不能被其他线程中断。)

这个句子的语法结构是:主语(The std::atomic type in C++)+ 动词(provides)+ 宾语(a way to perform atomic operations on certain data types)+ 定语从句(which means these operations cannot be interrupted by other threads during their execution)。在这个句子中,"which"引导的定语从句用来解释前面的主句内容。

2. std::atomic类的函数原型

std::atomic类提供了一系列的成员函数和非成员函数,用于执行各种原子操作。

2.1 成员函数

以下是std::atomic的一些主要成员函数:

成员函数 描述 英文描述
load 读取存储的值 Read the stored value
store 存储新的值 Store a new value
exchange 存储新的值,并返回旧的值 Store a new value and return the old value
compare_exchange_weak 比较并交换值(弱版本) Compare and exchange values (weak version)
compare_exchange_strong 比较并交换值(强版本) Compare and exchange values (strong version)
operator= 赋值操作符 Assignment operator
operator T 类型转换操作符 Type conversion operator
operator++, operator-- 自增和自减操作符 Increment and decrement operators
operator+=, operator-=, operator&=, `operator =operator^=` 复合赋值操作符

2.2 非成员函数

std::atomic也有一些非成员函数,如std::atomic_is_lock_freestd::atomic_thread_fencestd::atomic_signal_fence等。这些函数主要用于查询原子类型的属性或者控制内存访问的顺序。

在英语口语交流中,我们通常会这样描述std::atomic的成员函数和非成员函数:“The std::atomic class provides a set of member functions and non-member functions for performing various atomic operations. The member functions include load, store, exchange, compare_exchange_weak, compare_exchange_strong, etc. The non-member functions include std::atomic_is_lock_free, std::atomic_thread_fence, std::atomic_signal_fence, etc.” (std::atomic类提供了一套成员函数和非成员函数,用于执行各种原子操作。成员函数包括loadstoreexchangecompare_exchange_weakcompare_exchange_strong等。非成员函数包括std::atomic_is_lock_freestd::atomic_thread_fencestd::atomic_signal_fence等。)

2.3 std::atomic_bool

我觉得最常用的还是布尔型了,这边介绍一下注意事项

2.3.1 std::atomic_boolstd::atomic 的区别

在C++中,std::atomic_boolstd::atomic实际上是等价的。std::atomic_boolstd::atomic的类型别名,这意味着它们是完全相同的类型。在C++标准库中,为了方便使用,为一些常用的类型提供了类型别名,例如std::atomic_intstd::atomic的类型别名,std::atomic_longstd::atomic的类型别名,等等。

这两种方式在使用上没有区别,你可以根据自己的喜好选择使用哪一种。例如,你可以这样声明和使用一个原子布尔变量:

std::atomic_bool b1(false);
std::atomic<bool> b2(false);
b1.store(true);
b2.store(true);
bool v1 = b1.load();
bool v2 = b2.load();

在这个例子中,b1b2是完全相同的类型,它们的行为也是完全相同的。

2.3.2 初始化操作

std::atomic的初始化需要在构造函数的初始化列表中进行,而不能在类的成员变量声明处进行。这是因为std::atomic没有默认的拷贝构造函数,所以不能在类的成员变量声明处进行初始化。

以下是如何在类中使用std::atomic的示例:

#include <atomic>
class MyClass {
public:
    MyClass() : a(false) {}  // 在构造函数的初始化列表中初始化a
    void set(bool value) {
        a.store(value);  // 设置值
    }
    bool get() const {
        return a.load();  // 获取值
    }
private:
    std::atomic<bool> a;  // 声明一个std::atomic<bool>成员变量
};

在这个例子中,std::atomic成员变量aMyClass的构造函数的初始化列表中被初始化为false。然后,你可以使用setget成员函数来设置和获取a的值。这些操作都是线程安全的。

由于没有std::atomic没有拷贝构造函数,这意味着你不能通过复制一个已存在的std::atomic对象来创建一个新的std::atomic对象。

当你在类的成员变量声明处直接初始化一个std::atomic对象时,例如:

class MyClass {
private:
    std::atomic<bool> a = false;  // 这里会报错
};

这种语法实际上是在尝试使用拷贝构造函数来创建a。编译器会首先创建一个临时的std::atomic对象,然后尝试使用拷贝构造函数来创建a。但是,因为std::atomic没有拷贝构造函数,所以这会导致编译错误。

然而,当你在构造函数的初始化列表中初始化一个std::atomic对象时,例如:

class MyClass {
public:
    MyClass() : a(false) {}  // 这里不会报错
private:
    std::atomic<bool> a;
};

这种语法是在直接调用std::atomic的构造函数来创建a,而不是尝试使用拷贝构造函数。因此,这不会导致编译错误。

2.3.3 赋值操作

对于std::atomic对象,你可以使用=操作符来进行赋值操作,这是线程安全的。例如:

std::atomic<bool> a(false);  // 初始化为false
a = true;  // 设置值为true

在这个例子中,=操作符用于设置std::atomic对象的值,这个操作是线程安全的。

然而,std::atomic类也提供了storeload成员函数,这些函数提供了一种更明确的方式来进行赋值和读取操作。例如:

std::atomic<bool> a(false);  // 初始化为false
a.store(true);  // 设置值为true
bool b = a.load();  // 获取值

在这个例子中,store函数用于设置std::atomic对象的值,load函数用于获取它的值。这两个操作都是线程安全的。

总的来说,对于std::atomic对象,你可以选择使用=操作符或者storeload函数来进行赋值和读取操作。这两种方式都是线程安全的,但storeload函数提供了一种更明确的方式来表达你的意图。


另外注意,a.load() = true这样的写法是错误的,这是因为load()函数返回的是std::atomic对象的值的副本,而不是引用,所以你不能直接对它赋值。

如果你想改变std::atomic对象的值,你应该使用store函数或者=操作符,如下所示:

std::atomic<bool> a(false);  // 初始化为false
a.store(true);  // 使用store函数设置值为true
a = true;  // 使用=操作符设置值为true

在这两个例子中,std::atomic对象的值被设置为true。这两种方式都是线程安全的。

2.3.4 读取操作

a.load()操作是线程安全的。这意味着你可以在多线程环境中安全地读取std::atomic对象的值。例如:

std::atomic<bool> a(false);  // 初始化为false
bool b = a.load();  // 获取值

在这个例子中,load函数用于获取std::atomic对象的值,这个操作是线程安全的,即使在其他线程可能正在使用store函数改变a的值的情况下,load函数也能正确地获取a的值。

然而,你也可以直接使用=操作符来获取std::atomic对象的值,这也是线程安全的。例如:

std::atomic<bool> a(false);  // 初始化为false
bool b = a;  // 获取值

2.4 为什么std::atomic没有拷贝构造函数

在C++中,如果你没有为类提供拷贝构造函数,编译器会自动为你生成一个。然而,对于std::atomic类,拷贝构造函数被显式地删除了。

这是因为std::atomic类被设计为不能被拷贝。这是为了防止在多线程环境中出现意外的行为。如果你能够拷贝一个std::atomic对象,那么你可能会在不同的线程中操作同一个std::atomic对象的不同副本,这可能会导致数据竞争和其他并发问题。

因此,为了避免这种情况,std::atomic类的拷贝构造函数被删除,这意味着你不能拷贝std::atomic对象。这也是为什么你不能在类的成员变量声明处直接初始化std::atomic对象,因为这实际上是在尝试拷贝一个std::atomic对象。

2.5 示例

以下是一个使用std::atomic的复杂示例,展示了所有的成员函数和非成员函数的使用:

#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> atomicInt(0); // 使用原子整型,初始值为0
void increment() {
    for (int i = 0; i < 100000; ++i) {
        atomicInt++; // 使用原子操作进行自增
    }
}
void complexOperations() {
    int expected = 0;
    while (!atomicInt.compare_exchange_weak(expected, 100)) { 
        // compare_exchange_weak尝试将atomicInt的值设为100,如果当前值等于expected(0),则设为100并返回true,否则将当前值赋给expected并返回false
        expected = 0; // 重置expected
    }
    int oldValue = atomicInt.exchange(50); 
    // exchange将atomicInt的值设为50,并返回旧的值
    atomicInt.store(25); 
    // store将atomicInt的值设为25
    int loadedValue = atomicInt.load(); 
    // load读取并返回atomicInt的值
    std::cout << "Old value from exchange: " << oldValue << "\n";
    std::cout << "Loaded value: " << loadedValue << "\n";
}
int main() {
    std::thread t1(increment);
    std::thread t2(complexOperations);
    t1.join();
    t2.join();
    std::cout << "Final value: " << atomicInt << "\n"; // 输出最终的atomicInt值
    return 0;
}

在这个示例中,我们创建了一个原子整型atomicInt,并在一个线程中对其进行自增操作。在另一个线程中,我们使用了compare_exchange_weakexchangestore,和load等成员函数进行复杂的操作。这些操作都是原子的,即在执行过程中不会被其他线程中断。最后,我们输出了atomicInt的最终值。

这个示例展示了如何在多线程环境中使用std::atomic进行原子操作,以避免数据竞争问题。

3. std::atomic类的构造情况

std::atomic类提供了一系列的构造函数,用于创建和初始化原子对象。

3.1 默认构造函数

默认构造函数创建一个std::atomic对象,但不进行初始化。这意味着,除非显式地使用storeoperator=进行初始化,否则该对象的值是未定义的。

std::atomic<int> a; // a的值是未定义的

3.2 拷贝构造函数

std::atomic类的拷贝构造函数被删除,这意味着不能通过拷贝构造函数创建std::atomic对象。这是因为,原子操作的语义要求每个std::atomic对象都是唯一的。

std::atomic<int> a(0);
std::atomic<int> b(a); // 错误:拷贝构造函数被删除

3.3 移动构造函数

与拷贝构造函数一样,std::atomic类的移动构造函数也被删除。

std::atomic<int> a(0);
std::atomic<int> b(std::move(a)); // 错误:移动构造函数被删除

3.4 其他构造函数

std::atomic类还提供了一个构造函数,用于创建并初始化std::atomic对象。

std::atomic<int> a(0); // 创建并初始化a为0

在英语口语交流中,我们通常会这样描述std::atomic的构造函数:“The std::atomic class provides a default constructor for creating an atomic object without initialization. It also provides a constructor for creating and initializing an atomic object. However, the copy constructor and move constructor are deleted, which means each std::atomic object must be unique.” (std::atomic类提供了一个默认构造函数,用于创建但不初始化原子对象。它还提供了一个构造函数,用于创建并初始化原子对象。然而,拷贝构造函数和移动构造函数被删除,这意味着每个std::atomic对象必须是唯一的。)

4. std::atomic类的使用场景 (Usage Scenarios of std::atomic)

在C++中,std::atomic类被广泛用于多线程编程,特别是在需要进行原子操作的情况下。以下是std::atomic类的一些主要使用场景。

4.1 多线程同步 (Multithreading Synchronization)

在多线程环境中,我们经常需要确保对共享数据的访问是原子的,也就是说,在任何时刻,只有一个线程可以访问特定的数据。这就是std::atomic类的主要用途。例如,我们可以使用std::atomic来实现一个线程安全的计数器。

#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0); // 初始化一个原子整数
void increment() {
    for (int i = 0; i < 1000; ++i) {
        ++counter; // 原子操作
    }
}
int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.push_back(std::thread(increment)); // 创建10个线程,每个线程都会调用increment函数
    }
    for (auto& thread : threads) {
        thread.join(); // 等待所有线程完成
    }
    std::cout << "Counter = " << counter << std::endl; // 输出结果应该是10000
    return 0;
}

在这个例子中,我们创建了10个线程,每个线程都会调用increment函数,该函数会对counter变量进行1000次增加操作。由于counter是一个std::atomic类型的变量,所以这些操作是原子的,也就是说,在任何时刻,只有一个线程可以对counter进行增加操作。因此,当所有线程都完成后,counter的值应该是10000。

4.2 内存模型 (Memory Model)

std::atomic类还可以用于实现复杂的内存模型,例如“释放-获取”模型(“release-acquire” model)。在这种模型中,一个线程可以在一个std::atomic变量上执行一个“释放”操作(“release” operation),然后另一个线程可以在同一个std::atomic变量上执行一个“获取”操作(“acquire” operation)。这可以确保在“释放”操作之前的所有写操作都在“获取”操作之后的读操作之前完成,从而实现线程间的同步。

#include <atomic>
#include <thread>
std::atomic<bool> ready(false);
std::atomic<int> data(0);
void producer() {
    data.store(42, std::memory_order_release); // 释放操作
    ready.store(true, std::memory_order_release); // 释放操作
}
void consumer() {
    while (!ready.load(std::memory_order_acquire));
// 获取操作,等待ready变为true
    int result = data.load(std::memory_order_acquire); // 获取操作
    std::cout << "The answer is " << result << std::endl; // 输出结果应该是42
}
int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

在这个例子中,producer线程首先将42存储到data变量中,然后将ready变量设置为true。这两个操作都是“释放”操作。然后,consumer线程在一个循环中等待ready变量变为true。当ready变为true时,它会从data变量中读取数据。这两个操作都是“获取”操作。由于“释放-获取”模型的特性,我们可以确保当consumer线程读取data变量时,它读取的是producer线程存储的值,而不是任何旧的或未初始化的值。

4.3 其他使用场景 (Other Use Cases)

std::atomic类的应用并不仅限于上述的多线程同步和内存模型,它还可以用于实现更为复杂的并发算法和数据结构。以下是一些更高级的使用场景。

4.3.1 无锁数据结构 (Lock-Free Data Structures)

无锁数据结构是一种特殊的数据结构,它们在设计和实现上能够避免使用互斥锁(mutexes)或其他形式的锁。这些数据结构通常依赖于原子操作来保证线程安全,因此std::atomic类在这里发挥了关键作用。

例如,我们可以使用std::atomic来实现一个无锁的栈。在这个栈中,push和pop操作都是原子的,因此可以在多线程环境中安全地使用。

template <typename T>
class lock_free_stack {
private:
    struct node {
        T data;
        node* next;
        node(const T& data) : data(data), next(nullptr) {}
    };
    std::atomic<node*> head;
public:
    void push(const T& data) {
        node* new_node = new node(data);
        new_node->next = head.load();
        while (!head.compare_exchange_weak(new_node->next, new_node));
    }
    std::optional<T> pop() {
        node* old_head = head.load();
        while (old_head && !head.compare_exchange_weak(old_head, old_head->next));
        if (old_head) {
            std::optional<T> res = old_head->data;
            delete old_head;
            return res;
        } else {
            return std::nullopt;
        }
    }
};

在这个例子中,我们使用std::atomic来表示栈的头部。push操作创建一个新的节点,并使用compare_exchange_weak函数来尝试将其设置为新的头部。pop操作则尝试移除头部的节点。这两个操作都是原子的,因此这个栈是线程安全的。

4.3.2 原子指针 (Atomic Pointers)

std::atomic类也可以用于实现原子指针。原子指针是一种特殊的指针,它的所有操作都是原子的。这对于实现某些高级的并发算法非常有用。

例如,我们可以使用std::atomic来实现一个原子的指针交换操作:

std::atomic<void*> ptr1;
std::atomic<void*> ptr2;
void swap_ptrs() {
    void* tmp = ptr1.load();
    while (!ptr1.compare_exchange_weak(tmp, ptr2.load()));
    ptr2.store(tmp);
}

在这个例子中,swap_ptrs函数尝试交换ptr1和ptr2的值。这个操作是原子的,因此可以在多线程环境中安全地使用。

以上就是std::atomic类的一些高级使用场景。需要注意的是,这些高级话题通常需要深入理解并发编程和内存模型,因此在实际编程中,我们应该根据自己的经验和需求来选择合适的工具和技术。std::atomic类是C++提供的一个强大的工具,它可以帮助我们更有效地处理多线程编程中的各种问题。

5. 使用std::atomic类需要注意的点

在使用std::atomic类时,我们需要注意以下几个关键点。这些注意事项将帮助我们更好地理解和使用这个类。

5.1 数据类型限制

std::atomic类模板可以用于任何TriviallyCopyable类型(简单复制类型)。但是,对于非整数类型,只有部分操作是原子的。例如,对于std::atomic或std::atomic,只有load()和store()等操作是原子的,而对于std::atomic或std::atomic等整数类型,所有操作都是原子的。

在口语交流中,我们可以这样描述这个问题:“The std::atomic template can be used with any TriviallyCopyable types. However, for non-integer types, only some operations are atomic."(std::atomic模板可以用于任何简单复制类型。但是,对于非整数类型,只有部分操作是原子的。)

5.2 原子操作的性能考虑

虽然std::atomic提供了一种线程安全的方式来操作数据,但是原子操作通常比非原子操作要慢。这是因为原子操作需要确保在多线程环境中的一致性,这通常需要额外的处理器指令。因此,在设计并发代码时,我们需要权衡原子操作的线程安全性和性能开销。

在口语交流中,我们可以这样描述这个问题:“Although std::atomic provides a thread-safe way to manipulate data, atomic operations are usually slower than non-atomic operations. This is because atomic operations need to ensure consistency in a multithreaded environment, which usually requires additional processor instructions."(虽然std::atomic提供了一种线程安全的方式来操作数据,但是原子操作通常比非原子操作要慢。这是因为原子操作需要确保在多线程环境中的一致性,这通常需要额外的处理器指令。)

5.3 其他注意事项

在使用std::atomic时,我们还需要注意以下几点:

  • std::atomic不支持复制构造和复制赋值,这是因为这些操作无法保证原子性。
  • std::atomic的成员函数是线程安全的,但是如果你在多个线程中同时调用同一个std::atomic对象的成员函数,那么这些调用之间的顺序是未定义的。
  • std::atomic不支持复制构造和复制赋值,这是因为这些操作无法保证原子性。在口语交流中,我们可以这样描述这个问题:“std::atomic does not support copy construction and copy assignment, because these operations cannot guarantee atomicity."(std::atomic不支持复制构造和复制赋值,因为这些操作无法保证原子性。)
  • std::atomic的成员函数是线程安全的,但是如果你在多个线程中同时调用同一个std::atomic对象的成员函数,那么这些调用之间的顺序是未定义的。在口语交流中,我们可以这样描述这个问题:“The member functions of std::atomic are thread-safe, but if you call a member function of the same std::atomic object in multiple threads at the same time, the order of these calls is undefined."(std::atomic的成员函数是线程安全的,但是如果你在多个线程中同时调用同一个std::atomic对象的成员函数,那么这些调用之间的顺序是未定义的。)
  • 对于std::atomic的复合赋值操作(如+=,-=等),我们需要注意这些操作是原子的,但是对于相同的std::atomic对象,不同线程中的复合赋值操作的顺序是未定义的。在口语交流中,我们可以这样描述这个问题:“For the compound assignment operations of std::atomic (such as +=, -=, etc.), these operations are atomic, but the order of compound assignment operations on the same std::atomic object in different threads is undefined."(对于std::atomic的复合赋值操作(如+=,-=等),这些操作是原子的,但是对于相同的std::atomic对象,不同线程中的复合赋值操作的顺序是未定义的。)

以上就是使用std::atomic类需要注意的一些关键点。在编写并发代码时,我们需要充分理解和考虑这些问题,以确保代码的正确性和性能。

6. 为什么要使用std::atomic类

在C++中,std::atomic类(原子类)是一个模板类,用于保证对某种类型的对象进行的操作是原子的,即这些操作在多线程环境下是线程安全的。在多线程编程中,原子操作是非常重要的概念,它可以避免数据竞争(Data Race)和其他并发问题。

6.1 解决数据竞争问题

数据竞争(Data Race)是并发编程中的一个常见问题,当两个或更多的线程在没有同步的情况下访问某些数据,并且至少有一个线程对数据进行写操作时,就会发生数据竞争。数据竞争会导致程序的行为变得不可预测和难以复现,这在实际的软件开发中是非常危险的。

在C++中,我们可以使用std::atomic类来避免数据竞争。std::atomic类提供了一种机制,可以确保对某种类型的对象进行的操作是原子的,即这些操作在多线程环境下是线程安全的。这意味着,当一个线程正在对一个std::atomic对象进行操作时,其他线程不能同时对该对象进行操作。这样,我们就可以避免数据竞争。

例如,我们可以使用std::atomic类来实现一个线程安全的计数器:

#include <atomic>
#include <thread>
std::atomic<int> counter(0);  // 初始化一个原子整数,初始值为0
void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;  // 对原子整数进行自增操作
    }
}
int main() {
    std::thread t1(increment);  // 创建并启动两个线程,分别执行increment函数
    std::thread t2(increment);
    t1.join();  // 等待两个线程结束
    t2.join();
    std::cout << counter << std::endl;  // 输出结果应该为200000
    return 0;
}

在这个例子中,我们使用std::atomic来声明一个原子整数counter,然后在两个线程中对这个原子整数进行自增操作。由于std::atomic保证了自增操作是原子的,所以我们可以确保最后的结果是正确的,即使在多线程环境下。

6.2 提高并发性能

除了避免数据竞争,std::atomic类还可以用来提高并发性能。在多线程编程中,我们通常使用锁(例如std::mutex)来保证数据的一致性

和线程安全。然而,锁的使用会带来一定的性能开销,因为它会阻塞线程的执行,直到锁被释放。相比之下,std::atomic类提供的原子操作通常更高效,因为它们不需要阻塞线程的执行。

例如,考虑以下使用锁的代码:

#include <mutex>
#include <thread>
std::mutex mtx;
int counter = 0;
void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << counter << std::endl;
    return 0;
}

在这个例子中,我们使用std::mutex来保护counter,以确保在多线程环境下对counter的操作是线程安全的。然而,每次对counter进行操作时,我们都需要获取和释放锁,这会带来一定的性能开销。

相比之下,如果我们使用std::atomic类,就可以避免这种性能开销:

#include <atomic>
#include <thread>
std::atomic<int> counter(0);
void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << counter << std::endl;
    return 0;
}

在这个例子中,我们使用std::atomic来声明一个原子整数counter,然后在两个线程中对这个原子整数进行自增操作。由于std::atomic保证了自增操作是原子的,所以我们不需要使用锁,从而可以避免锁带来的性能开销。

总的来说,std::atomic类提供了一种高效的方式来实现线程安全的操作,这对于提高并发性能是非常有用的。

6.3 其他理由

除了上述的原因,使用std::atomic类还有其他的理由。例如,std::atomic类提供了一种简单的方式来实现复杂的并发算法,例如无锁数据结构和原子操作。此外,std::atomic类还提供了一种方式来实现低级别的同步和通信,这对于某些特定的应用场景是非常有用的。

总的来说,std::atomic类是C++中实现并发编程的一个重要工具,它提供了一种高效、安全的方式来实现原子操作。无论是在解决数据竞争问题,还是在提高并发性能,甚至在实现复杂的并发算法和

低级别的同步和通信,std::atomic类都能发挥重要的作用。

例如,我们可以使用std::atomic_flag来实现一个简单的自旋锁(Spinlock):

#include <atomic>
#include <thread>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_acquire))  // acquire lock
             ; // spin
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // release lock
    }
}
int main()
{
    std::thread t1(f, 1);
    std::thread t2(f, 2);
    t1.join();
    t2.join();
    return 0;
}

在这个例子中,我们使用std::atomic_flag来实现一个自旋锁。当一个线程需要获取锁时,它会不断地尝试设置std::atomic_flag,直到成功为止。当一个线程需要释放锁时,它会清除std::atomic_flag。这是一个非常简单的例子,但是它展示了std::atomic类在实现复杂的并发算法中的作用。

总的来说,std::atomic类是C++中实现并发编程的一个重要工具,它提供了一种高效、安全的方式来实现原子操作。无论是在解决数据竞争问题,还是在提高并发性能,甚至在实现复杂的并发算法和低级别的同步和通信,std::atomic类都能发挥重要的作用。

7. std::atomic类在Qt中的表现形式

在Qt中,我们可以使用std::atomic来实现线程安全的操作。这是因为std::atomic提供了一种机制,可以确保对数据的操作在多线程环境中是原子的,即不可分割的。这样,我们就可以避免在多线程环境中出现的数据竞争问题。

7.1 Qt中的std::atomic类使用示例

以下是一个在Qt中使用std::atomic的示例。在这个示例中,我们创建了一个std::atomic对象,并在多个线程中对其进行操作。

#include <QThread>
#include <atomic>
#include <iostream>
std::atomic<int> count(0); // 创建一个std::atomic<int>对象
class Worker : public QThread
{
public:
    void run() override {
        for(int i = 0; i < 100000; ++i) {
            ++count; // 在多个线程中对std::atomic<int>对象进行操作
        }
    }
};
int main() {
    Worker workers[10];
    for(Worker& worker : workers) {
        worker.start(); // 启动线程
    }
    for(Worker& worker : workers) {
        worker.wait(); // 等待线程结束
    }
    std::cout << "Final count: " << count << std::endl; // 输出最终的计数值
    return 0;
}

在这个示例中,我们创建了10个工作线程,每个线程都会对同一个std::atomic对象进行100000次增加操作。由于std::atomic保证了这些操作是原子的,所以最终的计数值应该是1000000。

7.2 Qt中的std::atomic类与QAtomic类的比较

在Qt中,除了std::atomic类,还有一个QAtomic类,它也提供了一种实现线程安全操作的机制。下表是std::atomic类和QAtomic类的一些主要方法的比较:

方法 std::atomic QAtomic
加法操作 std::atomic<T>::fetch_add QAtomicInt::fetchAndAddRelaxed
比较并交换操作 std::atomic<T>::compare_exchange_strong QAtomicInt::testAndSetRelaxed
获取当前值 std::atomic<T>::load QAtomicInt::load
设置当前值 std::atomic<T>::store QAtomicInt::store

虽然std::atomic和QAtomic在功能上有很多相似之处,但是在使用上还是有一些区别的。例如,std::atomic支持更多的数据类型,而QAtomic只支持int和pointer类型。此外,std::atomic的接口更加标准化,更符合C++的编程习

在Qt中,std::atomic可以用于实现线程安全的操作。这是因为std::atomic提供了一种机制,可以确保对数据的操作在多线程环境中是原子的,即不可分割的。这样,我们就可以避免在多线程环境中出现的数据竞争问题。

例如,如果我们有一个全局变量,我们需要在多个线程中对其进行操作,那么我们可以使用std::atomic来确保这些操作是线程安全的。以下是一个示例:

#include <QThread>
#include <atomic>
#include <iostream>
std::atomic<int> count(0); // 创建一个std::atomic<int>对象
class Worker : public QThread
{
public:
    void run() override {
        for(int i = 0; i < 100000; ++i) {
            ++count; // 在多个线程中对std::atomic<int>对象进行操作
        }
    }
};
int main() {
    Worker workers[10];
    for(Worker& worker : workers) {
        worker.start(); // 启动线程
    }
    for(Worker& worker : workers) {
        worker.wait(); // 等待线程结束
    }
    std::cout << "Final count: " << count << std::endl; // 输出最终的计数值
    return 0;
}

在这个示例中,我们创建了10个工作线程,每个线程都会对同一个std::atomic对象进行100000次增加操作。由于std::atomic保证了这些操作是原子的,所以最终的计数值应该是1000000。

此外,Qt还提供了QAtomic类,它也可以用于实现线程安全的操作。但是,std::atomic和QAtomic在使用上有一些区别。例如,std::atomic支持更多的数据类型,而QAtomic只支持int和pointer类型。此外,std::atomic的接口更加标准化,更符合C++的编程习惯。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。

目录
相关文章
|
26天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
42 2
|
28天前
|
设计模式 安全 数据库连接
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
35 2
|
6天前
|
存储 对象存储 C++
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
本文深入对比了 C++ 标准库中的 `std::array` 和 `std::vector`,从内存管理、性能、功能特性、使用场景等方面详细分析了两者的差异。`std::array` 适合固定大小的数据和高性能需求,而 `std::vector` 则提供了动态调整大小的灵活性,适用于数据量不确定或需要频繁操作的场景。选择合适的容器可以提高代码的效率和可靠性。
27 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
84 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
81 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
89 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)