DotNet同步策略

公共语言运行库(CLR)提供了三种策略去同步访问实现,静态方法和实例字段,即:
同步上下文;
同步代码区;
手控同步.
1 同步上下文
     上下文是一组属性或使用规则,这组属性或使用规则对执行时相关的对象集合是通用的.能够添加的上下文属性包括有关同步,线程式亲缘性的事务处理的策略.简言之,上下文把类似的对象组合在一起.这种策略将使用SynchronizationArrribute类对ContextBoundObject对象进行简单的自动同步,驻留在上下文中,符合上下文规则的对象称为上下文对象称为上下文绑定对象. .NET把同步锁和对象自动关联起来.在每种方法调用前锁定对象,方法返回后释放对象(让其它等待着的线程访问).由于线程同步和并发管理是开发人员遇到的最困难的任务.因此这种方法极大的提高了效率.
    SynochronizationAttribute 类对缺少手工处理同步经难的程序员来说是很有用的.因为它包含了实例变量,实例方法和应用这个属性的类的实例字段.然而,它不是处理静态字段和方法同步.如果必须同步代码块,它也不起作用.同步整个对象是对轻松使用必须付出的代码.在使用SynchronizationAttribute编程时,SynchronizationAttribute非常方便.因为一个上下文(例如事务处理)中的对象由 COM+ 运行库组合在一起.我们可以使用下面的代码来使用一个类具有安全性:
    [SynchronizationAttribute(SynchronizationOption.Required)]
    public class className
    {
            //class code here
    }
   
    SynchronizationAttribute类有两个构造函数.一个不带参数(默认SynchronizationOption.Required),另一个构造函数包含一个SynchronizationOption 枚举.这个枚举的值如下所示:

Disabled            :忽略对象的同步请求,即对象不是线程安全的.
NotSupported    :创建组件时没有控制同步,即论调用者牌什么状态,对象都不能参与任何同步操作.
Required            :确保所创建的所有的对象都已同步.
RequiredNew    :无论调用者是什么,组件总是参与新的同步.
Supported           :使用这个选项的对象只有存在(取决于调用者)时,才能参与同步操作.

2 同步代码区
    第二种同步策略是同步代码区,这些特定的代码区是方法中重要的代码段.它们可以改变对象的状态,或者更新另一个资源(如数据库,文件等).先看Monitor类.
    Monitor类用于同步代码区,其方式是使用Monitor.Enter()方法获得一个锁.然后,使用Monitor.Exit()方法释放该锁.锁的概念通常用于解释Monitor类.一个线程获得锁,其它线程就要等到该锁释放后才能使用.一旦在代码区上获取了一个锁,就可以在Monitor.Enter()和Monitor.Exit()程序块内使用如下方法:
    Wait()--此方法用于释放对象上的锁.并暂停当前的线程.直到它重新获得锁.
    Pulse()-此方法用于通知正在队列中等待的线程,对象的状态已经改变了.
    PulseAll():此方法用于通知所有正在队列中等待的线程,对象的状态已经有了改变.

    Enter()方法和Exit()方法
    Monitor方法是静态方法.能够被Monitor类自身调用,而不是由该类的实例调用.在.NET Framework中,每个对象都有一个与之相关的锁,可以获取和释放该锁,这样,在任一时刻仅有一个线程可以访问对象的实例变量和方法.与之类似,.NET Framework中的每个对象也提供一种允许它处于等待状态的机制.正如锁的机制. 设计这种机制的主要原因是帮助线程间的通信.如果一个线程进入对象的重要代码段,并需要一定的条件才能存在,而另一个线程可以在该代码段中创建该条件,此时就需要这种机制.
    诀窍在于,在任一时刻,重要的代码段只允许一个线程访问,当第一个线程进入代码段后,其它的线程就不能再进入了,如果第线程A从数据库中获取了一些数据,在线程A接收到所有的数据,并处理这些数据的过程中,另一个线程B必须调用Wait()方法.等待线程A通知它何时才能接收数据.在接收到数据时,线程A就调用Pulse()方法,通知线程B,让线程B处理数据.这可以通过"等待和发出脉冲"机制来实现.第一个线程进入重要代码段,执行Wait() 方法. Wait() 方法先释放锁, 再使第一个线程进入等待状态.现在第二个线程进入代码段.修改必须的条件.然后调用Pulse() 方法来通知等待的线程.当条件满足后,就可以继续执行.接着第一个线程先重新获得锁,再从Monitor.Wait() 方法中返回,并从调用Monitor.Wait()的地方继续执行.任何两个线程都不能同时进入 Enter()  方法.
    下面是一个有关Monitor的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        private int result = 0;
        public Program()
        {
        }
        public void NonCriticalSection()
        {
            Console.WriteLine("Enter thread " + Thread.CurrentThread.GetHashCode());
            for (int i = 0; i <= 15; i++)
            {
                Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(2500);
            }
            Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
        }
        public void CriticalSection()
        {
            Monitor.Enter(this);
            Console.WriteLine("Entered Thread " + Thread.CurrentThread.GetHashCode());
            for (int i = 0; i <= 15; i++)
            {
                Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(1500);
            }
            Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
            Monitor.Exit(this);
        }
        public static void Main()
        {
            Program p = new Program();
            int i = Console.Read();
            if (i % 2 == 0)
            {
                Thread t1 = new Thread(new ThreadStart(p.NonCriticalSection));
                t1.Start();
                Thread t2 = new Thread(new ThreadStart(p.NonCriticalSection));
                t2.Start();
            }
            else
            {
                Thread t1 = new Thread(new ThreadStart(p.CriticalSection));
                t1.Start();
                Thread t2 = new Thread(new ThreadStart(p.CriticalSection));
                t2.Start();
            }
        }
    }
}

在上面的例子中,重要代码段定义为 Monitor.Enter(this) 和 Monitor.Exit(this)之间的代码块.参数this指定应将锁保存在当前对象上,把哪个对象作为参数传递给Enter()方法况是不好确定.在需要锁定对象,不上其他线程访问它时,应把指针this作为参数传递.

Wait()和Pulse()机制
    Wait()和Pulse()机制用于线程式间的交互.当在一个对象上执行Wait()时,正在访问该对象的线程就会进入等待状态,直到它得到一个唤醒信号.Pulse()和PulseAll()用于给等待线程发送信号.下面列出的上一个Wait()和Pulse()方法如何工作的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ConsoleApplication1
{
    class LockMe
    {
        public LockMe()
        {
        }
    }
    class Monitor1
    {
        public LockMe _lm;
        public Monitor1(LockMe lm)
        {
            this._lm = lm;
        }

        public void CriticalSection()
        {
            Monitor.Enter(this._lm);
            Console.WriteLine("Enter M1");
            for(int i= 0 ;i < 10; i++)
            {
                Thread.Sleep(3000);
                Console.WriteLine("M1 Wait");
                Monitor.Wait(this._lm);

                Console.WriteLine("M1 WorkUP");
                Monitor.Pulse(this._lm);
               
            }
            Monitor.Exit(this._lm);
        }
    }
    class Monitor2
    {
        private LockMe _lm;
        public Monitor2(LockMe lm)
        {
            this._lm = lm;
        }
        public void CriticalSection()
        {
            Monitor.Enter(this._lm);
            Console.WriteLine("Enter M2");
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(3000);
                Console.WriteLine("M2 WorkUp");
                Monitor.Pulse(this._lm);
                Console.WriteLine("M2 Wait");
                Monitor.Wait(this._lm);
            }
            Monitor.Exit(this._lm);
        }
    }
    class Program
    {
        public static void Main()
        {
            LockMe l = new LockMe();
            Monitor1 m1 = new Monitor1(l);
            Monitor2 m2 = new Monitor2(l);

            Thread t1 = new Thread(new ThreadStart(m1.CriticalSection));
            t1.Start();

            Thread t2 = new Thread(new ThreadStart(m2.CriticalSection));
            t2.Start();
        }
    }
}
注意:当线程执行Monitor.Wait()方法时,它会临时释放LockMe对象上的锁,以便其它线程可以访问它.线程 t1 进入等待状态后, 线程t2就可以自由访问 LockMe 对象.即使LockMe对象是一个独立的对象,这两个线程也均指向同一个对象的引用. 线程t2获得LockMe对象上的锁后,一进入For循环,就给LockMe对象上等待的线程发送一个运行进通知.之后便进行等待状态.然后t1 醒来,获得LockMe对象上的锁,接着 t1 执行一些操作,并给  LockMe对象上等待的线程发送一个运行时通知.这个循环一直持续到For循环结束.
注意每一个 Enter() 方法都要伴随一个 Exit()方法,否则程序就会进入列循环.
    Enter将一个对象作为其参数,如果该对象参数为 null, 方法变量或者值类型对象如整型值,系统将会抛出一个异常.

TryEnter()方法
   
Moniter类的TryEnter()方法非常类似于Enter()方法,它试图获得对象的独占锁,不过它不会像Enter()方法那样暂停.如果线程成功进入,则TryEnter()方法返回True.
   
TryEnter()方法有三个重载,其中两个都带超时参数,表示等待锁定的时间.如:
    Monitor.TryEnter(this,1000);
    ......
    在可能发生竞争,但不希望线程睡眠某个示指定的时间,就可以使用TryEnter().
Lock
    Lock关键字可以用作monitor方法的一个替换用法,下面的两部分代码是等价的:
    Monitor.Enter(X);
    .....
    Monitor.Exit(X);

    lock(this)
    {
       .....
    }

ReaderWriterLock类
    ReaderWriterLock定义了实现单写程序和多读程序语义的锁.这个类主要用于文件操作.即多个线程可以读取文件,但只能用一个线程来更新文件.ReaderWriterLock类中4个主要的方法是:
     AcquireReaderLock():该重载方法获得一个读程序锁,超时值使用整数或TimeSpan.超时是可用于检测死锁的好工具.
    AcquireWriterLock():该重载方法获得一个写程序锁.超时值使用整数或TimeSpan.
    ReleaseReaderLock():释放读程序锁.
    ReleaseWriterLock():释放写程序锁.

    使用ReaderWriterLock类时,任意数量的线程都可以同时安全的读取数据.只有当线程进行更新时,数据才被锁定.只有在没有占用锁的写程序线程时,读程序线程才能获得锁.只有没有占用锁的读程序或写程序线程时,写程序才能获得锁.
   

3手控同步
    DotNet Framework提供了一套经典的技术允许程序员使用类似于Win32线程的低级线程API创建和管理多线程应用程序.下表列出了System.Threading命名空间中的一些可以用于手控同步和类.
   
    AutoResetEvent:
       AutoResetEvent类可以使线程处于等待状态.直到通过调用Set()方法某事件将它置于信号状态为止.有信号状态是指没有线程在等待.在释放一个等待线程后.系统自动将AutoResetEvent类生置为无信号状态.如果没正在等待的线程,事件对象的状态保持警惕为有信号状态.指定ManualRest参数为False时,AutoResetEvent类对应于Win32的CreateEvent调用.
   
    ManualResetEvent
       ManualResetEvent类也用来使线程式处于等待状态.直到通过调用Set()方法某事件将它置于有信号状态.ManualResetEvent对象的状态会一直保持有信号,直到Reset()方法将它设置为无信号状态.指定 bManualReset 参数为True时, ManualResetEvent对应于Win32的CreateEvent调用.

    Mutex
       Mutex锁提供了跨进程和跨线程同步.如果没有线程占用Mutex锁,Mutex的状态就轩为有信号状态.Mutex并不上具有Monitor类的所有等待和发出脉冲功能.但可以创建在进程之间使用的命名互斥(使用重载构造函数),使用Mutex 类优于使用 Monitor 类,因为Mutex类可以跨进程使用,而Monitor类不行.

    Interlocked
       Interlocked类为在多个线程之间共享的原子的,非暂停整体更新提供了方法.如果变量位于共享的内在中,不同进程的线程就可以使用这种机制.

 ManualResetEvent类
       ManulaResetEvent类对象只能拥有两种状态之一:有信号(True)和无信号(False),ManualResetEvent类继承于WaitHandler类,其构造函数的参数可以确定对象的初始状态,Set()和Reset()方法返回一个布尔值,表示是否进行了成功的修改.
    下面的代码首先创建一个对象 mansig , 并赋予false值,方法WaitOne()将一直等待 mansig 变为 true 或超时为止. 由于时间在等待中过去.且 mansig 的值没有被设置障为 true,所以程序就停止暂停并返回 false值.
       public static void  Main(string [] args)
        {
             ManualResetEvent mansig;
             mansig = new ManualResetEvent(false);
             Console.WriteLine("ManualResetEvent Before WaitOne");
             bool b = mansig.WaitOne(1000,false);
             Console.WriteLine("ManualResetEvent After WaitOne" + b);
          }
     上面的代码输出如下:
          ManualResetEvent Before Waitone
          ManulaResetEvent After WaitOne false
       布尔值 false 把ManualResetEvent对象的初始状态设置为无信号,接着调用基类的 WaitHandle的 WaitOne() 方法,WaitOne()方法有两个参数,第一个参数是线程在WaitOne()方法中等待的毫秒数,因此在线程退出前等待一秒,第二个参数是exitContext , 如果已以在上下文的同步域中,想退出同步上下文,或者要重新获得同步上下文,就把该参数设置为 true;程序在WaitOne()方法中暂停一秒,然后因为超时而退出.ManulaResetEvent的状态仍然是 false,因而WaitOne()返回的布尔值是 false. 如果在创建 ManualResetEvent时就把它的状态设定为有信号(treu)时,如下代码:
       ManualResetEvent mansig;
       mansig = new ManualResetEvent(true):
       Console.WriteLine("ManualResetEvent Before WaitOne");
       bool b = mansig.WaitOne(1000,false);
       Console.WriteLine("ManualResetEvent After WaitOne " + b);
    输出结果如下所示:
       ManualResetEvent Before WaitOne
       ManualResetEvent After WaitOne true

       将ManualResetEvent的初始状态修改为有信号的,即使指定超时值为1000毫秒,线程在WaitOne()中也不会等待.当ManualResetEvent的状态为无信号状态时,线程将等待状态变为有信号状态. 但1000毫秒后线程超时.状态 已经是有信号的.必须调用ManualResetEvent 的Reset()方法.为了把状态修改为有信号的,必须调用Set()方法.
       下面的代码演示了如何使用 Set() 方法和 Reset()方法.
       static void Main()
       {
             ManualResetEvent mre = new ManualResetEvent(true);
             mre = new ManualResetEvent(1000,true);
             bool state = mre.WaitOne(1000,true);
             Console.WriteLine("ManualResetEvent After WaitOne" + state);
           
             //change the state non-signaled
             mre.Reset();
             state = mre.WaitOne(5000,true);
             Console.WriteLine("ManualResetEvent After secodn WaitOne" + state);    
        }
       程序输出:
       ManualResetEvent Ater first WaitOne  true
       ManualResetEvent After Second WaitOne false
       ManualResetEvent对象的构造函数在将其初始化时设置为有信号的(true),结果,线程不在第一个WaitOne()方法中等待,并返回 true 值,接着将ManualResetEvent对象的状态重新设置为无信号的(false),于是线程在超时之间等待5秒钟.
       ManualResetEvent对象的WaitAll() 方法将等待所有的事件对象都变成 true 或者有信号时,否则它将保持不变直到超时.WaitAny()方法等待任一事件对象变成true或有信号.
   
AutoResetEvent类
      
AutoResetEvent类的工作方式类似于ManualResetEvent类,它等待时间超时或者事件变成有信号的状态.接着将此事件通知等待线程.ManualResetEvent和AutoResetEvent的一个重要区别就是AutoResetEvent在WaitOne()方法中改变状态.下面的代码展示了AutoResetEvent类的用法:
       static void Main()
       {
          AutoResetEvent are = new AutoResetEvent (true):
          Console.WriteLine("Before First WaitOne");
          bool state = are.WaitOne(1000,true);

          Console.WriteLine("After First WaitOne " + state);
          state = are.WaitOne(5000,true);
          Console.WriteLine("After Second WaitOne" + state);
       }

    静态变量,方法和同步
       静态变量和方法与同步锁中的实例 变量和方法有不同的作用.静态变量是类变量,而属于一个对象的变量是对象或者实例变量.静态变量只能有一个实例,静态方法由同一类的多个对象共享.同一类的每个对象都有自己的一组实例变量和方法.如果同步一个静态变量或者静态方法,锁就要应用于整个类上,其结果是,其它对象不能使用此类的静态变量.
ThreadStaticAttribute 类
       ThreadStaticAttribute类用于在静态变量上为每个执行它的线程创建一个单独的变量,而不是在线程之间共享(默认行为)静态变量.这意味着,带有ThreadStaticAttribute的静态变量不会在访问变量的不同线程之间共享.每个访问变量的线程都有该变量的一个副本.如果一个线程修改了变量,另一个访问变量的线程就不能看到这些变化.这种行为是有背于静态变量的默认行为的.总之,ThreadStaticAttribute提供了最好的方法(静态和实例).如下代码:
    class Program
    
{
        
static void Main(string[] args)
        
{
            StaticThread ts 
= new StaticThread();
            Thread t1 
= new Thread(new ThreadStart(ts.Run));
            Thread t2 
= new Thread(new ThreadStart(ts.Run));
            t1.Start();
            t2.Start();

            Thread.Sleep(
1000000);
        }

    }

    
class StaticThread
    
{
        [System.ThreadStatic]
        
public static int x = 1;
        
public static int y = 1;
        
public void Run()
        
{
            
for (int i = 0; i <= 10; i++)
            
{
                Thread t2 
= Thread.CurrentThread;
                x
++;
                y
++;                Console.WriteLine("i=" + i + "ThreadID=" + t2.GetHashCode() + "x(static attribute)= " + x + " y = " + y);
                Thread.Sleep(
1000);
            }

        }


    }
        通过运行上面的代码可知:静态变量是类量,它的值在类的多个对象中保持不变,ThreadStaticAttribute允许每个访问静态变量的线程拥有自己的副本,上面的代码中,变量x 使 ThreadStaticAttribute应用于它,结果,线程t1和t2将分别拥有静态变量x的一个副本,并且线程t1对变量x的修改对线程t2来说是不可见的.另一方面,线程t1对变量y的修改对线程t2来说是可见的.
       带有ThreadStaticAttribute的静态变量和实例变量之间的区别是:静态变量不需求对象访问它,但如果没有创建对象的实例就试图访问实例变量,刚会抛出异常.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章