【C++11多线程入门教程】系列之atomic

原子atomic功能介绍

  C++里面提供专门的原子操作库为细粒度的原子操作提供组件,允许无锁并发编程。涉及同一对象的每个原子操作,相对于任何其他原子操作是不可分的。原子对象不具有数据竞争。我们都知道多线程并发程序最需要解决的就是数据竞争所带来的问题,因此我们可以通过前面所学的互斥量加锁与解锁机制来管理数据竞争的问题。另外,我们还可以通过原子操作来进行解决。C++11多线程提供原子操作atomic类模板来方便我们进行使用。

  既然我们能够通过互斥量来进行避免数据竞争的错误,为什么我们还需要进行使用原子操作呢?目前我的浅显理解是这样的,一、原子操作是细粒度最小的操作,无法继续拆分,不可能存在数据竞争的问题。二、原子操作更偏底层一些,效率相对于互斥量加锁与解锁更高效。那为什么我们不能放弃互斥量完全转换至原子操作呢?主要原因在于C++11原子操作提供功能有限,同时灵活性并不如互斥量方便。下面我们通过一个小程序来对比一下互斥量与原子操作的时间消耗:

#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>
#include <mutex>

//int N = 0; // 使用互斥锁
std::atomic<int> N = 0; // 使用原子
std::mutex mtx;

void SumIter()
{
	for (int i = 0; i < 1000000; ++i)
	{
		//mtx.lock(); // 加锁
		//if (N.is_lock_free()) // 判断原子是否上锁
		++N;      // 调用原子operator++ 运算符
		//N += 1; // 调用原子operator+= 运算符
		//N++; // 调用原子operator++ 运算符
		//N.fetch_add(1); // 原子对象N每次加1
		//N.fetch_sub(1); // 原子对象N每次减1
		//N = N + 1;      // 错误,非原装操作
		//mtx.unlock(); // 解锁
	}
}

int main(void)
{
	auto start = std::chrono::steady_clock::now();

	std::thread th1(SumIter);
	std::thread th2(SumIter);

	th1.join();
	th2.join();

	auto end = std::chrono::steady_clock::now();
	auto elapseTime = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

	std::cout << "elapse time is: " << elapseTime.count() << " millseconds." << std::endl;

	std::cout << "the number after calc is: " << N << std::endl;

	system("pause");
	return 0;
}

首先在结果一致情况下,使用原子操作与互斥量操作的结果耗时见下表:

原子操作耗时互斥量耗时
84ms362ms

可见,原子操作要比互斥量机制速度快4倍左右。下面我们根据上面的小程序来进行分析:

  上述代码里面使用atomic原子类模板的成员函数运算符或者是特化成员函数。其中,表达式`N=N+1`并非原子操作,导致结果出现错误。关于自增操作,可以使用operator()++或者使用fetch_add()特化函数,其它关于自减操作,异或等都在上面的特化函数里面。

  我们注意到上述的代码中存在fetch_add()这些特化函数,有两个参数。第二个参数其实是对std::memory_order的一些属性设置,默认为memory_order_seq_cst。关于第二个参数有哪些请看下面列表:

// ENUM memory_order
enum memory_order {
	memory_order_relaxed, // 松散内存顺序,不会执行顺序做任何保障
	memory_order_consume, // 本线程中所有后续有关的原子类型操作,必须在本条原子操作完成后执行
	memory_order_acquire, // 本线程中,所有后续的读操作均在本条原子操作完成后执行
	memory_order_release, // 本线程中所有值钱的写操作完成后才能执行本条原子操作
	memory_order_acq_rel, // 同时包含memory_order_acquire和memory_order_release标记
	memory_order_seq_cst  // 全部存取都按顺序执行
};

下面我们来看下原子std::atomic类模板的相关成员函数:

小结

  作为原子操作入门课程,我们先简单了解std::atomic相关成员函数及其特化函数。如何使用std::atomic原子类进行相应的功能使用,当然最多的是对某个单变量计数操作等等。当然,还需要避免一些表达式操作导致并非原子操作的模式,例如N=N+1等。

关于原子操作与互斥量的对比小结:

  • std::atomic原子类模板使用主要用来作为计数等比较便利,原子操作一般是针对一个变量,互斥量操作一般是一段代码;
  • 原子操作比互斥量效率更高;
  • 原子操作是无锁并发,不会遇到数据竞争的问题,细粒度最小;
  • 原子操作的灵活性比互斥量在多线程管理中较低;
参考

atomic

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言中的`<stdatomic.h>`库提供了原子操作接口,用于实现线程安全的原子操作原子操作是不可被中断的,即在执行原子操作期间不会有其他线程干扰。 在C11标准中,`<stdatomic.h>`库引入了一组原子类型和原子操作函数。这些原子类型和函数可以用于实现共享变量的原子访问、更新和同步。 下面是一些常用的原子类型和相关函数: 1. `atomic_flag`类型:用于简单的原子标志操作,只有两个操作:`atomic_flag_test_and_set()`和`atomic_flag_clear()`。 2. 原子整型类型(如`atomic_int`、`atomic_uint`等):支持常见的整型操作,如赋值、加法、减法、比较交换等。 3. 原子指针类型(如`atomic_intptr_t`、`atomic_ptrdiff_t`等):支持指针类型的原子操作,如原子加载、存储和比较交换等。 4. `atomic_thread_fence()`函数:用于实现内存屏障,确保指令重排序不会破坏多线程程序的正确性。 5. `atomic_load()`和`atomic_store()`函数:用于原子加载和存储操作。 6. `atomic_exchange()`函数:用于原子交换操作,可以原子地交换一个值并返回旧值。 7. `atomic_compare_exchange_strong()`和`atomic_compare_exchange_weak()`函数:用于原子比较并交换操作,可以原子地比较并交换一个值。 通过使用这些原子类型和函数,我们可以实现线程安全的并发操作。注意,原子操作并不意味着完全的线程同步,额外的同步机制(如互斥锁)可能仍然是必需的来确保正确的并发访问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值