.NET中保证线程安全的高级方法Interlocked类使用介绍

说到线程安全,不要一下子就想到加锁,尤其是可能会调用频繁或者是要求高性能的场合。

对于性能要求不高或者同步的对象数量不多的时候,加锁是一个比较简单而且易于实现的选择。比方说.NET提供的一些基础类库,比如线程安全的堆栈和队列,如果使用加锁的方式那么会使性能大打折扣(速度可能会降低好几个数量级),而且如果设计得不好的话还有可能发生死锁。

现在通过查看微软的源代码来学习一些不直接lock(等价于Monitor类)的线程同步技巧吧。

这里我们主要用的是Interlocked类,这个类按照M$的描述,是“为多个线程共享的变量提供原子操作”,当然这个类是一个静态类。这个类的源代码看不到,因为是调用的CLR内部的方法,不过基本思想应该是通过硬件原语try and set来实现的。

该类提供的Add、Increment、Decrement能够完成简单的原子操作。

假如我们要提供一个计数器,每访问一次就递增地返回一个新的数值用于计数。在多线程环境下,s++这一条语句不是线程安全的。因为执行这个语句要经过:移到寄存器(读取)、运算、写入这几个步骤,在任何时候都可能会切换到其他线程,这样子s被多个线程访问值可能会在切换的过程中丢失。有了Interlocked提供的这几个原子操作的方法,就不用自己去加锁实现这些简单的运算了。由于是使用的硬件原语,其效率自然也比加锁高得多。

但是大多数情况下,问题并没有执行相加相减运算那么简单,这时如果不想用锁的话就要想想办法了。

以微软的ConcurrentStack提供的线程安全的堆栈为例,分析一下如何实现如果往栈头添加数据。

m_head是指向堆顶的指针,在定义的时候由于是多线程访问的,所以要加上volatile修饰符:

代码如下:

private volatile Node m_head;

如果是单线程的,那么入栈语句就是下面这个样子:

代码如下:

1.    Node newNode = new Node(item);
2.    newNode.m_next = m_head;
3.    m_head = newNode;

假如有两个线程并发访问入栈方法的话,那么可能会产生如下情况:第一个线程执行完第二条语句被打断,第二个线程执行到第二条语句又切换回第一个线程,两个线程执行完后有一个入栈的元素就不见了。那么如何实现线程安全呢?M$的代码是这样写的:

代码如下:

Node newNode = new Node(item);
newNode.m_next = m_head;
if (Interlocked.CompareExchange(ref m_head, newNode, newNode.m_next) == newNode.m_next)
{
    return;
}

// If we failed, go to the slow path and loop around until we succeed.
PushCore(newNode, newNode);

首先,Interlocked.CompareExchange比较两个元素是否相等,并根据比较的结果替换其中一个元素,返回结果始终是第一个元素的原值。这个方法是原子操作。

那么这段代码首先设置newNode的下一节点为堆栈顶部的元素,接下来CompareExchange,判断栈顶元素有没有被修改过。假如此时没有另一个线程修改栈顶元素,那么m_head还是原来的值(上一条语句设置的新栈顶的下一个元素),此时就可以安全地把栈顶指针指向新元素,操作完成(return)。注意CompareExchange是原子操作的,所以在这期间栈顶元素不可能再被修改。

如果比较结果不相等,那么说明栈顶元素已经被其他线程修改了(此时返回值就是被修改后的栈顶,和上一条语句设置m_next不一样),这样CompareExchange就不会修改m_head,说明入栈不成功,执行PushCore方法。

这个东东的代码如下:

代码如下:

private void PushCore(Node head, Node tail)
        {
            SpinWait spin = new SpinWait();

// Keep trying to CAS the exising head with the new node until we succeed.
            do
            {
                spin.SpinOnce();
                // Reread the head and link our new node.
                tail.m_next = m_head;
            }
            while (Interlocked.CompareExchange(
                ref m_head, head, tail.m_next) != tail.m_next);

#if !FEATURE_PAL && !FEATURE_CORECLR
            if (CDSCollectionETWBCLProvider.Log.IsEnabled())
            {
                CDSCollectionETWBCLProvider.Log.ConcurrentStack_FastPushFailed(spin.Count);
            }
#endif //!FEATURE_PAL && !FEATURE_CORECLR
        }

可以看到其逻辑还是和上面那个一样,只是加了一个循环直到操作完成。在这期间使用了一个SpinWait对象和SpinOnce方法,那么我们又要了解一下这是干嘛的。

关于SpinWait对象,M$的说明是:System.Threading.SpinWait 是一个轻量同步类型,可以在低级别方案中使用它来避免内核事件所需的高开销的上下文切换和内核转换。

关于它的说明还有一堆,你可以参考这里。如果不想看那么多,那么只需了解它的使用场合是在资源不会被占用很长时间的时候进行等待,以用户模式自旋以避免高额的开销。

SpinOnce的说明很简单,就是执行单一自旋,可以理解为等待一个很短的时间。总的来说,当自旋此时达到5次时,会切换到同一处理器上的另一个线程,当达到20次时,会调用Thread的Sleep方法阻塞当前线程,此时可以切换到其他同优先级或更高优先级的线程上去。

这样,就可以避免加锁(lock free)的高昂代价来实现线程的同步。

但是有的时候我们不能保证线程安全。比如堆栈的Count属性,在程序调用这个属性后,我们并不能保证这个属性返回的时候是正确的,在返回到应用程序的线程前元素数量是有可能变化的,因此我们也就只能保证我们的返回值曾经正确。

不过显而易见,这是可以接受的。对于开发者来说,假如我们要访问一个多线程字典(ConcurrentDictionary)中的指定元素,我们不应该是先判断是否为空再取元素(因为元素可能在这两步操作之间被删掉),而是应该使用TryGetValue这种保证线程安全的方法来进行操作。

(0)

相关推荐

  • 深入多线程之:深入分析Interlocked

    在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤: 一:将实例变量中的值加载到寄存器中. 二:增加或减少该值. 三:在实例变量中存储该值. 在多线程环境下,线程会在执行完前两个步骤后被抢先.然后由另一个线程执行所有三个步骤,当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失. Interlocked可以为多个线程共享的变量提供原子操作. Interlocked.Increment:以原子操作的形式递增指定变量的值并存储结果.    Interl

  • C#中使用Interlocked进行原子操作的技巧

    什么是原子操作? 原子(atom)本意是"不能被进一步分割的最小粒子",而原子操作(atomic operation)意为"不可被中断的一个或一系列操作" .在C#中有多个线程同时对某个变量进行操作的时候,我们应该使用原子操作,防止多线程取到的值不是最新的值. 例如:int result = 0; 多线程A正在执行 result(0)+1 多线程B同时执行 result(0)+1 那么最终result的结果是1还是2呢,这个就很难说了.如果在CPU中2个线程同时计算

  • .NET中保证线程安全的高级方法Interlocked类使用介绍

    说到线程安全,不要一下子就想到加锁,尤其是可能会调用频繁或者是要求高性能的场合. 对于性能要求不高或者同步的对象数量不多的时候,加锁是一个比较简单而且易于实现的选择.比方说.NET提供的一些基础类库,比如线程安全的堆栈和队列,如果使用加锁的方式那么会使性能大打折扣(速度可能会降低好几个数量级),而且如果设计得不好的话还有可能发生死锁. 现在通过查看微软的源代码来学习一些不直接lock(等价于Monitor类)的线程同步技巧吧. 这里我们主要用的是Interlocked类,这个类按照M$的描述,是

  • Java中保证线程顺序执行的操作代码

    只要了解过多线程,我们就知道线程开始的顺序跟执行的顺序是不一样的.如果只是创建三个线程然后执行,最后的执行顺序是不可预期的.这是因为在创建完线程之后,线程执行的开始时间取决于CPU何时分配时间片,线程可以看成是相对于的主线程的一个异步操作. public class FIFOThreadExample { public synchronized static void foo(String name) { System.out.print(name); } public static void

  • Java中实现线程的超时中断方法实例

    背景 之前在实现熔断降级组件时,需要实现一个接口的超时中断,意思是,业务在使用熔断降级功能时,在平台上设置了一个超时时间,如果在请求进入熔断器开始计时,并且接口在超时时间内没有响应,则需要提早中断该请求并返回. 比如正常下游接口的超时时间为800ms,但是因为自身业务的特殊需求,最多只能等200ms,如果200ms之内没有数据返回,则返回降级数据.这里处理请求的线程可以看成是tomcat线程池中的一个线程,如果通过线程池返回的Future,可以很轻松的实现超时返回,但是这种情况下,并不能拿到Fu

  • Java中终止线程的三种方法

    Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的! 1.线程正常执行完毕,正常结束 也就是让run方法执行完毕,该线程就会正常结束. 但有时候线程是永远无法结束的,比如while(true). 2.监视某些条件,结束线程的不间断运行 需要while()循环在某以特定条件下退出,最直接的办法就是设一个boolean标志,并通过设置这个标志来控制循环

  • 深入分析C++中执行多个exe文件方法的批处理代码介绍

    不同目录下的多个二进制执行文件的批处理代码 @echo offpushd "G:\apache-activemq-5.5.0-bin\apache-activemq-5.5.0\bin\"start /min ""   G:\apache-activemq-5.5.0-bin\apache-activemq-5.5.0\bin\activemq.batpopd ping 127.0.0.1  -n 5 pushd "G:\backup2011-10-31\

  • Java中启动线程start和run的两种方法

    一.区别 Java中启动线程有两种方法,继承Thread类和实现Runnable接口,由于Java无法实现多重继承,所以一般通过实现Runnable接口来创建线程.但是无论哪种方法都可以通过start()和run()方法来启动线程,下面就来介绍一下他们的区别. start方法: 通过该方法启动线程的同时也创建了一个线程,真正实现了多线程.无需等待run()方法中的代码执行完毕,就可以接着执行下面的代码.此时start()的这个线程处于就绪状态,当得到CPU的时间片后就会执行其中的run()方法.

  • Java中锁的分类与使用方法

    Lock和synchronized 锁是一种工具,用于控制对共享资源的访问 Lock和synchronized,这两个是最创建的锁,他们都可以达到线程安全的目的,但是使用和功能上有较大不同 Lock不是完全替代synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,提供高级功能 Lock 最常见的是ReentrantLock实现 为啥需要Lock syn效率低:锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程 不够灵活,加锁和释放的时

  • SpringMVC中RequestContextHolder获取请求信息的方法

    RequestContextHolder的作用是: 在Service层获取获取request和response信息 代码示例: ServletRequestAttributes attrs = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attrs.getRequest(); 源码分析: 定义了两个ThreadLocal变量用来存储Reque

  • Qt 中开启线程的多种方式小结

    目录 简介 1. 继承 QThread 重写 run 函数 2. 继承 QObject 调用 moveToThread 3. 继承 QRunnable 重新 run 函数,结合 QThreadPool 实现线程池 4. 使用 C++ 11 中的 sth::thread 5. Qt QtConcurrent 之 Run 函数 简介 在开发过程中,使用线程是经常会遇到的场景,本篇文章就来整理一下 Qt 中使用线程的五种方式,方便后期回顾.前面两种比较简单,一笔带过了,主要介绍后面三种.最后两种方法博

  • C++详解多线程中的线程同步与互斥量

    目录 线程同步 互斥量 线程同步 /* 使用多线程实现买票的案例. 有3个窗口,一共是100张票. */ #include <stdio.h> #include <pthread.h> #include <unistd.h> // 全局变量,所有的线程都共享这一份资源. int tickets = 100; void * sellticket(void * arg) { // 卖票 while(tickets > 0) { usleep(6000); //微秒 p

随机推荐