一、概述
std::atomic 是C++11引入的一个模板类,用于提供原子操作的类型。在多线程编程中,当多个线程同时访问同一块数据时,可能会导致数据竞争和不确定的行为。std::atomic 可以用来创建原子类型的变量,保证对该变量的操作是原子的,不会被中断,从而避免了数据竞争。
std::atomic 适用于以下场景:
- 在多线程环境下对共享数据进行原子操作;
- 需要保证特定操作的原子性,如递增、递减、交换等操作;
- 需要避免使用锁的情况下进行线程同步。
举个具体的例子:
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
void incrementCounter()
{
for (int i = 0; i < 100000; ++i)
{
counter++;
}
}
int main(int argc, char *argv[])
{
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
qDebug() << counter;
return 0;
}
这段代码存在一个线程安全的问题。多个线程同时访问和修改同一个全局变量 counter
,而没有进行同步操作会导致竞态条件。
竞态条件指的是多个线程并发执行时,由于执行顺序不确定导致程序出现意料之外的结果的情况。
就上面的代码来说,在 incrementCounter()
函数中,多个线程同时对 counter
进行递增操作,而递增操作不是一个原子操作,它包括读取 counter
的当前值、对该值加一、然后写回到 counter
。由于线程间的执行顺序是不确定的,就可能出现以下情况:
- 线程 A 读取
counter
的值为 0,然后执行加一操作得到 1; - 此时线程 B 也读取
counter
的值为 0,执行加一操作得到 1; - 然后线程 A 和线程 B 都把值 1 写回到
counter
,导致实际的递增次数少于预期。
因此,竞态条件可能会导致 counter
的最终结果少于预期的值。
为了解决这个问题,可以使用互斥锁来保护对 counter
的访问,确保同时只有一个线程能够访问和修改 counter
,从而避免了竞态条件的问题:
int counter = 0;
std::mutex mutex;
void incrementCounter()
{
for (int i = 0; i < 100000; ++i)
{
std::lock_guard<std::mutex> lock(mutex);
counter++;
}
}
int main(int argc, char *argv[])
{
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
qDebug() << counter;
return 0;
}
使用std::atomic是另一种做法:
#include <atomic>
std::atomic<int> counter(0);
void incrementCounter()
{
for (int i = 0; i < 100000; ++i)
{
counter++;
}
}
int main(int argc, char *argv[])
{
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
qDebug() << counter;
return 0;
}
使用std::atomic<int> 类型的计数器 counter,并在两个线程中并发地对其进行递增操作。由于 counter 是 std::atomic 类型,它会保证递增操作的原子性,避免了多线程竞争导致的问题。最终输出的 counter 值会是预期的 200000。
二、成员函数
1、compare_exchange_weak()
、compare_exchange_strong()
比较和交换函数。这两个函数的作用都是:当原子对象的值等于期望值时,用新值替换原子对象的值。
compare_exchange_weak
函数
- 比较和交换不成功时,允许原子变量的值被其他线程更改,并返回
false
,表示失败。 - 比较和交换成功时,将原子变量的值设置为新值,并返回
true
,表示成功。
std::atomic<int> atomic_var(10);
void increment(std::atomic<int>& var)
{
int expected = 10;
int new_value = 20;
bool success = var.compare_exchange_weak(expected, new_value);
if (success) {
std::cout << "线程 " << std::this_thread::get_id()<< std::endl
<< " 比较和交换成功,原子变量的值已修改为: " << var << std::endl;
} else {
std::cout << "线程 " << std::this_thread::get_id()<< std::endl
<< " 比较和交换失败,原子变量的值未修改" << std::endl;
}
}
int main(int argc, char *argv[])
{
std::thread t1(increment, std::ref(atomic_var));
std::thread t2(increment, std::ref(atomic_var));
t1.join();
t2.join();
return 0;
}
如果某个线程在比较和交换时发现 atomic_var
的值已经被其他线程更改,那么它会允许原子变量的值被其他线程更改,并返回 false
,表示失败。因此,上面代码输出结果可能会是以下两种情况之一:
结果1
线程 1 比较和交换成功,原子变量的值已修改为: 20
线程 2 比较和交换失败,原子变量的值未修改
结果2
线程 2 比较和交换失败,原子变量的值未修改
线程 1 比较和交换成功,原子变量的值已修改为: 20
compare_exchange_strong
函数
- 比较和交换不成功时,拒绝其他线程对原子变量的更改,并返回
false
,表示失败。 - 比较和交换成功时,将原子变量的值设置为新值,并返回
true
,表示成功。
std::atomic<int> atomic_var(10);
void increment(std::atomic<int>& var)
{
int expected = 10;
int new_value = 20;
bool success = var.compare_exchange_strong(expected, new_value);
if (success) {
std::cout << "线程 " << std::this_thread::get_id()<< std::endl
<< " 比较和交换成功,原子变量的值已修改为: " << var << std::endl;
} else {
std::cout << "线程 " << std::this_thread::get_id()<< std::endl
<< " 比较和交换失败,原子变量的值未修改" << std::endl;
}
}
int main(int argc, char *argv[])
{
std::thread t1(increment, std::ref(atomic_var));
std::thread t2(increment, std::ref(atomic_var));
t1.join();
t2.join();
return 0;
}
如果某个线程在比较和交换时发现 atomic_var 的值已经被其他线程更改,那么它会拒绝其他线程对原子变量的更改,并返回 false,表示失败。因此,无论哪个线程先执行,只会有一个线程能够成功修改原子变量的值。上述代码输出结果可能会是以下两种情况之一:
结果1
线程 1 比较和交换成功,原子变量的值已修改为: 20
线程 2 比较和交换失败,原子变量的值未修改
结果2
线程 2 比较和交换成功,原子变量的值已修改为: 20
线程 1 比较和交换失败,原子变量的值未修改
2、exchange()
原子地交换原子变量的值,并返回原来的值。
std::atomic<int> atomic_var(10);
// 将 atomic_var 的值交换为 42
int old_value = atomic_var.exchange(42);
std::cout << "旧值: " << old_value << std::endl;
std::cout << "新值: " << atomic_var << std::endl;
3、fetch_add()、fetch_sub()
原子地将原子变量的值增加 / 减少指定的增量,并返回增加前的旧值。
std::atomic<int> atomic_var(10);
// 将 atomic_var 的值原子地增加 5
int old_value = atomic_var.fetch_add(5);
std::cout << "增加前的值: " << old_value << std::endl;
std::cout << "增加后的值: " << atomic_var << std::endl;
4、fetch_and()、fetch_or()、fetch_xor()
原子地将原子变量的值与指定的值进行按位 与 / 或 / 异或 操作,并返回按位与前的旧值。
5、load()、operator T()
获取对象中存储的值,同时确保在多线程环境下进行安全的原子操作(也就是可以安全地在多线程环境下进行,而无需额外的同步控制)。
std::atomic<int> atomicInt(42);
int value = atomicInt.load();//value = 42;
6、store()
用于将给定的值存储到
std::atomic
对象中,并确保在多线程环境下进行安全的原子操作。该函数没有返回值,它仅负责将值存储到std::atomic
对象中。它确保在存储新值之前,不会发生其他线程访问该std::atomic
对象的竞争条件。
三、一个自定义类型的示例
std::atomic
不仅支持数值类型,还支持其他可赋值类型。
#include <iostream>
#include <thread>
#include <atomic>
#include <string>
struct Person {
std::string name;
int age;
};
std::atomic<Person> atomicPerson;
void updatePerson() {
Person p{"Alice", 25};
atomicPerson.store(p);
}
void printPerson() {
Person p = atomicPerson.load();
std::cout << "Name: " << p.name << ", Age: " << p.age << std::endl;
}
int main() {
std::thread t1(updatePerson);
std::thread t2(printPerson);
t1.join();
t2.join();
return 0;
}
四、std::atomic_ref
此模板类可以对各种类型的非原子变量进行原子操作,比如整型、指针、自定义结构等。它允许在需要时对非原子变量进行原子操作。
在多线程环境中使用 std::atomic_ref
可以避免对非原子变量进行竞争条件的操作,从而提高线程安全性。
此类也有如上述的成员函数。
例1:
int value = 42;
std::atomic_ref<int> atomicValue(value);
atomicValue.store(10, std::memory_order_relaxed);
std::cout << "Value is: " << value << std::endl; // 输出 10
这里首先创建了一个普通的整型变量 value
,然后使用 std::atomic_ref
类型的 atomicValue
对其进行原子操作。通过 store
函数,将新的值 10 存储到 atomicValue
中,这也直接影响到了原始的变量 value
。
例2:
#include <iostream>
#include <atomic>
#include <string>
struct UserData
{
std::string name;
int age;
};
int main(int argc, char *argv[])
{
UserData user {"Alice", 30};
std::atomic_ref<int> atomicAge(user.age);
// 在多线程环境中,使用原子操作更新用户的年龄
auto updateAge = [&atomicAge](int newAge)
{
atomicAge.store(newAge, std::memory_order_relaxed);
};
std::thread t1(updateAge, 35);
std::thread t2(updateAge, 40);
t1.join();
t2.join();
std::cout << "Updated user1's age: " << user.age << std::endl; // 输出更新后的年龄
return 0;
}
这里使用 std::atomic_ref
类型的 atomicAge
对 user
结构体中的 age
成员变量进行原子操作。在 updateAge
函数中,将新的 age 值存储到 atomicAge
中,并利用 std::thread
创建了两个线程来更新 user
的 age 。