张三思评博客:volatile的用法和原子序操作

标题: 【muduo】base篇—Atomic
作者: lx青萍之末

原文链接: https://blog.csdn.net/daaikuaichuan/article/details/86179766

扩充:张三思

文章目录

  • * 一、原子性操作的概念
    * 二、Atomic源码分析
    * 三、volatile关键字详解
    * * 1、volatile关键字的概念
      * 2、volatile与多线程有关系吗?
      * 3、计算机中内存、cache和寄存器之间的关系及区别
      * 4、volatile关键字的三大特性
      * * (1)易变性
        * (2)不可优化
        * (3)顺序性
    

一、原子性操作的概念

所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。
原子操作不需要加锁

C/C++中数值操作,如自加 (n++) 自减 (n–) 及赋值 (n=1)
操作都不是原子操作。如果多线程程序需要使用全局计数器,程序就需要使用锁或者互斥量保证操作的安全性,对于较高并发的程序,这种做法会造成一定的性能瓶颈。
【C++11可以使用atomic关键字】

二、Atomic源码分析

【gcc提供的原子操作】:

// 原子自增操作,返回的是更新前的值
type __sync_fetch_and_add (type *ptr, type value)
    
// 原子比较和交换(设置)操作
​// 如果*ptr == oldval,就将newval写入*ptr,第一个函数返回操作之前的值,第二个函数在相等并写入的情况下返回true
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval)
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval)

// 原子赋值操作,将*ptr设为value并返回*ptr操作之前的值
type __sync_lock_test_and_set (type *ptr, type value)

muduo原子性操作的代码中,就是利用gcc提供的原子操作来实现赋值、自加等原子性操作。

#ifndef MUDUO_BASE_ATOMIC_H
#define MUDUO_BASE_ATOMIC_H

#include <muduo/base/noncopyable.h>

#include <stdint.h>

namespace muduo
{

namespace detail
{
template<typename T>
// noncopyable表示禁止默认拷贝构造函数和赋值运算符
class AtomicIntegerT : noncopyable
{
 public:
  AtomicIntegerT()
    : value_(0)
  {
  }

  T get()
  {
    // in gcc >= 4.7: __atomic_load_n(&value_, __ATOMIC_SEQ_CST)
    return __sync_val_compare_and_swap(&value_, 0, 0);
  }

  T getAndAdd(T x)
  {
    // in gcc >= 4.7: __atomic_fetch_add(&value_, x, __ATOMIC_SEQ_CST)
    return __sync_fetch_and_add(&value_, x);
  }

  T addAndGet(T x)
  {
    return getAndAdd(x) + x;
  }

  T incrementAndGet()
  {
    return addAndGet(1);
  }

  T decrementAndGet()
  {
    return addAndGet(-1);
  }

  void add(T x)
  {
    getAndAdd(x);
  }

  void increment()
  {
    incrementAndGet();
  }

  void decrement()
  {
    decrementAndGet();
  }

  T getAndSet(T newValue)
  {
    // in gcc >= 4.7: __atomic_exchange_n(&value, newValue, __ATOMIC_SEQ_CST)
    return __sync_lock_test_and_set(&value_, newValue);
  }

 private:
  volatile T value_;
};
}  // namespace detail

typedef detail::AtomicIntegerT<int32_t> AtomicInt32; // 原子操作的数据类型
typedef detail::AtomicIntegerT<int64_t> AtomicInt64;

}  // namespace muduo

#endif  // MUDUO_BASE_ATOMIC_H

张三思评:

这里写的比较模糊,有待提升。

三、volatile关键字详解

1、volatile关键字的概念

volatile的作用: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。简单地说就是 防止编译器对代码进行优化。当要求使用volatile 声明的变量的值的时候,系统总是重新 从它所在的内存读取数据 ,而 不是使用保存在寄存器中的备份。即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

张三思评:

所谓的volatile就是指,如果用volatile修饰一个变量,那么这个变量被修改后,应该立刻写会到内存,如果要利用这个变量给其他变量赋值,那么也应该再去内存里读取,而非使用寄存器里的缓存值。

volatile

A situation that is volatile is likely to change suddenly and unexpectedly. 变化无常的

If someone is volatile, their mood often changes quickly. 情绪不稳定的

A volatile liquid or substance is one that will quickly change into a gas. 易挥发的 [技术]

2、volatile与多线程有关系吗?

volatile 跟多线程无关,它不是一种同步手段,用它来实现线程安全是错的。 原子和锁 会保证线程安全性。

张三思评:

如果和多线程无关,那么volatile的意义又何在呢? 所以,这里的说法是有问题的,准确的说, volatile 不能保障线程安全而已。 那么这个鸡肋的玩意是干嘛的呢?

有待调查。 请认真阅读[参考1],那篇文章说的很好!

最后提出一个问题:volatile既然不能保障线程安全,那么它到底有什么用呢?在Java和C++中,其作用是不一样的,这个问题我暂时很难回答。只能背一背答案,因为探究其原理,成本比较高,在[参考1]里面,几个图片已经缺失,很难搞清楚。

3、volatile关键字的三大特性

张三思评:
这块写的很好,非常有必要看一下!!!

(1)易变性

所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。
在这里插入图片描述
在这里插入图片描述

a = fn©执行后,寄存器ecx中的a,被写回内存:mov dword ptr [esp+0Ch], ecx。然后,在执行b = a +
1;语句时,变量a有重新被从内存中读取出来:mov eax, dword ptr [esp + 0Ch],而不再直接使用寄存器ecx中的内容。

(2)不可优化

volatile告诉编译器, 不要对我这个变量进行各种激进的优化 ,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。
在这里插入图片描述
非volatile变量a,b,c全部被编译器优化掉了 (optimize out),因为编译器通过分析,发觉a,b,c三个变量是无用的,可以进行常量替换。
在这里插入图片描述
a,b,c三个变量,都是volatile变量。这个区别,反映到汇编语言中,就是三个变量仍旧存在,需要将三个变量从内存读入到寄存器之中,然后再调用printf()函数。

(3)顺序性

保证volatile变量间的顺序性,编译器不会进行乱序优化。

一个简单的示例,全局变量A,B均为非volatile变量。通过gcc
O2优化进行编译,你可以惊奇的发现,A,B两个变量的赋值顺序被调换了!!!在对应的汇编代码中,B = 0语句先被执行,然后才是A = B +1语句被执行。如此看来,C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。

同时将A,B两个变量都声明为volatile变量,再来看看对应的汇编。奇迹发生了,A,B赋值乱序的现象消失。此时的汇编代码,与用户代码顺序高度一直,先赋值变量A,然后赋值变量B。如此看来,C/C++
Volatile变量间的操作,是不会被编译器交换顺序的。

张三思评:

所以,volatile到底怎么用?有待调查。

这篇文章并没有回答我们的疑问。

张三思评:

后面本人又查阅了一些博客,和室友请教, 得到了以下的知识

volatile仅仅适合一个线程写,其他线程读的场景,是一个通知里的用法。

为啥一个参数或指针可以既是const又是volatile的?
以防止编译器将其优化成从寄存器中读取。一个定义为volatile的变量时说这个变量可能会被意想不到地改变,这样编译器就不会去假设这个变量的值。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。一个参数可以既是const又是volatile,const是不让程序修改,volatile是意想不到的改变,不是程序修改。一个指针也可以是volatile,中断服务子程序修改一个指向buffer的指针。

volatile关键字可以保证 可见性 和 有序性 ,并 不能保证原子性 。

使用场景:
由于volatile并不能保证线程安全,并且我们书写的代码中绝大部分都不是原子操作,所以volatile的使用场景其实非常有限,想要正确的使用volatile关键字,需要遵循以下两点:

  • 对volatile变量的写操作不依赖当前值
  • volatile变量不包含在含有其它变量的不变式中

说得通俗一点就是:volatile只能作为一个独立变量使用,通常就是用于各种flag。[参考3]提到比如这篇文章 :如何优雅的中断线程。倒数第二个例子便是使用了volatile关键字。
volatile变量的++操作正是违反了第一点,所以不算是正确的用法。

4、计算机中内存、cache和寄存器之间的关系及区别

  • 寄存器是中央处理器(CPU)内的组成部份 。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,包含的寄存器有累加器(ACC)。

  • 内存 包含的范围非常广,一般分为只读存储器(ROM)、随机存储器(RAM)和高速缓存存储器(cache)。

  • Cache :即高速缓冲存储器,是 位于CPU与主内存间的一种容量较小但速度很高的存储器 。由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,这样就减少了CPU的等待时间,提高了系统的效率。Cache又分为一级Cache(L1 Cache)和二级Cache(L2 Cache),L1 Cache集成在CPU内部,L2 Cache早期一般是焊在主板上,现在也都集成在CPU内部,常见的容量有256KB或512KB L2 Cache。

大致来说数据是通过 内存-Cache-寄存器 ,Cache缓存则是为了弥补CPU与内存之间运算速度的差异而设置的的部件。

在这里插入图片描述

参考:

[1] c++ volatile 深度剖析
https://www.cnblogs.com/god-of-death/p/7852394.html

[2] 计算机中内存、cache和寄存器之间的关系及区别
https://blog.csdn.net/hellojoy/article/details/54744231

[3]解析java中的volatile关键字
https://blog.csdn.net/Baisitao_/article/details/100942368

[4] Java并发编程的艺术

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值