建网站外包,嵌入式培训总结,怎么自己做代刷网站,上海物流公司网站建设开始《异步编程#xff1a;同步基元对象#xff08;上#xff09;》
示例#xff1a;异步编程#xff1a;线程同步基元对象.rar 如今的应用程序越来越复杂#xff0c;我们常常需要多线程技术来提高我们应用程序的响应速度。每个线程都由自己的线程ID#xff0c;当前指令…
开始《异步编程同步基元对象上》
示例异步编程线程同步基元对象.rar 如今的应用程序越来越复杂我们常常需要多线程技术来提高我们应用程序的响应速度。每个线程都由自己的线程ID当前指令指针(PC寄存器集合和堆栈组成但代码区是共享的即不同的线程可以执行同样的函数。所以在并发环境中多个线程“同时”访问共享资源时会造成共享数据损坏我们可用线程同步锁来防止。(如果多个线程同时对共享数据只进行只读访问是不需要进行同步的) 数据损坏 在并发环境里当同时对其共享资源进行访问时可能造成资源损坏为了避免资源损坏必须对共享资源进行同步或控制对共享资源的访问。如果在相同或不同的应用程序域中未能正确地使访问同步则会导致出现一些问题这些问题包括死锁和争用条件等
1) 死锁当两个线程中的每一个线程都在试图锁定另外一个线程已锁定的资源时就会发生死锁。其中任何一个线程都不能继续执行。
2) 争用条件两个或多个线程都可以到达并执行一个代码块的条件根据哪个线程先到达代码程序结果会差异很大。如果所有结果都是有效的则争用条件是良性的。但是争用条件可以与同步错误关联起来从而导致一个进程干扰另一个进程并可能会引入漏洞。通常争用条件的可能结果是使程序处于一种不稳定或无效的状态。
EG线程T修改资源R后释放了它对R的写访问权之后又重新夺回R的读访问权再使用它并以为它的状态仍然保持在它释放它之后的状态。但是在写访问权释放后到重新夺回读访问权的这段时间间隔中可能另一个线程已经修改了R的状态。
需要同步的资源包括
1) 系统资源如通信端口。
2) 多个进程所共享的资源如文件句柄。
3) 由多个线程访问的单个应用程序域的资源如全局、静态和实例字段。
要郑重声明的是 使一个方法线程安全并不是说它一定要在内部获取一个线程同步锁。一个线程安全的方法意味着在两个线程试图同时访问数据时数据不会被破坏。比如System.Math类的一个静态Max()方法 1 2 3 public static Int32 Max(Int32 val1,Int32 val2) { return (val1val2)?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) 类型如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。
4) 具有以下基类型之一的枚举类型byte、sbyte、short、ushort、int 或 uint。
5) 已知为引用类型的泛型类型参数。
6) IntPtr 和 UIntPtr。 volatile也带来了一个问题因为volatile标注的成员不受优化器优化
egm_amountm_amountm_amount // m_amount是类中定义的一个volatile字段
通常要倍增一个整数只需将它的所有位都左移1位许多编译器都能检测到上述代码的意图并执行优化。然而如果m_amount是volatile字段就不允许执行这个优化编译器必须生成代码将m_amount读入一个寄存器再把它读入另一个寄存器将两个寄存器加到一起再将结果写回m_amount字段。未优化的代码肯定会更大更慢。
另外C#不支持以传引用的方式将volatile字段传给方法。 有时为了利用CPU的寄存器和编译器的优化我们会采用下面两种原子操作。 2. 互锁结构推荐使用
互锁结构是由 Interlocked 类的静态方法对某个内存位置执行的简单原子操作即提供同步对多个线程共享的变量的访问的方法。这些原子操作包括添加、递增和递减、交换、依赖于比较的条件交换、内存屏障以及 32 位平台上的 64 位long值的读取操作。
Interlocked的所有方法都建立了完美的内存栅栏。换言之调用某个Interlocked方法之前的任何变量写入都在这个Interlocked方法调用之前执行而这个调用之后的任何变量读取都在这个调用之后读取。
详细情况请看如下API和注释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 CompareExchangeT(ref T location1, T value, T comparand) where T : class; // 以原子操作的形式将location1的值设置为value返回结果location1 的原始值。 public static T ExchangeT(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) 如果 locationInt32.MaxValue则 location1 Int32.MinValuelocation2Int32.MinValue1……。
b) 如果 locationInt32.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 参数必须是可赋值的变量”。 比如 1 2 3 4 5 6 7 int o2; // 编译报错“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静态类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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监视器而存在的关键字。其行为等价于 1 2 3 4 5 6 7 8 9 10 11 Boolean lockTakenfalse; try { Mnoitor.Enter(锁定对象,ref lockTaken); …… } Finally { if(lockTaken) Monitor.Exit(锁定对象); } 尽管lock使用起来比Monitor对象更加简洁然而Monitor类还提供了其他的方法通过这些方法可以对获得锁的过程有更多的控制而且可以使用超超时。
3. 流程图
认识了Monitor和lock后我们再看下内部获得独占锁的流程图能让我们有更好的理解 1. 示例双检锁(Double-Check Locking)
双检锁(Double-Check Locking)开发人员用它将一个单实例对象的构造推迟到一个应用程序首次请求这个对象的时候进行。这有时也称为延迟初始化lazy initialization。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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) 细腻的你可能认为会出现一种情况第一个if将s_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。得出结果是执行完构造函数后才会将变量引用返回。代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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; } } } 输出结果 4) 代码中没有使用try-catch-finally确保锁总是得以释放。原因 (所以我们要避免使用lock关键字)
a) 在try块中如果在更改状态的时候发生了一个异常这个状态处于损坏状态。锁在finally块中退出时另一个线程可能操作损坏的状态。
b) 进入和离开try块也会影响方法的性能。 使用Win32对象同步互斥体、事件与信号量
1. WaitHandle抽象类 System.Threading.WaitHandle抽象基类提供了三个继承类如图所示 等待句柄提供了丰富的等待和通知功能。等待句柄派生自 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()基于WaitmultipleObject。WaitmultipleObject实现要比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. 事件等待句柄--- EventWaitHandle、AutoResetEvent、ManualResetEvent
事件等待句柄简称事件就是可以通过发出相应的信号来释放一个或多个等待线程的等待句柄。
事件等待句柄通常比使用 Monitor.Wait() 和 Monitor.Pulse(Object) 方法更简单并且可以对信号发送提供更多控制。命名事件等待句柄也可用于跨应用程序域和进程同步活动而监视器Monitor只能用于本地的应用程序域。
1) EventWaitHandle
EventWaitHandle 类允许线程通过发出信号和等待信号来互相通信。信号发出后可以用手动或自动方式重置事件等待句柄。 EventWaitHandle 类既可以表示本地事件等待句柄本地事件也可以表示命名系统事件等待句柄命名事件或系统事件对所有进程可见。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 还是手动重置。 枚举值 1 2 3 4 5 public enum EventResetMode { AutoReset 0, ManualReset 1, } createdNew 在此方法返回时如果创建了本地事件如果 name 为空字符串或指定的命名系统事件则为 true如果指定的命名系统事件已存在则为 false。可以创建多个表示同一系统事件的 EventWaitHandle 对象。 eventSecurity 一个 EventWaitHandleSecurity 对象表示应用于【已命名的系统事件】的访问控制安全性。如果系统事件不存在则使用指定的访问控制安全性创建它。如果该事件存在则忽略指定的访问控制安全性。 ii. OpenExisting中使用的EventWaitHandleRights枚举 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 指定可应用于命名的系统事件对象的访问控制权限。 [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。 1 2 3 4 public sealed class AutoResetEvent : EventWaitHandle { public AutoResetEvent(bool initialState); } 使用方式调用 Set() 向 AutoResetEvent 发信号以释放等待线程。AutoResetEvent 将保持终止状态直到一个正在等待的线程被释放然后自动重置为非终止状态。如果没有任何线程在等待则状态将无限期地保持为终止状态直到一个线程进入就绪队列此时线程会立马被释放继续执行等待句柄也会被设置为非终止状态从而等待下一次Set()。
3) ManualResetEvent类本地事件
ManualResetEvent表示必须手动重置的本地事件。在功能上等效于用EventResetMode.ManualReset 创建的本地 EventWaitHandle。 1 2 3 4 public sealed class ManualResetEvent : EventWaitHandle { public ManualResetEvent(bool initialState); } 使用方式调用 Set()向ManualResetEvent发信号以释放等待线程。ManualResetEvent将一直保持终止状态直到它主动调用 Reset ()方法或直到释放完等待句柄中的所有线程(即所有WaitOne()都获得信号)。
4) Mutex互斥体
Mutex 是同步基元它只向一个线程授予对共享资源的独占访问权。
Mutex的API与EventWaitHandleAPI类似。 1 2 3 4 5 6 7 8 9 10 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信号量
限制可同时访问某一资源或资源池的线程数。
Semaphore的API与EventWaitHandleAPI类似。 1 2 3 4 5 6 7 8 9 10 11 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. 事件等待句柄EventWaitHandle、AutoResetEvent 和 ManualResetEvent以及信号量(Semaphore)没有线程关联。任何线程都可以发送事件等待句柄或信号量的信号。 命名事件
Windows 操作系统允许事件等待句柄具有名称。命名事件是系统范围的事件。即创建命名事件后它对所有进程中的所有线程都是可见的。因此命名事件可用于同步进程的活动以及线程的活动。系统范围的可以用来协调跨进程边界的资源使用。
注意
1) 因为命名事件是系统范围的事件所以可以有多个表示相同命名事件的 EventWaitHandle 对象。每当调用构造函数或 OpenExisting 方法时时都会创建一个新的 EventWaitHandle 对象。重复指定相同名称会创建多个表示相同命名事件的对象。
2) 使用命名事件时要小心。因为它们是系统范围的事件所以使用同一名称的其他进程可能会意外地阻止您的线程。在同一计算机上执行的恶意代码可能以此作为一个切入点来发动拒绝服务攻击。
应使用访问控制安全机制来保护表示命名事件的 EventWaitHandle 对象
a) 最好通过使用可指定 EventWaitHandleSecurity 对象的构造函数来实施保护。
b) 也可以使用 SetAccessControl 方法来应用访问控制安全但这一做法会在事件等待句柄的创建时间和设置保护时间之间留出一段漏洞时间。
使用访问控制安全机制来保护事件可帮助阻止恶意攻击但无法解决意外发生的名称冲突问题。
3) Mutex、Semaphore对象类似EventWaitHandle。AutoResetEvent 和 ManualResetEvent 只能表示本地等待句柄不能表示命名系统事件。 利用特性进行上下文同步和方法同步
1. SynchronizationAttribute(AttributeTargets.Class)
应用SynchronizaitonAttribute的类CLR会自动对这个类实施同步机制。为当前上下文和所有共享同一实例的上下文强制一个同步域(同步域之所以有意义就在于它不能被多个线程所共享。换句话说一个处在同步域中的对象的方法是不能被多个线程同时执行的。这也意味着在任一时刻最多只有一个线程处于同步域中)。
被应用SynchronizationAttribute的类必须是上下文绑定的。换句话说它必须继承于System.ContextBoundObject类。
一般类所建立的对象为上下文灵活对象context-agile它们都由CLR自动管理可存在于任意的上下文当中一般在默认上下文中。而 ContextBoundObject 的子类所建立的对象只能在建立它的对应上下文中正常运行此状态被称为上下文绑定。其他对象想要访问ContextBoundObject 的子类对象时都只能通过代透明理来操作。
示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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锁的吞吐量低一般不使用
除此之外还有另一个SynchronizationAttribute。System.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 语句而不是该属性。
验证示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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在一些集合类比如Queue、ArrayList、HashTable和Stack已经提供了Synchronized ()方法和SyncRoot属性。
1. Synchronized()原理是返回了一个线程安全的对象比如Hashtable.Synchronized(new Hashtable())返回了一个继承自Hashtable类的SyncHashtable对象该对象在冲突操作上进行了lock(SyncRoot属性)从而确保了线程同步。
2. SyncRoot属性提供了一个专门待锁定对象如Hashtable中实现源码 1 2 3 4 5 6 7 8 9 10 11 public virtual object SyncRoot { get { if(this._syncRootnull) { Interlocked.CompareExchange(ref this._syncRoot, new object(), null); } return this._syncRoot; } } 从源码可知SyncRoot实际上就是通过Interlocked返回一个同步的object类型对象。
注意此处的SyncRoot模式并不推荐使用因为至始至终都应使用私有的锁推荐在自己的类中实现私有的SyncRoot模式并使用。 本博文介绍了死锁争用条件线程同步锁带来的问题原子操作volatile\Interlocker\Monitor\WaitHandle\Mutex\EventWaitHandle\AutoResetEvent\ManualResetEvent\SemaphoreSynchronizationAttribute\MethodImplAttribute…… 接下来将介绍.NET4.0新增加的混合线程同步基元篇幅较长所以分为上、下两篇。在下篇将介绍.NET4.0增加的新混合线程同步基元这些新基元在一些场合下为我们提供了更好的性能之所以性能好是因为用户基元模式与内核基元模式的性能差别敬请观看下文。 本节到此结束感谢大家的观赏。赞的话还请多推荐啊 (*^_^*)----预祝各位“元旦快乐” 推荐阅读 《理论与实践中的 C# 内存模型》 参考资料 《CLR via C#(第三版)》 MSDN