有关volatile的说明

以下代码是以linux下的,linux下的线程的实现是一个轻量级的进程。

在计算机操作系统中,轻量级进程(LWP)是一种实现多任务的方法。与普通进程相比,LWP与其他进程共享所有(或大部分)它的逻辑地址空间和系统资源;与线程相比,LWP有它自己的进程标识符,优先级,状态,以及栈和局部存储区,并和其他进程有着父子关系;这是和类Unix操作系统的系统调用vfork()生成的进程一样的。另外,线程既可由应用程序管理,又可由内核管理,而LWP只能由内核管理并像普通进程一样被调度。Linux内核是支持LWP的典型例子。

  在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息,而这也是它之所以被称为轻量级的原因。一般来说,一个进程代表程序的一个实例,而LWP代表程序的执行线程(其实,在内核不支持线程的时候,LWP可以很方便地提供线程的实现)。因为一个执行线程不像进程那样需要那么多状态信息,所以LWP也不带有这样的信息。

  LWP的一个重要作用是提供了一个用户级线程实现的中间系统。LWP可以通过系统调用获得内核提供的服务,因此,当一个用户级线程运行时,只需要将它连接到一个LWP上便可以具有内核支持线程的所有属性。

  而因为LWP之间共享它们的大部分资源,所以它在某些应用程序就不适用了;这个时候就要使用多个普通的进程了。例如,为了避免内存泄漏(a process can be replaced by another one)和实现特权分隔(processes can run under other credentials and have other permissions)。

  使用多个进程也使得应用程序在出现进程池内的进程崩溃或被攻击的情况下变得更加健壮。

所以我理解,在windows下应该是可以用锁达到同步的,而且下面的代码,既然用了原子操作,那么锁其实是多余的了。

 

昨天跟同事一起找一个服务器崩溃的bug,牵出一个比较严重的错误认识问题,现在我记录如下,以供大家参考。
我说的尽量简化点,看下面类:

class Message
{
    public:
        Message()
        {
            m_bIsEncryptedBeforeSend = false;
        }
        void checkEncryption()
        {
            if ( m_bIsEncryptedBeforeSend )
            {
                return;    
            }           
            
//dosomething();            
            m_bIsEncryptedBeforeSend = true;
        }
        
//other member function
    private:
        volatile bool m_bIsEncryptedBeforeSend;
 //! count the checkEncryption is called
        
//other member data field
};

       这个类是一个网络消息体,在发生消息之前,网络线程会调用 Message::checkEncryption() 来检查是否已经加密,而且这段程序的意图是希望 dosomething() 部分只执行一次。这里几乎没有做任何线程安全方面的工作,于是发生昨晚的杯具。昨晚我们同一个Message会同时发给的客户端,因为是放到网络线程中处理的,所以会出现多个线程同时 Message::checkEncryption() 的情况,那么在某些时候会出现多个线程同时进入到 Message::checkEncryption() 函数体中,从而出现我们不希望看到的问题:dosomething() 这段代码被多个线程多次调用。
       我们找到这个问题后,很想当然的认为给这个 Message::checkEncryption() 函数入口处加一把锁就能解决问题,于是有了下面的代码:

class Message
{
    public:
        Message()
        {
            m_bIsEncryptedBeforeSend = false;
        }
        void checkEncryption()
        {
            m_lkEncrypt.lock();
            if ( m_bIsEncryptedBeforeSend )
            {
                m_lkEncrypt.unlock();
                return;    
            } 
            
//dosomething();
            m_bIsEncryptedBeforeSend = true;
            m_lkEncrypt.unlock();
        }
        
//other member function
    private:
        volatile bool m_bIsEncryptedBeforeSend;
 //! count the checkEncryption is called
        Lock m_lkEncrypt;
 //! lock for checkEncryption
        
//other member data field
};

       加如锁之后,原来的问题看似解决了,不然,很快同样的问题继续出现,只是频率要低多了,但是还是偶尔会发生。也就是说,即使加了锁,我们还是发现 Message::checkEncryption() 函数体中的 dosomething(); 这一段代码被多个线程调用过。这是为什么呢? 
        因为 volatile 修饰的变量仅仅是保证编译器不会优化这个变量,同时也不会放到cpu寄存器中,但似乎并没有说明必须从物理内存中取数据而不从cpu缓存取数据。昨天加锁后问题依旧出现。情况看起来只有这种可能了:
       线程A、B同时来到 Message::checkEncryption() 函数体中,其中线程A获得了锁,进入函数体,线程B等待。当线程A退出函数体释放锁,线程B再进入,由于刚刚线程B将锁相关的数据读入cpu缓存中时候顺带也将 m_bIsEncryptedBeforeSend 读入cpu缓存中,这个时候线程B访问m_bIsEncryptedBeforeSend发现是false(old value,并没有因为线程A修改了而得到刷新),于是继续执行,从而出现我们不愿看到的结果,dosomething();被执行两次。(注释:这里线程A、B可能分别在两个不同的cpu核上执行,而每个cpu核可能有自己独立的cpu缓存)。
       下面关于 volatile 关键字,我们顺带多说下。
       我找了很大一圈关于 volatile 关键字的资料,最终发现网上有两种截然不同的看法:
       看法一:用 volatile 变量可以解决多线程中的共享数据问题
       看法二:用 volatile 变量不会带来任何好处,根本不能解决多线程共享数据冲突问题,反而会因为不优化而带来性能损降。
     昨天出现问题,我发现在多核cpu情况下,看法二是比较符合实际情况的。下面举一些网络上的比较权威的资料:
1. www.open-std.org Should volatile Acquire Atomicity and Thread Visibility Semantics?
       网址:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html )
       这里说了一句话:According to David Butenhof, "the use of volatile accomplishes nothing but to prevent the compiler from making useful and desirable optimizations, providing no help whatsoever in making code 'thread safe'" (comp.programming.threads posting, July 3, 1997, according to the Google archive). 翻译过来就是:根据David Butenhof所说,使用 volatile 除了阻止编译做一些有益的事情和优化,并不会达到任何目的,对于写多线程代码的线程安全问题不会提供任何帮助。
2. www.kernel.org Why the "volatile" type class should not be used
       网址:http://kernel.org/doc/Documentation/volatile-considered-harmful.txt 这里提供了一篇文章说明了 "volatile" 变量不应该被使用的原因。               
        解决上述线程冲突问题,除了加锁外,我们还需要对 m_bIsEncryptedBeforeSend 变量的修改进行原子操作(其知识点在下文)才能保证一个线程(该线程在cpu A核上)改了,另一个线程(该线程可能不在cpu A核而在其他核上)能知道这种更改。具体解决办法请见下面代码:

class Message
{
    public:
        Message()
        {
            m_bIsEncryptedBeforeSend = 0;
        }
        void checkEncryption()
        {
            m_lkEncrypt.lock();
            if( AtomicInc32( &m_nIsEncryptedBeforeSend ) > 1 )
            {
                m_lkEncrypt.unlock();
                return;    
            }        
            
//dosomething();           
            m_lkEncrypt.unlock();
        }
        
//other member function

    private:
        AtomicInt32 m_nIsEncryptedBeforeSend;
 //! count the checkEncryption is called

        Lock m_lkEncrypt;
 //! lock for checkEncryption

        
//other member data field

};

    OK.本文的所涉及的code基本上伪代码,不过应该都能很容易的看懂。

 

    所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。 原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。 原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。原子类型定义如下: 
typedef struct 

    volatile int counter; 

}atomic_t; 
volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。  

原子操作API包括:  
1、atomic_read(atomic_t v); 该函数对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。  
2、atomic_set(atomic_t v, int i); 该函数设置原子类型的变量v的值为i。  
3、void atomic_add(int i, atomic_t *v); 该函数给原子类型的变量v增加值i。  
4、atomic_sub(int i, atomic_t *v); 该函数从原子类型的变量v中减去i。  
5、int atomic_sub_and_test(int i, atomic_t *v);    该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。  
6、void atomic_inc(atomic_t *v); 该函数对原子类型变量v原子地增加1。  
7、void atomic_dec(atomic_t *v); 该函数对原子类型的变量v原子地减1。  
    8、int atomic_dec_and_test(atomic_t *v); 该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。  
    9、int atomic_inc_and_test(atomic_t *v); 该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。  
    10、int atomic_add_negative(int i, atomic_t *v); 该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。  
    11、int atomic_add_return(int i, atomic_t *v); 该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。  
    12、int atomic_sub_return(int i, atomic_t *v); 该函数从原子类型的变量v中减去i,并且返回指向v的指针。  
13、int atomic_inc_return(atomic_t v); 该函数对原子类型的变量v原子地增加1并且返回指向v的指针。  
14、int atomic_dec_return(atomic_t v); 该函数对原子类型的变量v原子地减1并且返回指向v的指针。  
    原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1。当不需要引用该IP碎片时,就使用函数 ipq_put来释放该IP碎片,ipq_put使用函atomic_dec_and_test把引用计数减1并判断引用计数是否为0,如果是就释放 IP碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过使用函数atomic_dec实现)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值