异步编程:线程同步基元对象

 

clip_image001

         从此图中我们会发现 .NET C# 的每个版本发布都是有一个“主题”。即:C#1.0托管代码→C#2.0泛型→C#3.0LINQC#4.0动态语言→C#5.0异步编程。现在我为最新版本的“异步编程”主题写系列分享,期待你的查看及点评。

 

传送门:异步编程系列目录……

 

开始《异步编程:同步基元对象(上)》

示例:异步编程:线程同步基元对象.rar

       如今的应用程序越来越复杂,我们常常需要多线程技术来提高我们应用程序的响应速度。每个线程都由自己的线程ID,当前指令指针(PC),寄存器集合和堆栈组成,但代码区是共享的,即不同的线程可以执行同样的函数。所以在并发环境中,多个线程“同时”访问共享资源时,会造成共享数据损坏,我们可用线程同步锁来防止。(如果多个线程同时对共享数据只进行只读访问是不需要进行同步的)

 

数据损坏

       在并发环境里,当同时对其共享资源进行访问时可能造成资源损坏,为了避免资源损坏,必须对共享资源进行同步或控制对共享资源的访问。如果在相同或不同的应用程序域中未能正确地使访问同步,则会导致出现一些问题,这些问题包括死锁和争用条件等:

1)   死锁:当两个线程中的每一个线程都在试图锁定另外一个线程已锁定的资源时,就会发生死锁。其中任何一个线程都不能继续执行。

2)   争用条件:两个或多个线程都可以到达并执行一个代码块的条件,根据哪个线程先到达代码,程序结果会差异很大。如果所有结果都是有效的,则争用条件是良性的。但是,争用条件可以与同步错误关联起来,从而导致一个进程干扰另一个进程并可能会引入漏洞。通常争用条件的可能结果是使程序处于一种不稳定或无效的状态

EG:线程T修改资源R后,释放了它对R的写访问权,之后又重新夺回R的读访问权再使用它,并以为它的状态仍然保持在它释放它之后的状态。但是在写访问权释放后到重新夺回读访问权的这段时间间隔中,可能另一个线程已经修改了R的状态。

需要同步的资源包括:

1)   系统资源(如通信端口)。

2)   多个进程所共享的资源(如文件句柄)。

3)   由多个线程访问的单个应用程序域的资源(如全局、静态和实例字段)。

要郑重声明的是:

    使一个方法线程安全,并不是说它一定要在内部获取一个线程同步锁。一个线程安全的方法意味着在两个线程试图同时访问数据时,数据不会被破坏。比如,System.Math类的一个静态Max()方法:

public static Int32 Max(Int32 val1,Int32 val2) {
    return (val1<val2)?val2:val1;
}

    这个方法是线程安全的,即使它没有获取任何锁。由于Int32是值类型,所以传给Max的两个Int32值会复制到方法内部。多个线程可以同时调用Max()方法,每个线程处理的都是它自己的数据,线程之间互不干扰。

 

线程同步锁带来的问题

并发的环境里,“线程同步锁”可以保护共享数据,但是也会存在一些问题:

1)   实现比较繁琐,而且容易错漏。你必须标识出可能由多个线程访问的所有共享数据。然后,必须为其获取和释放一个线程同步琐,并且保证已经正确为所有共享资源添加了锁定代码。

2)   由于临界区无法并发运行,进入临界区就需要等待,加锁带来效率的降低。

3)   在复杂的情况下,很容易造成死锁,并发实体之间无止境的互相等待。

4)   优先级倒置造成实时系统不能正常工作。优先级低的进程拿到高优先级进程需要的锁,结果是高/低优先级的进程都无法运行,中等优先级的进程可能在狂跑。

5)   当线程池中一个线程被阻塞时,可能造成线程池根据CPU使用情况误判创建更多的线程以便执行其他任务,然而新创建的线程也可能因请求的共享资源而被阻塞,恶性循环,徒增线程上下文切换的次数,并且降低了程序的伸缩性。(这一点很重要)

 

什么是原子操作

原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事物中断。

如何识别原子操作?32位处理器(x86系列)32位软件理论上一次能处理32位,也就是4个字节的数据;而64位处理器(x64系列)64位软件理论上一次就能处理64位,即8个字节的数据。在处理器|软件能一次处理的位数范围内的单个操作即为原子操作。(这段文字也告诉我们:(164位操作系统或64位软件理论上运行更快;(232位操作系统上为什么不能运行64位软件,而反过来却可以。)

在多线程编程环境中指:一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。.NET为我们提供了多种线程同步的方法,我们可以根据待同步粒度大小来选择合适的同步方式。

    下面介绍下.NET下线程同步的方法。

 

.NET提供的原子操作

1.   易失结构

volatile 关键字指示一个字段可以由多个同时执行的线程修改。JIT编译器确保对易失字段的所有访问都是易失读取和易失写入的方式执行,而不用显示调用Thread的静态VolatileRead()VolatileWrite()方法。

另外,Volatile关键字告诉C#JIT编译器不将字段缓存到CPU的寄存器中,确保字段的所有读取操作都在RAM中进行。(这也会降低一些性能)

volatile 关键字可应用于以下类型的字段:

1)   引用类型。

2)   指针类型(在不安全的上下文中)。请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。换句话说,您无法声明“指向可变对象的指针”。

3)   类型,如 sbytebyteshortushortintuintcharfloat bool

4)   具有以下基类型之一的枚举类型:bytesbyteshortushortint uint

5)   已知为引用类型的泛型类型参数。

6)   IntPtr UIntPtr

 

volatile也带来了一个问题,因为volatile标注的成员不受优化器优化:

egm_amount=m_amount+m_amount  // m_amount是类中定义的一个volatile字段

通常,要倍增一个整数,只需将它的所有位都左移1位,许多编译器都能检测到上述代码的意图,并执行优化。然而,如果m_amountvolatile字段,就不允许执行这个优化,编译器必须生成代码将m_amount读入一个寄存器,再把它读入另一个寄存器,将两个寄存器加到一起,再将结果写回m_amount字段。未优化的代码肯定会更大,更慢。

另外,C#不支持以传引用的方式将volatile字段传给方法。

 

有时为了利用CPU的寄存器和编译器的优化我们会采用下面两种原子操作。

 

2.   互锁结构(推荐使用)

互锁结构是由 Interlocked 类的静态方法对某个内存位置执行的简单原子操作,即提供同步对多个线程共享的变量的访问的方法。这些原子操作包括添加、递增和递减、交换、依赖于比较的条件交换、内存屏障,以及 32 位平台上的 64 long值的读取操作。

Interlocked的所有方法都建立了完美的内存栅栏。换言之,调用某个Interlocked方法之前的任何变量写入都在这个Interlocked方法调用之前执行;而这个调用之后的任何变量读取都在这个调用之后读取。

详细情况请看如下API注释

public static class Interlocked
{
    // 对两个 32|64 位整数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。返回结果:location1的新值。
    public static int Add(ref int location1, int value);
    public static long Add(ref long location1, long value);

    // 以原子操作的形式递增|递减指定变量的值。返回结果:location1的新值。
    public static int Increment(ref int location);
    public static long Increment(ref long location);
    public static int Decrement(ref int location);
    public static long Decrement(ref long location);

    // 比较指定的location1和comparand是否相等,如果相等,则将location1值设置为value。返回结果:location1 的原始值。
    public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class;
    // 以原子操作的形式,将location1的值设置为value,返回结果:location1 的原始值。
    public static T Exchange<T>(ref T location1, T value) where T : class;

    // 按如下方式同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 Interlocked.MemoryBarrier()
    // 调用之后的内存存取,再执行 Interlocked.MemoryBarrier() 调用之前的内存存取的方式。
    /// 此方法在.NET Framework 4.5 中引入,它是 Thread.MemoryBarrier() 方法的包装。 
    public static void MemoryBarrier();

    // 返回一个以原子操作形式加载的 64 位值。location:要加载的 64 位值。
    public static long Read(ref long location);
    ……
}

注意:

1)   在使用Add()Increament()Decrement()方法时可能出现溢出情况,则遵循规则:

a)   如果 location=Int32.MaxValue,则 location+1 =Int32.MinValuelocation+2=Int32.MinValue+1……。

b)   如果 location=Int32.MinValue,则 location- 1 =Int32.MaxValuelocation- 2 =Int32.MaxValue-1……

2)   Read(ref long location) 返回一个以原子操作形式加载的 64 位值。由于 64 位读取操作已经是原子的,因此 64 位系统上不需要 Read 方法。在 32 位系统上,64 位读取操作除非用 Read 执行,否则不是原子的。

3)   Exchange CompareExchange 方法具有接受 object 类型的参数的重载。这重载的第一个参数都是 ref object,传递给此参数的变量严格类型化为object,不能在调用这些方法时简单地将第一个参数强制转换为object类型,否则报错“ref out 参数必须是可赋值的变量”

这实际是类型强制转换的一个细节,强制转换时编译器会生成一个临时引用,然后把这个临时引用传给一个和转换类型相同的引用,这个临时引用比较特别,它不能被赋值,所以会报“ref out 参数必须是可赋值的变量”。

        比如:

int o=2;
// 编译报错“ref 或 out 参数必须是可赋值的变量”
Interlocked.Exchange(ref (object)o,new object());

// 编译通过
objectobj = (object)o;
Interlocked.Exchange(ref obj, new object());

4)   示例

在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:

a)         将实例变量中的值加载到寄存器中。

b)         增加或减少该值。

c)         在实例变量中存储该值。

如果不使用 Increment Decrement,线程可能会在执行完前两个步骤后被抢先。然后由另一个线程执行所有三个步骤。当第一个线程重新开始执行时,它改写实例变量中的值,造成第二个线程执行增减操作的结果丢失。(线程都维护着自己的寄存器)

3.   Thread类为我们提供的VolatileRead()VolatileWrite()静态方法。请参见异步编程:线程概述及使用

 

同步代码块(临界区)

1.   Monitor(监视器)

Monitor(监视器)放置多个线程同时执行代码块。Enter 方法允许一个且仅一个线程继续执行后面的语句;其他所有线程都将被阻止,直到执行语句的线程调用 Exit

Monitor 锁定对象是引用类型,而非值类型,该对象用来定义锁的范围尽管可以向 Enter Exit 传递值类型,但对于每次调用它都是分别装箱的。因为每次调用都创建一个独立的对象(即,锁定的对象不一样),所以 Enter要保护的代码并没有真正同步。另外,传递给 Exit 的被装箱对象不同于传递给 Enter 的被装箱的对象,所以 Monitor 将引发 SynchronizationLockException,并显示以下消息:“从不同步的代码块中调用了对象同步方法。”

    Monitor将为每个同步对象来维护以下信息:

1)   对当前持有锁的线程的引用。

2)   对就绪队列的引用。当一个线程尝试着lock一个同步对象的时候,该线程就在就绪队列中排队。一旦没人拥有该同步对象,就绪队列中的线程就可以占有该同步对象。(队列:先进先出)

3)   对等待队列的引用。占有同步对象的线程可以暂时通过Wait()释放对象锁,将其在等待队列中排队。该队列中的线程必须通过Pulse()\PulseAll()方法通知才能进入到就绪队列。(队列:先进先出)

Monitor静态类

    public static class Monitor
    {
        // 确定当前线程是否保留指定对象锁。
        public static bool IsEntered(object obj);
        // 获取指定对象上的排他锁(设置获取锁的结果lockTaken,通过引用传递。 输入必须为 false。 如果已获取锁,则输出为 true;否则输出为 false)
        public static void Enter(object obj);
        public static void Enter(object obj, ref bool lockTaken);        
        // 在指定的一段时间内,尝试获取指定对象上的排他锁. 
        // (设置获取锁的结果lockTaken,通过引用传递。 输入必须为 false。如果已获取锁,则输出为 true;否则输出为 false)
        // System.TimeSpan,表示等待锁所需的时间量。 值为 -1 毫秒表示指定无限期等待。
        public static bool TryEnter(object obj, TimeSpan timeout);
        public static void TryEnter(object obj, TimeSpan timeout, ref bool lockTaken);

        // 释放指定对象上的排他锁。
        public static void Exit(object obj);

        // 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 System.TimeSpan,表示线程进入就绪队列之前等待的时间量。
        // exitContext标识可以在等待之前退出同步上下文的同步域,随后重新获取该域。
        public static bool Wait(object obj, TimeSpan timeout, bool exitContext);

        //  通知等待队列中的线程锁定对象状态的更改。
        public static void Pulse(object obj);
        // 通知所有的等待线程对象状态的更改。
        public static void PulseAll(object obj);
        ……
    }

    分析:

1)   同一线程在不阻止的情况下允许多次调用 Enter();但在该对象上等待的其他线程取消阻止之前必须调用相同数目的 Exit()

2)   如果释放了锁并且其他线程处于该对象的【就绪队列】中,则其中一个线程将获取该锁。如果其他线程处于【等待队列】中,则它们不会在锁的所有者调用 Exit ()时自动移动到就绪队列中。

3)   唤醒机制:Wait()释放参数指定对象的对象锁,以便允许其他被阻塞的线程获取对象锁。调用Wait()的线程进入【等待队列】中,等待状态必须由其他线程调用方法Pulse()PulseAll()唤醒,使等待状态线程变为就绪状态。

方法Pulse()PulseAll():向【等待队列】中第一个或所有等待线程发送信息,占用对象锁的线程准备释放对象锁。在即将调用Exit()方法前调用,通知等待队列线程移入就绪队列,待执行方法Exit()释放对象锁后被Wait()的线程将重新获取对象锁。

2.   lock

lock .NET为简化Monitor(监视器)而存在的关键字。其行为等价于:

Boolean lockTaken=false;
try
{
    Mnoitor.Enter(锁定对象,ref lockTaken);
    ……
}
Finally
{
    if(lockTaken)
        Monitor.Exit(锁定对象);
}

尽管lock使用起来比Monitor对象更加简洁,然而Monitor类还提供了其他的方法,通过这些方法可以对获得锁的过程有更多的控制,而且可以使用超超时。

3.   流程图

认识了Monitorlock后,我们再看下内部获得独占锁的流程图,能让我们有更好的理解:

image

1.   示例,双检锁(Double-Check Locking)

双检锁(Double-Check Locking),开发人员用它将一个单实例对象的构造推迟到一个应用程序首次请求这个对象的时候进行。这有时也称为延迟初始化(lazy initialization)。

  public sealed class Singleton
    {
        private static Object s_lock = new object();
        private static Singleton s_value = null;
        // 私有构造器,阻止这个类外部的任何代码创建实例
        private Singleton() { }

        public static Singleton GetSingleton()
        {
            if (s_value != null) return s_value;

            Monitor.Enter(s_lock);
            if (s_value == null)
            {
                s_value = new Singleton();
                // Singleton temp = new Singleton();
                // Interlocked.Exchange(ref s_value, temp);
            }
            Monitor.Exit(s_lock);
            return s_value;
        }
    }

    分析:

1)   里面有两个if,当第一个if判断存在对象时就快速返回,就不需线程同步。如果第一个if判断对象还没创建好,就会获取一个线程同步锁来确保只有一个线程构造单实例函数。

2)   细腻的你可能认为会出现一种情况:第一个ifs_value空值读入到一个CPU寄存器中,而到第二个if读取s_value时也是从寄存器中读取该空值,但此时s_value内存中的值可能已经不为空了。

          CLR已经帮我们解决了这个问题,在CLR中任何锁的调用构成了一个完整的内存栅栏,在栅栏之前写入的任何变量都必须在栅栏之前完成;在栅栏之后的任何变量都必须在栅栏之后开始。即此处的Monitor.Enter()使s_value之前寄存器中的缓存无效化,需重新从内存中读取。

3)  Interlocked.Exchange()方法的调用。若不使用此方法可能出现:为Singleton分配内存,将引用赋给s_value,再调用构造器。在调用构造器之前另一个线程访问第一个if语句,并返回了一个构造器还没有执行完毕的实例。

    这个结论是错误的,验证思路:(感谢园友 JustForKim 指出问题)

a)  本想试着借用ildasm工具反编译出IL代码进行查看,结果……看不懂

b)  采取第二种方式:跑两个线程,一个线程创建实例,并在构造函数中加入耗时操作Thread.Spin(Int32.MaxValue),另一个线程不断访问s_value。得出结果是执行完构造函数后才会将变量引用返回。代码如下:

    class Program
    {
        private static Singleton s_value = null;

        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem((obj) => { Singleton.GetSingleton(); });
            ThreadPool.QueueUserWorkItem((obj) => {
                while (true)
                {
                    if (s_value != null)
                    {
                        Console.WriteLine("s_lock不为null");
                        break;
                    }
                }
            });

            Console.Read();
        }

        public sealed class Singleton
        {
            private static Object s_lock = new object();
           
            // 私有构造器,阻止这个类外部的任何代码创建实例
            private Singleton() {
                Thread.SpinWait(Int32.MaxValue);
                Console.WriteLine("对象创建完成");
            }

            public static Singleton GetSingleton()
            {
                if (s_value != null) return s_value;

                Monitor.Enter(s_lock);
                if (s_value == null)
                {
                    s_value = new Singleton();
                    //Singleton temp = new Singleton();
                    //Interlocked.Exchange(ref s_value, temp);
                    Console.WriteLine("赋值完成");
                }
                Monitor.Exit(s_lock);
                return s_value;
            }
        }
    }

输出结果:

image

4)   代码中没有使用try-catch-finally确保锁总是得以释放。原因: (所以我们要避免使用lock关键字)

a)   try块中,如果在更改状态的时候发生了一个异常,这个状态处于损坏状态。锁在finally块中退出时,另一个线程可能操作损坏的状态。

b)   进入和离开try块也会影响方法的性能。

 

使用Win32对象同步:互斥体、事件与信号量

1.   WaitHandle抽象类

    System.Threading.WaitHandle抽象基类提供了三个继承类,如图所示:

clip_image004

等待句柄提供了丰富的等待和通知功能。等待句柄派生自 WaitHandle 类,WaitHandle 类又派生自 MarshalByRefObject。因此,等待句柄可用于跨应用程序域边界同步线程的活动。 

1)   字段:

public const int WaitTimeout           WaitAny返回满足等待的对象的数组索引;如果没有任何对象满足等待,并且WaitAny()设置的等待的时间间隔已过,则返回WaitTimeout

2)         属性:

HandleSafeWaitHandle        获取或设置一个Win32内核对象的句柄,该句柄在构造一个WaitHandle派生类时初始化。

a)         Handle已过时,给 Handle 属性赋新值不会关闭上一个句柄。这可能导致句柄泄漏。

b)         SafeWaitHandle,代替Handle,给 SafeWaitHandle 属性赋新值将关闭上一个句柄。

3)         Close()Dispose()

使用Close()方法释放由 WaitHandle 的实例持有的所有资源。Close()释放后不会像DbConnection对象一样还可打开,所以通常在对象使用完后直接通过IDisposable.Dispose() 方法释放对象。

4)         SignalAndWait()WaitAll()WaitAny()WaitOne()

共同参数:

等待的间隔

如果值是 System.Threading.Timeout.Infinite,即 -1,则等待是无限期的。

是否退出上下文的同步域

如果等待之前先退出上下文的同步域(如果在同步上下文中),并在稍后重新获取它,则为 true;即线程在等待时退出上下文同步域并释放资源,这样该同步域被阻塞的线程才能获取锁定资源。当等待方法返回时,执行调用的线程必须等待重新进入同步域。

SignalAndWait()WaitOne()默认传false

WaitAll()WaitAny()默认传true

WaitOne()基于WaitSingleObjectWaitAny() WaitAll()基于WaitmultipleObjectWaitmultipleObject实现要比WaitSingleObject复杂的多,性能也不好,尽量少用。

a)         SignalAndWait (WaitHandle toSignal, WaitHandle toWaitOn)

toSignal 发出信号并等待toWaitOn。如果信号和等待都成功完成,则为 true;如果等待没有完成,则此方法不返回。这样toSignal所在线程结束前必须调用toWaitOn.Set()或和别的线程协作由别的线程调用toWaitOn.Set()SignalAndWait()才不阻塞调用线程。

b)         WaitAll()

接收WaitHandle对象数组作为参数,等待该数组中的所有WaitHandle对象都收到信号。在具有 STAThreadAttribute 的线程中不支持 WaitAll ()方法。

c)         WaitAny()

接收WaitHandle对象数组作为参数,等待该数组中的任意WaitHandle对象都收到信号。返回值:满足等待的对象的数组索引;如果没有任何对象满足等待,并且WaitAny()设置的等待的时间间隔已过,则为返回WaitTimeout

d)         WaitOne()

阻塞当前线程,直到当前的 WaitHandle 收到信号

e)         注意一个限制:

在传给WaitAny()WaitAll()方法的数组中,包含的元素不能超过64个,否则方法会抛出一个System.NotSupportedException

2.   事件等待句柄--- EventWaitHandleAutoResetEventManualResetEvent

事件等待句柄(简称事件)就是可以通过发出相应的信号来释放一个或多个等待线程的等待句柄。

事件等待句柄通常比使用 Monitor.Wait() Monitor.Pulse(Object) 方法更简单,并且可以对信号发送提供更多控制。命名事件等待句柄也可用于跨应用程序域和进程同步活动,而监视器Monitor只能用于本地的应用程序域。

1)   EventWaitHandle

EventWaitHandle 类允许线程通过发出信号和等待信号来互相通信。信号发出后,可以用手动或自动方式重置事件等待句柄。 EventWaitHandle 类既可以表示本地事件等待句柄(本地事件),也可以表示命名系统事件等待句柄(命名事件或系统事件,对所有进程可见)。

public class EventWaitHandle : WaitHandle
{
    public EventWaitHandle(bool initialState, EventResetMode mode, string name
            , out bool createdNew, EventWaitHandleSecurity eventSecurity);

    // 获取 System.Security.AccessControl.EventWaitHandleSecurity 对象,
    // 该对象表示由当前 EventWaitHandle 对象表示的已命名系统事件的访问控制安全性。
    public EventWaitHandleSecurity GetAccessControl();
    // 设置已命名的系统事件的访问控制安全性。
    public void SetAccessControl(EventWaitHandleSecurity eventSecurity);

    // 打开指定名称为同步事件(如果已经存在)。
    public static EventWaitHandle OpenExisting(string name);
    // 用安全访问权限打开指定名称为同步事件(如果已经存在)。
    public static EventWaitHandle OpenExisting(string name, EventWaitHandleRights rights);
    public static bool TryOpenExisting(string name, out EventWaitHandle result);
    public static bool TryOpenExisting(string name, EventWaitHandleRights rights, out EventWaitHandle result);

    // 将事件状态设置为非终止状态,导致线程阻止。
    public bool Reset();
    // 将事件状态设置为终止状态,允许一个或多个等待线程继续。
    public bool Set();
    ……
}

                         i.              构造函数

initialState

如果为 trueEventWaitHandle为有信号状态,此时不阻塞线程。

EventResetMode

指示在接收信号后是自动重置 EventWaitHandle 还是手动重置。

枚举值:

    public enum EventResetMode
    {
        AutoReset = 0,
        ManualReset = 1,
    }

createdNew   

在此方法返回时,如果创建了本地事件(如果 name 为空字符串)或指定的命名系统事件,则为 true;如果指定的命名系统事件已存在,则为 false。可以创建多个表示同一系统事件的 EventWaitHandle 对象。

eventSecurity

一个 EventWaitHandleSecurity 对象,表示应用于【已命名的系统事件】的访问控制安全性。如果系统事件不存在,则使用指定的访问控制安全性创建它。如果该事件存在,则忽略指定的访问控制安全性。

                       ii.              OpenExisting中使用的EventWaitHandleRights枚举

// 指定可应用于命名的系统事件对象的访问控制权限。
[Flags]
public enum EventWaitHandleRights
{
    // set()或reset()命名的事件的信号发送状态的权限。
    Modify = 2,
    // 删除命名的事件的权限。
    Delete = 65536,
    // 打开并复制某个命名的事件的访问规则和审核规则的权限。
    ReadPermissions = 131072,
    // 更改与命名的事件关联的安全和审核规则的权限。
    ChangePermissions = 262144,
    // 更改命名的事件的所有者的权限。
    TakeOwnership = 524288,
    // 在命名的事件上等待的权限。
    Synchronize = 1048576,
    // 对某个命名的事件进行完全控制和修改其访问规则和审核规则的权限。
    FullControl = 2031619,
}

默认设置为EventWaitHandleRights.Synchronize | EventWaitHandleRights.Modify。如果你显示为其设置权限,也必须给予这两个权限。

2)   AutoResetEvent类(本地事件)

AutoResetEvent用于表示自动重置的本地事件。在功能上等效于用EventResetMode.AutoReset 创建的本地EventWaitHandle

public sealed class AutoResetEvent : EventWaitHandle
{
     public AutoResetEvent(bool initialState);
}

 

使用方式:调用 Set() AutoResetEvent 发信号以释放等待线程。AutoResetEvent 保持终止状态,直到一个正在等待的线程被释放,然后自动重置为非终止状态。如果没有任何线程在等待,则状态将无限期地保持为终止状态,直到一个线程进入就绪队列,此时线程会立马被释放继续执行,等待句柄也会被设置为非终止状态从而等待下一次Set()。

3)   ManualResetEvent(本地事件)

ManualResetEvent表示必须手动重置的本地事件。在功能上等效于用EventResetMode.ManualReset 创建的本地 EventWaitHandle

public sealed class ManualResetEvent : EventWaitHandle
{
    public ManualResetEvent(bool initialState);
}

使用方式:调用 Set()ManualResetEvent发信号以释放等待线程。ManualResetEvent将一直保持终止状态,直到它主动调用 Reset ()方法或直到释放完等待句柄中的所有线程(即所有WaitOne()都获得信号)。

4)   Mutex(互斥体)

Mutex 是同步基元,它只向一个线程授予对共享资源的独占访问权。

MutexAPIEventWaitHandleAPI类似。

public sealed class Mutex : WaitHandle
{
    // 使用一个指示调用线程是否应拥有互斥体的初始所属权的布尔值、一个作为互斥体名称的字符串,
    // 以及一个在方法返回时指示调用线程是否被授予互斥体的初始所属权的布尔值来初始化 Mutex 类的新实例。
    public Mutex(bool initiallyOwned, string name, out bool createdNew);

    // 释放 System.Threading.Mutex 一次。
    public void ReleaseMutex();
    ……
}

                  构造器参数:

initiallyOwned        如果为 true,则给予调用线程已命名的系统互斥体的初始所属权;否则为 false

如果 name 不为空字符串且 initiallyOwned true,则只有当参数 createdNew 在调用后为 true 时,调用线程才拥有已命名的互斥体。否则,此线程可通过调用 WaitOne() 方法来请求互斥体。

使用方式:可以使用Mutex.WaitOne() 方法请求互斥体的所属权。拥有互斥体的线程可以在对 WaitOne()的重复调用中请求相同的互斥体而不会阻止其执行。但线程必须调用 ReleaseMutex() 方法同样多的次数以释放互斥体的所属权。(工作方式类似Monitor监视器)

Mutex 类比 Monitor 类使用更多系统资源,但是它可以使用命名互斥体跨应用程序域边界进行封送处理,可用于多个等待(WaitAny()/WaitAll()),并且可用于同步不同进程中的线程。

在运行终端服务的服务器上,已命名的“系统 mutex”可以具有两级可见性。

a)         如果名称以前缀“Global\”开头,则 mutex 在所有终端服务器会话中均为可见。

b)         如果名称以前缀“Local\”开头,则 mutex 仅在创建它的终端服务器会话中可见。在这种情况下,服务器上各个其他终端服务器会话中都可以拥有一个名称相同的独立 mutex。如果创建已命名 mutex 时不指定前缀,则默认将采用前缀“Local\”。

异常:如果线程终止而未释放 Mutex,则认为该 mutex 已放弃。这是严重的编程错误,因为该 mutex 正在保护的资源可能会处于不一致的状态,获取该 mutex 的下一个线程中将引发 AbandonedMutexException

5)   Semaphore(信号量)

限制可同时访问某一资源或资源池的线程数。

SemaphoreAPIEventWaitHandleAPI类似。

public sealed class Semaphore : WaitHandle
{
    // 初始化 Semaphore 类的新实例,并指定最大并发入口数及初始请求数,以及选择指定系统信号量对象的名称。
    public Semaphore(int initialCount, int maximumCount, string name);

    // 退出信号量并返回调用 Semaphore.Release 方法前信号量的计数。
    public int Release();
    // 以指定的次数退出信号量并返回调用 Semaphore.Release 方法前信号量的计数。
    public int Release(int releaseCount);
    ……
}

使用方式:信号量的计数在每次线程进入信号量时减小(egWaitOne()),在线程释放信号量时增加(egRelease())。当计数为零时,后面的请求将被阻塞,直到有其他线程释放信号量。

 

WaitHandle的派生类具有不同的线程关联

1.         Mutex具有线程关联。拥有Mutex 的线程必须将其释放,而如果在不拥有mutex的线程上调用ReleaseMutex方法,则将引发异常ApplicationException

2.         事件等待句柄(EventWaitHandleAutoResetEvent ManualResetEvent)以及信号量(Semaphore)没有线程关联。任何线程都可以发送事件等待句柄或信号量的信号

 

命名事件

Windows 操作系统允许事件等待句柄具有名称。命名事件是系统范围的事件。即,创建命名事件后,它对所有进程中的所有线程都是可见的。因此,命名事件可用于同步进程的活动以及线程的活动。系统范围的,可以用来协调跨进程边界的资源使用。

注意:

1)         因为命名事件是系统范围的事件,所以可以有多个表示相同命名事件的 EventWaitHandle 对象。每当调用构造函数或 OpenExisting 方法时时,都会创建一个新的 EventWaitHandle 对象。重复指定相同名称会创建多个表示相同命名事件的对象。

2)         使用命名事件时要小心。因为它们是系统范围的事件,所以使用同一名称的其他进程可能会意外地阻止您的线程。在同一计算机上执行的恶意代码可能以此作为一个切入点来发动拒绝服务攻击。

应使用访问控制安全机制来保护表示命名事件的 EventWaitHandle 对象

a)         最好通过使用可指定 EventWaitHandleSecurity 对象的构造函数来实施保护。

b)         也可以使用 SetAccessControl 方法来应用访问控制安全,但这一做法会在事件等待句柄的创建时间和设置保护时间之间留出一段漏洞时间。

使用访问控制安全机制来保护事件可帮助阻止恶意攻击,但无法解决意外发生的名称冲突问题。 

3)   MutexSemaphore对象类似EventWaitHandle。(AutoResetEvent ManualResetEvent 只能表示本地等待句柄,不能表示命名系统事件。

 

利用特性进行上下文同步和方法同步

1.   SynchronizationAttribute(AttributeTargets.Class)

应用SynchronizaitonAttribute的类,CLR会自动对这个类实施同步机制。为当前上下文和所有共享同一实例的上下文强制一个同步域(同步域之所以有意义就在于它不能被多个线程所共享。换句话说,一个处在同步域中的对象的方法是不能被多个线程同时执行的。这也意味着在任一时刻,最多只有一个线程处于同步域中)

被应用SynchronizationAttribute的类必须是上下文绑定的。换句话说,它必须继承于System.ContextBoundObject类。

一般类所建立的对象为上下文灵活对象(context-agile),它们都由CLR自动管理,可存在于任意的上下文当中(一般在默认上下文中)。而 ContextBoundObject 的子类所建立的对象只能在建立它的对应上下文中正常运行,此状态被称为上下文绑定。其他对象想要访问ContextBoundObject 的子类对象时,都只能通过代透明理来操作。

示例:

using System.Runtime.Remoting.Contexts;
class Synchronization_Test
{
    public static void Test()
    {
        class1 c = new class1();
        ThreadPool.QueueUserWorkItem(o => { c.Test1(); });
        Thread.Sleep(100);
        ThreadPool.QueueUserWorkItem(o => { c.Test2(); });
    }

    [Synchronization(SynchronizationAttribute.REQUIRED)]  
    internal class class1 : ContextBoundObject
    {// 必须继承于System.ContextBoundObject类
        public void Test1()
        {
            Thread.Sleep(1000);
            Console.WriteLine("Test1");
            Console.WriteLine("1秒后");
         }

        public void Test2()
        {
            Console.WriteLine("Test2");
        }
    }
}
/*
  输出:
    Test1
    1秒后
    Test2
/*

SynchronizationAttribute 类对那些没有手动处理同步问题经验的开发人员来说是很有用的,因为它囊括了特性所标注的类的实例变量,实例方法以及实例字段。它不处理静态字段和静态方法的同步。 SynchronizationAttribute锁的吞吐量低,一般不使用)

除此之外,还有另一个SynchronizationAttributeSystem.EnterpriseServices. SynchronizationAttribute拥有同样的目的只不过在内部使用了COM+中用于同步的企业服务。

基于以下原因,我们优先选择使用System.Runtime.Remoting.Contexts.SynchronizationAttribute

1)   它的使用更加高效。

2)   相较于COM+的版本,该机制支持异步调用。

2.   MethodImplAttribute(AttributeTargets.Constructor | AttributeTargets.Method)

如果临界区跨越整个方法,则可以通过将 System.Runtime.CompilerServices.MethodImplAttribute 放置在方法上,并指定MethodImplOptions.Synchronized参数,可以确保在不同线程中运行的该方法以同步的方式运行。

a)   MethodImplAttribute应用到instance method相当于lock(this),锁定该类实例。所以它们和不使用此特性,直接使用lock(this)的方法互斥。

b)   MethodImplAttribute应用到static method相当于lock (typeof (该类))。所以它们和不使用此特性,直接使用lock (typeof (该类))的方法互斥。

该属性将使当前线程持有锁,直到方法返回如果可以更早释放锁,则使用 Monitor 类或 lock 语句而不是该属性。

验证示例:

internal class class1
{
    [MethodImpl(MethodImplOptions.Synchronized)]
    public static void Static_Test1()
    {
        Thread.Sleep(1000);
        Console.WriteLine("MethodImpl特性标注的静态方法----1");
        Console.WriteLine("1秒后释放lock (typeof(class1))");
    }

    public static void Static_Test2()
    {
        // MethodImplAttribute应用到static method相当于lock (typeof (该类))。
        lock (typeof(class1))
        {
            Console.WriteLine("MethodImpl特性标注的静态方法----2");
        }
    }

    public static void Static_Test3()
    {
        Console.WriteLine("MethodImpl特性标注的静态方法----3");
    }
}
// 调用:
ThreadPool.QueueUserWorkItem(o => { class1.Static_Test1(); });
Thread.Sleep(100);
ThreadPool.QueueUserWorkItem(o => { class1.Static_Test2(); });
ThreadPool.QueueUserWorkItem(o => { class1.Static_Test3(); });
/* 
输出:
    MethodImpl特性标注的静态方法----3
    MethodImpl特性标注的静态方法----1
    1秒后释放lock (typeof(class1))
    MethodImpl特性标注的静态方法----2
*/

 

集合类的同步

.NET在一些集合类,比如QueueArrayListHashTableStack,已经提供了Synchronized ()方法和SyncRoot属性。

1.   Synchronized()原理是返回了一个线程安全的对象,比如Hashtable.Synchronized(new Hashtable())返回了一个继承自Hashtable类的SyncHashtable对象,该对象在冲突操作上进行了lock(SyncRoot属性)从而确保了线程同步。

2.   SyncRoot属性提供了一个专门待锁定对象,如Hashtable中实现源码:

public virtual object SyncRoot
{
    get
    {
        if(this._syncRoot==null)
        {
            Interlocked.CompareExchange(ref this._syncRoot, new object(), null); 
        }
        return this._syncRoot;
    }
}

从源码可知,SyncRoot实际上就是通过Interlocked返回一个同步的object类型对象。

注意:此处的SyncRoot模式并不推荐使用,因为至始至终都应使用私有的锁;推荐在自己的类中实现私有的SyncRoot模式并使用。

 

 

 

 

 

    本博文介绍了死锁,争用条件,线程同步锁带来的问题,原子操作,volatile\Interlocker\Monitor\WaitHandle\Mutex\EventWaitHandle\AutoResetEvent\ManualResetEvent\Semaphore,SynchronizationAttribute\MethodImplAttribute……
    接下来将介绍.NET4.0新增加的混合线程同步基元,篇幅较长所以分为上、下两篇。在下篇将介绍.NET4.0增加的新混合线程同步基元,这些新基元在一些场合下为我们提供了更好的性能,之所以性能好是因为用户基元模式与内核基元模式的性能差别,敬请观看下文。

    本节到此结束,感谢大家的观赏。赞的话还请多推荐啊 (*^_^*)----预祝各位“元旦快乐”

 

 

推荐阅读:
            《理论与实践中的 C# 内存模型》

 

参考资料:

         CLR via C#(第三版)

         MSDN

 

 

posted on 2012-12-31 01:34  滴答的雨  阅读(9467)  评论(64编辑  收藏  举报