第一章—介绍原子性,条件同步以及自旋和阻塞的区别

来源:互联网 发布:华为大数据案例分析 编辑:程序博客网 时间:2024/05/01 08:07

     和所有其他并发书籍一样这本书开头也是列举了自增运算符在多并发场景下的可能出现的错误。


原子性:


     对于自增运算,现代的任何一个计算机都会把这个操作实现成三步:从内存加载数据到寄存器,增加寄存器中的数据,然后再把增加之后的数据写入到内存。


      如果我们的计数器初始值是0的话,在正常情况下,当两个线程执行完成之后,我们会得到2。但是如果一个现在在另一个线程执行第三步之前率先执行了第一步的话,两个线程都会往内存存入1,这样另一个线程的增加就会被丢失掉(不会得到2)。

      

       同步是一门用来防止程序进行错误交叉执行路径的艺术。在一个分布式系统中,同步被归类为通讯交流:如果T2线程从T1线程那里收到了一个消息,那么在所有可能的交叉执行路径中,T1中所有在发送消息之前发生的事件都会在T2收到消息之前发生。

       

         

       有一部分语言和系统保证在同一时刻只有一个线程在执行,并且线程上下文的切换会发生在实现定义好的点上。

            

             事实证明,在现实世界编程的模式中,基本上所有的同步模式都可以被认为是atomicity or condition synchronization中的一个实例。原子性保证了,一个指令执行序列在执行过程中是不会出现其他执行序列的。所有的交叉执行都是假设底层的机器指令是原子执行的。条件同步保证特定的操作只有在必要的前提条件是true的情况下才会出现。通常,这个前提条件都是其他线程某些操作的完成。

              

            实现原子性最简单的方式就是强制这些线程每次只有一个线程来执行他们的操作。这个策略被称作互斥性。那些被互斥性执行的操作集合称作临界区。

            

           如果在获取个字对方的资源之前,线程1获得lock2,线程2获得lock3。这俩会无限制的等待。一个最简单的解决方法是,线程总是优先获取数字最小的锁。这样的结果是,线程2在没有获得lock2的情况下是不会获取lock3的。但是在大多数情况下,维持这样一个静态的序列是复杂的。另一个可选的原子性实现方案是事务内存。从程序员的视角来看,细粒度的锁定是用更小的临界区来为大型的,复杂的操作实现原子性的一种手段。保证实现的正确性完全是程序员的责任。事务内存提高了抽象程度,这样让程序员可以把这个责任委托给底层的系统去实现。

        

          不管原子性是通过粗粒度的锁,程序员管理的细粒度的锁,或者某种个是的事务内存,所有的意图都是原子的区域都是不可分割的。换种说法就是程序的任何可行的执行路径——他的机器指令的所有可能交叉执行的情况——必须和那些按照时间执行的原子操作是不可区分的,这些原子操作都不会有其他指令在他里面执行。在第三章,有很多方法可以规范这些要求,更显著的可线性化和可串行化的几个变种。 



条件同步:

       

              在同步性研究中,一个并发队列有些时候会被称为划定边界的buffer,他是既包括同步又包括原子性的一个权威的例子。像我们上面介绍的await condition,bounded buffer中的条件可以放在临界区的开头。在更复杂的情况下,一个线程在临界区内不知道他将要等待的条件之前,执行了一个重要的工作。为了让条件成立,另一个线程可能会修改同一个数据结构,一个处于临界区中间的wait操作可能会打破整个临界区的原子性。在第七章我们会看到只支持简单的在临界区等待的同步机制,其他的同步机制允许条件出现在临界区的任意地方。

           在临界区之外,条件同步也是很有用的,比如同步栏栅(synchronization barrier),他保证了只有所有的线程都执行了最后一步,其他的线程才可以离开。

          假设原子性的实现比条件同步要简单是很吸引人的。毕竟,原子性可以被看作是条件同步的一个子类,一个线程一直等到临界区没有其他线程的时候才会执行。这么想带来的问题就是条件的范围。为了方便,我们把条件暂且考虑成变量的值而不是线程的状态。看到这里,原子性要求所有的线程要达成一致。

自旋和阻塞:

         

          像同步模式会分为两个阵营,他们的实现也会分为两个阵营:他们都采用自旋或者阻塞。自旋是一个简单的例子。比如条件同步,他采用一个一般的循环。


       实现互斥的最简单的方法就是使用一个硬件指令TAS,这个硬件指令在大多数现代计算机上都会有,设置一个布尔值为true,并且返回之前的值。用TAS可以实现简单的自旋锁。


        忙等待的明显缺点就是它浪费了cpu的执行周期。在一个多应用程序系统,经常会使用阻塞——让处理器去执行其他的可执行的线程。之前的线程也许会不久之后又会执行。


     负责选择哪个线程去执行的软件叫做调度器。在很多系统中,调度出现在两个不同的层次上。在操作系统层面上,一个内核调度器在处理器核上实现threads;在用户运行时层面上,一个用户调度器在内核调度器上实现用户级别的threads。不管在哪个层面上,实现threads的代码一般都是以库文件接口的行尸出现,包括一整套调用子程序。另外,内核或者应用程序所使用的语言可能会提供特定的线程管理和同步,这取决于编译器的实现。


       在不同层次上的调度器有特定的功能。内核级别的调度器,让不同应用程序之间不可以互相访问对方的内存地址来保护应用程序。用户级别的调度器,可能会用栈式的实现方式。更大程度上说,内核或者用户级别的调度器,有着相似的内部结构,并且在不同层次上,轮训和阻塞都会很有用。


        阻塞不用不停的去查看条件和锁的状态,但是他会在来回切换程序的时候有性能花费。如果线程等待的平均时间小上下文切换时间的两倍,则可以优先考虑轮训。当每个cpu核上之后一个线程在执行的时候,轮训也是不错的选择,这通常是发生生在嵌入式或者高性能的系统中。最终我们会在第七章中发现,阻塞(基于调度的同步)一定是基于轮训实现的,因为调度器使用的数据结构本身也需要同步。


      不管是基于自旋还是阻塞,实现正确的同步需要satety和liveness。简单的说,satety意味着坏的事情永远不会发生:在同一个锁的同一个临界区内不会有两个线程;永远不会发现系统中的所有线程都阻塞了。


      blocking的多种意思:在这个章节里,我们把他定义为一个重新调度的同义词,就是把cpu占有权或者内核线程让给其他的内核线程或者用户。在一个应用系统上下文中,它表示一个操作等待其他系统组建的回应。更理论的说,一个在某个条件上不断轮训的线程,必须被其他线程置为true的情况和放弃内核线程或者cpu权利的情况一样,被阻塞了,只有其他线程告诉调度器让这些阻塞的线程再次执行。在不同的上下文中,我们必须能明确的人变出他的意思。


         无活锁是最简单的活性属性。他规定不会呆在原地不停的执行。在关于锁的上下稳重,这个意味着锁L是空闲的,并且线程T调用了L.acquire()方法,那么在其他线程得到这个锁之前T肯定有有限的指令去执行。无饥饿限制更严格一点。在锁的上下问中,他表示,如果所有的线程都曾经得到过锁,并且释放了这个锁,如果T已经调用了L.acquire()方法,那么在他得到这个锁之前他肯定有有限的指令去执行。

0 0
原创粉丝点击