『C#基础』多线程笔记「二」线程同步

文章结构:

  1. 锁定
  2. 监视器
  3. 共享资源的同步访问
  4. 同步事件和等待句柄
  5. 多线程使用准则「MSDN」

 

锁定

无论是程序还是数据库,只要是涉及到并发的问题,都难免会有「锁」的概念。

在C#中,使用lock关键字来对某个对象实施加锁的操作。

lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。

lock调用

监视器

当多个线程公用一个对象的时候,应该使用监视器类「System.Threading.Monitor」,而不是锁「lock」。

首先,Monitor的作用与lock关键字类似,都是防止多个线程同时执行代码块。

其次,在对小型的代码块进行操作的时候,显然使用lock关键字更加的简洁。

但是,如果要实现一个很复杂的逻辑,或者要根据线程的远行情况来对当前锁定的对象进行控制的话,显然Monitor就更加的方便了。

而且,就MSDN上的说明,lock关键字的内部实现也是使用的Monitor,只不过其只是调用了「Enter」「Exit」这两个方法,并且加代码块中的代码放于「Try…Finally…」中。

在使用的时候,一定要注意的就是Monitor只是针对引用类型对象的操作,而不是对值类型的操作。

如果使用了值类型,就会引发「从不同步的代码块中调用了对象同步方法」。

这主要是因为:

Monitor 将锁定对象(即引用类型)而非值类型。 在您将值类型传递给 EnterExit 时,它会针对每个调用分别装箱。 由于每个调用都创建一个单独的对象,所以 Enter 从不拦截,并且其旨在保护的代码并未真正同步。 此外,传递给 Exit 的对象不同于传递给 Enter 的对象,所以 Monitor 将引发 SynchronizationLockException,并显示消息“从不同步的代码块中调用了对象同步方法”。下面的示例演示这些问题。

除了使用Monitor与lock以外,还可以使用「Mutex」提供对资源的独占访问。Mutex 类比 Monitor 类使用更多系统资源,但是它可以跨应用程序域边界进行封送处理,可用于多个等待,并且可用于同步不同进程中的线程。「MSDN」

 

名称说明
Enter(Object)在指定对象上获取排他锁。
Enter(Object, Boolean)获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。
Exit释放指定对象上的排他锁。
Pulse通知等待队列中的线程锁定对象状态的更改。
PulseAll通知所有的等待线程对象状态的更改。
TryEnter(Object)尝试获取指定对象的排他锁。
TryEnter(Object, Boolean)尝试获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。
TryEnter(Object, Int32)在指定的毫秒数内尝试获取指定对象上的排他锁。
TryEnter(Object, TimeSpan)在指定的时间量内尝试获取指定对象上的排他锁。
TryEnter(Object, Int32, Boolean)在指定的毫秒数中,尝试获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。
TryEnter(Object, TimeSpan, Boolean)在指定的一段时间内,尝试获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。
Wait(Object)释放对象上的锁并阻止当前线程,直到它重新获取该锁。
Wait(Object, Int32)释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。
Wait(Object, TimeSpan)释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。
Wait(Object, Int32, Boolean)释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。 此方法还指定是否在等待之前退出上下文的同步域(如果处于同步上下文中的话)然后重新获取该同步域。
Wait(Object, TimeSpan, Boolean)释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。 可以在等待之前退出同步上下文的同步域,随后重新获取该域。
「MSDN」中关于「Monitor」的例子

共享资源的同步访问

在多线程环境中保护模块中的全局数据:

  1. 应该程序中所有的线程都可以访问方法中的公用字段。
  2. 要同步对公用字段的访问,可以使用属性替代字段,并使用ReaderWriterLock对象控制访问。
  3. ReaderWriterLock:定义支持单个写线程和多个读线程的锁。
  4. 要注意ReaderWriterLock的ReleaseLockReleaseReaderLockReleaseWriterLock方法。
  5. 「MSDN」建议使用 ReaderWriterLockSlim 而不是 ReaderWriterLock。
  6. 长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。 为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。

同步事件和等待句柄

同步事件:

  1. AutoResetEvent:只要激活线程,它的状态将自动从终止变为非终止
  2. ManualResetEvent:允许它的终止状态激活任意多个线程,只有它的Reset方法被调用的时候才还原到非终止状态。

多线程使用准则「MSDN」:

  • 不要使用 Thread.Abort 终止其他线程。 对另一个线程调用 Abort 无异于引发该线程的异常,也不知道该线程已处理到哪个位置。

  • 不要使用 Thread.SuspendThread.Resume 同步多个线程的活动。 请使用 MutexManualResetEventAutoResetEventMonitor

  • 不要从主程序中控制辅助线程的执行(如使用事件), 而应在设计程序时让辅助线程负责等待任务,执行任务,并在完成时通知程序的其他部分。 如果不阻止辅助线程,请考虑使用线程池线程。 如果阻止辅助线程,Monitor.PulseAll 会很有帮助。

  • 不要将类型用作锁定对象。 例如,避免在 C# 中使用 lock(typeof(X)) 代码,或在 Visual Basic 中使用 SyncLock(GetType(X)) 代码,或将 Monitor.EnterType 对象一起使用。 对于给定类型,每个应用程序域只有一个 System.Type 实例。 如果您锁定的对象的类型是 public,您的代码之外的代码也可锁定它,但会导致死锁。 有关其他信息,请参见可靠性最佳做法

  • 锁定实例时要谨慎,例如,C# 中的 lock(this) 或 Visual Basic 中的 SyncLock(Me)。 如果您的应用程序中不属于该类型的其他代码锁定了该对象,则会发生死锁。

  • 一定要确保已进入监视器的线程始终离开该监视器,即使当线程在监视器中时发生异常也是如此。 C# 的 lock 语句和 Visual Basic 的 SyncLock 语句可自动提供此行为,它们用一个 finally块来确保调用 Monitor.Exit。 如果无法确保调用 Exit,请考虑将您的设计更改为使用 Mutex。 Mutex 在当前拥有它的线程终止后会自动释放。

  • 一定要针对那些需要不同资源的任务使用多线程,避免向单个资源指定多个线程。 例如,任何涉及 I/O 的任务都会从其拥有其自己的线程这一点得到好处,因为此线程在 I/O 操作期间将阻止,从而允许其他线程执行。 用户输入是另一种可从专用线程获益的资源。 在单处理器计算机上,涉及大量计算的任务可与用户输入和涉及 I/O 的任务并存,但多个计算量大的任务将相互竞争。

  • 对于简单的状态更改,请考虑使用 Interlocked 类的方法,而不是 lock 语句(在 Visual Basic 中为 SyncLock)。 lock 语句是一个优秀的通用工具,但是 Interlocked 类为必须是原子性的更新提供了更好的性能。 如果没有争夺,它会在内部执行一个锁定前缀。 在查看代码时,请注意类似于以下示例所示的代码。 在第一个示例中,状态变量是递增的:

 

其他参考:

  1. http://msdn.microsoft.com/zh-cn/library/ms173179.aspx
  2. http://msdn.microsoft.com/zh-cn/library/z8chs7ft.aspx
  3. http://msdn.microsoft.com/zh-cn/library/dd997305.aspx
  4. http://msdn.microsoft.com/zh-cn/magazine/cc163352.aspx    「死锁监控」
  5. http://msdn.microsoft.com/zh-cn/library/3e8s7xdd.aspx           「托管线程」
posted @ 2012-03-26 15:45  莫不逢  阅读(2869)  评论(0编辑  收藏  举报