zoukankan      html  css  js  c++  java
  • Lock,LockFree,MemoryBarrier,ConcurrentCollection

    最近看并行编程书本的一些心得,简单记录下多线程和并行编程必知必会的几个概念,再次加深自己的理解。

    .NET Framework4提供了一个新的命名空间System.Collections.Concurrent用于解决常用集合在并发情况下的线程安全问题(ps:通过这个命名空间还可以访问用于并行化循环和PLINQ的自定义分区器Partitioner)。这个命名空间下的所有线程安全集合都在某种程度上使用了无锁技术。也就是说,这些集合通过使用比较并交换(Compare And Swap,CAS)指令和内存屏障(Memory Barrier),避免了使用典型的互斥的重量级的锁,虽然实际开发中对性能要求不高的业务系统中加锁可以获得最经济实惠的开发效益。

    一、锁

    在多线程和并发这两个主题下从来都离不开锁的身影,锁是用来做并发最简单但是代价也可能是最高的方式,在某些场景下加锁是非常经济实惠的解决方案。

    锁很好用,代码也极好维护,但是必须清楚不能滥用,否则可能导致严重性能问题。因为加锁会增加系统内核态与用户态之间的切换开销以及线程调度开销。我们知道,加锁、释放锁会导致上下文切换和调度延时,等待锁的线程会被挂起直至锁释放。内核态的锁需要操作系统进行一次上下文切换,在上下文切换的时候,cpu之前缓存的指令和数据都将失效,对性能影响最大。

    1、使用锁的三大基本原则

    a、不使用锁

    b、使用小粒度的锁,常见的锁如互斥锁(lock)、读写锁(ReaderWriterLockSlim,ReaderWriterLock)等等

    c、锁住尽可能短的时间

    2、同步对象

    为了封装锁的逻辑,通常需要一个同步对象。比如常见的简单粗暴的同步代码里,lock(sth)的sth就是一个同步对象。

    同步对象必须是引用类型(字符串通常不适合做同步对象,想想为什么),而且它通常是私有的,通常是一个instance或者static field。

    为了精确的控制锁的scope和粒度,我们通常会创建一个dedicated字段,比如locker,asyncObj等;

    避免使用lock(this) 或者lock(typeof(sometype))或者lock(string),这种使用方法将无法封装锁的逻辑,难以避免死锁和过度的阻塞,甚至在一个进程内还会溢出app domain边界。

    3、如何减少锁?

    如果你的设计为了复用而有很多共享数据,那么在多线程高并发环境下使用Lock还是LockFree的同步算法都是不可避免的。

    根据经验,当我们需要访问共享的可写字段时,通常就可以通过锁来同步。

    为了减少锁,我们需要减少共享数据的使用。

    二、CAS

    1、基本原理

    CAS,简单说来就是比较并交换,大致逻辑就是如果A与B相等,那么将C赋值给A。

    CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。

    如果内存位置的值V与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B;否则,处理器不做任何操作。

    2、内部实现伪代码

    bool CAS(T* ptr, T expected, T fresh)
    {
        if(*ptr != expected)
             return false;
        *ptr = fresh;
        return true;
    }

    3、优点

    CAS是CPU指令级的操作,看上去只有一步原子操作,避免了请求操作系统来裁定锁的问题,所以一般很快。CAS操作是基于共享数据不会被修改的假设,当同步冲突出现的机会很少时,这种假设就能带来较大的性能提升。主要优点如下:

    a、避免通常加锁所导致的严重性能开销,减少了内核态与用户态之间的切换开销以及线程调度开销;

    b、实现更细力度的并行控制,提高系统吞吐量,有些情况下可以达到成倍的关键业务的性能提升。

    4、缺点

    CAS虽然有明显的优点,但天下没有免费的午餐 ,通过CAS实现的LockFree也存在很多问题,比如:
    a、与硬件体系结构的内存读写模型相关,所以存在移植问题
    b、实现复杂,其正确性很难被证明
        (a)、受限于CPU指令
        (b)、即使简单的数据结构也要通过复杂的算法来实现
        (c)、ABA问题
    c、代码难以维护
    d、存在活锁(livelock)问题

        所谓活锁,简单来讲就是指事物1可以使用资源,但它让其他事物先使用资源;事物2可以使用资源,但它也让其他事物先使用资源,于是两者一直谦让,结果两者都无法使用资源。

    三、MemoryBarrier

    为什么需要MemoryBarrier(内存屏蔽),MSDN的解释是:

    MemoryBarrier is required only on multiprocessor systems with weak memory ordering (for example, a system employing multiple Intel Itanium processors).

    Synchronizes memory access as follows: The processor executing the current thread cannot reorder instructions in such a way that memory accesses prior to the call to MemoryBarrier execute after memory accesses that follow the call to MemoryBarrier. 

    简单来说就是多核处理器会对运行CPU指令顺序重排优化,而编译后的程序可能因为编译器优化或者计算机硬件结构比如分布式系统等诸多原因,不以编码时的顺序执行,从而引发预期外的问题。

    Memory Barrier就是一种在底层保证语句按顺序执行的解决方案,调用Thread.MemoryBarrier()之后的代码中内存访问不能在这之前就完成了,也就是它可以限制指令重排和内存读写的缓存。

    参考:

    http://stackoverflow.com/questions/3556351/why-we-need-thread-memorybarrier

    Barrier类,允许多个任务同步它们不同阶段上的并发工作。

    四、并行集合

    System.Collections.Concurrent命名空间下主要的线程安全并行集合有如下几种:

    1、ConcurrenctQueue<T>

    ConcurrenctQueue是System.Collections.Queue的并发版本。它是一个FIFO(Fisrt In,First Out,先进先出)的集合。

    ConcurrenctQueue是完全无锁的,但是当CAS操作失败且面临资源争用的时候,它可能会自旋并且进行重试操作。

    2、ConcurrenctStack<T>

    ConcurrenctStack是System.Collections.Stack的并发版本。它是一个LIFO(Lastt In,First Out,后进先出)的集合。

    ConcurrenctStack是完全无锁的,但是当CAS操作失败且面临资源争用的时候,它可能会自旋并且进行重试操作。

    3、ConcurrenctBag<T>

    ConcurrenctBag提供了一个无序的对象集合,而且支持对象重复,当不用考虑顺序时非常有用。

    ConcurrenctBag使用了很多不同的机制,最大程度地减少了同步的需求以及同步所带来的开销。

    ConcurrenctBag为每一个访问集合的线程维护了一个本地队列,而且在可能的情况下,它会以无锁的方式访问这个本地队列。

    ConcurrenctBag在同一个线程添加元素(生产)和删除元素(消费)的场合下效率非常高。然而,ConcurrenctBag有时候会用到锁,因此,在生产者和消费者线程完全分开的场景下效率非常低下。

    4、ConcurrenctDictionary<TKey,TValue>

    ConcurrenctDictionary与经典的键值对的字典类似,提供了并发的键值访问。它是System.Collections.IDictionary实现的并发版本。

    ConcurrenctDictionary对于读操作是完全无锁的,它对于需要频繁使用读取的操作进行了优化。

    当很多任务或者线程在字典中添加或者修改数据的时候,ConcurrenctDictionary会使用细粒度的锁。

    5、 BlockingCollection<T>

    BlockingCollection与经典的阻塞队列数据结构类似,它是对一个IProducerConsumerCollection<T>实例的包装器,提供了阻塞(block)和限界(bound)的能力。

    BlockingCollection能够适用于有多个任务添加和删除数据的生产者-消费者的情形。

    这里再顺带提一下IProducerConsumerCollection<T>这个接口,它继承自IEnumerable<T>、ICollection和IEnumerable。忍不住要为IProducerConsumerCollection<T>、IEnumerable<T>、ICollection和IEnumerable这几个接口的抽象拍手叫好。可以说,MS对集合的设计是非常富有远见并适应变化的。

    PS:关于并行集合和线程安全,很久之前我也写过总结,可以参考之前的拙文浅析线程安全容器的实现

    参考:

    <<C#并行编程高级教程>>

    http://msdn.microsoft.com/zh-cn/library/system.collections.concurrent(v=vs.110).aspx

    http://msdn.microsoft.com/zh-cn/library/dd267312(v=vs.110).aspx

    http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

  • 相关阅读:
    Binary search tree
    搜索二叉树
    windows最基本命令行
    sublime package
    二叉树的层次遍历和其深度
    二叉树后序遍历
    PopupWindow的使用
    android之ViewPager的使用
    android部分开发摘要
    android4.0以后要求网络请求必须发生在子线程中
  • 原文地址:https://www.cnblogs.com/jeffwongishandsome/p/talk-about-multithread-and-parallel-programming-some-knowledge.html
Copyright © 2011-2022 走看看