也谈多线程同步

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

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这个数据结构,是先进先出的。

 

(未完待续)

原文地址:https://www.cnblogs.com/Jax/p/1279794.html