zoukankan      html  css  js  c++  java
  • 也谈多线程同步

    并发模式主要是为了处理以下两种类型的问题:

    1) 共享资源:每次只能由一个操作访问共享资源,从而不至于产生死锁。

    2) 操作顺序。在访问共享资源时,有时要保证多个访问操作按照特定的顺序进行。

    以下为11种并发模式:

    1.       单线程执行模式:最简单的解决方案,确保了每次最多只有一个线程访问一个资源。

    2.       静态锁定顺序:死锁的解决方案。

    3.       锁对象:通过锁定唯一对象,使一个操作可以独占访问多个资源。

    4.       受保护的挂起:当线程已经独占访问一个资源,却发现因为某个原因而不能完成对该资源的操作。

    5.       阻行:操作必须完成,或者根本不需要完成时。

    6.       调度器:专门用于处理操作顺序有影响的情形。

    7.       /写锁:处理了某些、操作可以共享同一资源而另一些不可以的情形。

    8.       生产/消费者:协调了生产资源的对象和消费资源的对象。

    9.       双缓冲:在需要资源之前生产出资源。

    10.   异步处理:callback回调技术

    11.   Future:使调用操作的类避免必须知道该操作是同步的还是异步的。

    1.单线程执行模式

    又名临界区(Critical Section)。确保每次最多只有一个线程访问一个资源。

    场景所需的一些要求

           一个类,拥有更新或者设置实例或类变量的方法和属性。

           这个方法,操作的外部资源每次只支持一个操作。

           这个方法可以被不同的线程并发调用

           不要求这个方法一经调用便立即执行,也就是说,可以有短暂的延时。

    实现

    将受保护方法的主体嵌入到lock语句块中,如:

            public void Do()
            
    {
                
    lock (this)
                
    {
                    n 
    += 1;
                }

            }

        目前,我们先锁住this这个对象。我们还可以锁定其它对象,下文会进行分析。

    这里有一种锁分解(Lock Factoring)的技术,如果在lock块中调用另外的一个受保护的方法DoIt,而且这个DoIt方法仅在这一个地方被调用,那么可以把DoIt方法改写为不受保护的方法,这是可以的,而且也能保证同步——这是一种不安全的优化方法,因为DoIt以后会被修改为在其它地方也可以调用。

    lock语句块,与下面语句块是等效的:

            public void Do()
            {
                
    try
                {
                    Monitor.Enter(
    this);

                    n 
    += 1;
                }
                
    finally
                {
                    Monitor.Exit(
    this);
                }
            }

         Monitor这个静态类,可以提供比lock语句块更细致的同步操作。在它的EnterExit方法之间,执行这些受保护的方法。Exit方法要放在finally块中,以表示无论操作是否成功,都要释放这个线程的资源。

    当然这里也可以不使用try语句块,直接使用Monitor.TryEnter静态方法:

            public bool Do()
            
    {
                
    if (!Monitor.TryEnter(this))

                    
    return false;

                n 
    += 1;
     
                Monitor.Exit(
    this);
     
                
    return true;
            }

         Monitor.TryEnter还有另外两种重载方法,多了一个时间参数,表示为了尝试获取这个锁需要等待多长时间:

            public static bool TryEnter(object obj, int millisecondsTimeout);

            public static bool TryEnter(object obj, TimeSpan timeout);

    此外,Monitor静态类还有WaitPulsePulseAll三个静态方法,会在下面进行分析。

    2. 静态锁定顺序

    这个模式是为了解决死锁的。

    死锁:某个操作在继续执行之前,必须等待另一个操作完成完成自己的任务。因为每个操作都在等待其他操作完成自己的任务,所以它们将永远等待下去,什么都不做。

        写一个最简单的死锁模拟程序——直接拿来1-2-3写过一篇文章里面的代码实例:

    Code

         可以看到,foo1方法,在锁定mk1的代码块中,又锁定了mk2;而foo2方法,则在锁定mk2的代码块中,又锁定了mk1。这种产生了互相等待,从而死锁。

           死锁,是人为造成的。为此,我们要避免写出上面的代码。之前介绍过“锁分解的技术”,不失为一种办法,从而减少了锁的数量;但是,如果不能减少锁,也就是说,要直接面对多重锁带来的死锁危机,那么就要使用静态锁定顺序这个同步技术。

    静态锁定顺序,可以认为是对MonitorEnterExit这两个代码块的封装。为此,建立泛型类MultiMonitor<T>

        public class MultiMonitor<T> where T : IComparable
        
    {
            
    public static void Enter(ICollection objs)
            
    {
                T[] myArray 
    = new T[objs.Count];
                objs.CopyTo(myArray, 
    0);
                Array.Sort(myArray);

                
    for (int i = 0; i < myArray.Length; i++)
                
    {
                    Monitor.Enter(myArray[i]);
                }

            }


            
    public static void MyExit(ICollection objs)
            
    {
                
    foreach (Object obj in objs)
                
    {
                    Monitor.Exit(obj);
                }

            }

        }

    新的Enter方法,按照一个固定的顺序,依次锁定objs中的对象,如ABCD,从而永远不会发生死锁(不会有BA的锁定顺序)。

         当然可以重载Enter方法,允许自定义这个排序规则IComparer

            public static void Enter(ICollection objs, IComparer comparer)
            
    {
                T[] myArray 
    = new T[objs.Count];
                objs.CopyTo(myArray, 
    0);
                Array.Sort(myArray, comparer);

                
    for (int i = 0; i < myArray.Length; i++)
                
    {
                    Monitor.Enter(myArray[i]);
                }

            }

    当然,要注意尽量不要在不同的线程中使用不同的排序规则IComparer,因为如果T1线程使用了ABCD的顺序,此时T2线程使用了AC的顺序,这是没有问题的;但是T3线程使用了CA的顺序,就会与T1线程发生死锁了。

    注:这个MultiMonitor<T>是一个很常用的小工具,大家可以将其嵌入到自己的程序中,进行同步操作控制。

    3. 锁对象

    这个同步方式,简单的说,就是创建并锁定一个新的无关对象,将受保护的方法放入这个锁定区域中。

            private Object s_lock = new Object();

            
    public void Foo()
            
    {
                
    lock (s_lock)
                
    {
                    n 
    += 1;
                }

            }

         注:如果受保护的方法是是静态,那么锁对象就也该是静态的。

    示例1:双检锁技术,double-check locking,也就是为Singleton模式的实例创建添加锁,在lock语句块的前后要判断两次对象的存在与否:

        public class Signleton
        
    {
            
    static Signleton mySignleton;

            
    static Object s_lock = new Object();

            
    private Signleton() { }

            
    public static Signleton Instance()
            
    {
                
    //判断单实例对象是否已经被创建
                if (mySignleton == null)
                
    {
                    
    lock (s_lock)
                    
    {
                        
    //锁定后再次判断——有没有另一个线程在创建它
                        if (mySignleton == null)
                            mySignleton 
    = new Signleton();
                    }

                }


                
    return mySignleton;
            }

        }

     

    示例2:事件与线程安全

    同步指导方针指出:方法永远不要在类型对象上加锁,否则这个锁将对所有代码公开(从而任何人都可以写代码锁住这个对象,导致死锁)。

    对于方法同步,我们可以对方法其应用[MethodImpl(MethodImplOptions.Synchronized)]特性。

        [MethodImpl(MethodImplOptions.Synchronized)]
        
    public void Method1()

           但是,对于事件中的addremove,却无法加上这个特性,所以要使用锁对象的技术。

    在事件的addremove上加锁,从而确保每次只有一个addremove可以执行,以免委托对象的链表被破坏:

            public readonly Object m_lock = new Object();

            
    private EventHandler<NewEventArgs> m_NewEvent;

            
    public event EventHandler<NewEventArgs> NewEvent
            
    {
                add
                
    {
                    
    lock (m_lock)
                    
    {
                        m_NewEvent 
    += value;
                    }

                }

                remove
                
    {
                    
    lock (m_lock)
                    
    {
                        m_NewEvent 
    -= value;
                    }

                }

            }

     

    4.受保护的挂起

    如果存在某个条件,它阻止方法完成它应该执行的事情。在这一条件消失之前,将一直挂起该方法。

           这种同步方式的实现,需要使用到Monitor类的WaitPulse方法。

        public class Widget
        
    {
            
    private SomeDataClass myData;
     
            
    public void Foo()
            
    {
                
    lock (myData)
                
    {
                    
    while (!myData.IsOK)
                    
    {
                        Monitor.Wait(myData);
                    }


                    
    //do something
                }

            }


            
    public void Bar(int x)
            
    {
                
    lock (myData)
                

                    
    //这里有一些代码,使myData的IsOK属性改为true

                    Monitor.Pulse(myData);
                }

            }

        }

    Foo方法的逻辑是:只要myDataIsOK属性不为true,就一直挂起而不会跳出while循环,线程会一直处于等待状态:

           Monitor.Wait(myData);

    那么接下来的代码段就不会执行。

    Bar方法的逻辑是:将myDataIsOK属性改为true,也就是条件不再满足,这时不再阻止,也就是释放这个锁:

         Monitor.Pulse(myData);

    下面介绍一个最典型的例子:Queue

    Queue这个数据结构,是先进先出的。

     

    (未完待续)

  • 相关阅读:
    小白的进阶之路7
    小白的进阶之路6
    小白的进阶之路5
    小白的进阶之路4
    小白的进阶之路3
    小白的进阶之路2
    小白的进阶之路1
    02CSS布局13
    02css定位12
    02css盒子模型11
  • 原文地址:https://www.cnblogs.com/Jax/p/1279794.html
Copyright © 2011-2022 走看看