C#学习笔记14
1.在多个线程的同步数据中,避免使用this、typeof(type)、string进行同步锁,使用这3个容易造成死锁。
2.使用Interlocked类:我们一般使用的互斥锁定模式(同步数据)为Lock关键字(即Monitor类),这个同步属于代价非常高的一种操作。除了使用Monitor之外,还有一个备选方案,它通常直接由处理器支持,而且面向特定的同步模式。Interlocked类中包含一些常用方法,如CompareExchange、Decrement、Increment、Exchange。这些都是针对单个的值(对象)进行同步数据处理。
3.多个线程时的事件通知:可以查看Utility.EventInThread()代码清单。
4.同步设计的最佳实践:
(1)避免死锁;如两个线程都等待对方锁定的资源释放,线程A锁定sync1资源,线程B锁定sync2资源,线程A请求锁定sync2资源,线程B请求锁定sync1资源,此时便出现死锁。死锁的发生必须满足4个基本条件。互斥、占有并等待、不可抢先、循环等待条件。
(2)何时提供同步;通常针对静态数据进行同步,并有公共方法来修改数据,方法内部应处理好同步问题。
(3)避免不必要的锁定。
5.更多同步类型:System.Threading.Mutex类在概念上与Monitor类一致,只是其是为支持进程之间的同步。如同步对文件或其他跨进程资源的访问,限制程序只能运行一个实例。如Utility.UseMutex()代码清单。Mutex类派生自WaitHandle,可以自动获取多个锁(这是Monitor类所不支持的)。
6.WaitHandle类:多个同步类是继承于它,如Mutex、EventWaitHandle、Semaphore,WaitHandle类的关键方法为WaitOne(),它有多个重载版本,这些方法会阻塞当前线程,直到WaitHandle实例收到信号或被设置(调用Set())。
7.重置事件类:重置事件与C#中委托以及事件没有任何关系,用于多线程的控制,重置事件用于强迫代码等候另一个线程的执行,直到获得事件已发生的通知。重置事件类有ManualResetEvent、ManualResetEventSlim(.net4.0新增,针对前者进行优化)、AutoResetEvent(主要使用前面2个类型),它们提供的关键方法为Set()与Wait()。调用Wait()方法会阻塞一个线程的执行,直到一个不同的线程调用Set(),或者设定的等待时间结束,才会继续运行。可查看Utility.UseManualResetEvent()代码清单。
8.并发集合类:.net4.0新增了一些类是并发集合类,这些类专门设计用来包含内建的同步代码,使它们能支持多个线程访问而不必关心竞态条件。如BlockingCollection<T>、ConcurrentBag<T>、ConcurrentDictionary<K,V>、ConcurrentQueue<T>、ConcurrentStack<T>,利用并发集合,可以实现的一个常见的模式是生产者和消费者的线程安全的访问。
9.线程本地存储:同步的一个替代方案是隔离,而实现隔离的一个办法是使用线程本地存储,利用线程本地存储,线程就有了专属的变量实例。线程本地存储实现有2中方式,分别为ThreadLocal<T>和ThreadStaticAttribute类,其中ThreadLocal<T>类是.net4.0新增的。可查看LocalVarThread类的代码清单。
10.计时器:有三种计时器分别为System.Windows.Forms.Timer、System.Timers.Timer、System.Threading.Timer,Forms.Timer用于用户界面编程,能够安全的访问用户界面上的窗体与控件,Timers.Timer是Threading.Timer的包装器,是对其功能的抽象(System.Threading.Timer类型轻量一些)。
功能描述 |
System.Timers.Timer |
System.Threading.Timer |
System.Window.Forms.Timer |
支持在计时器实例化之后添加和删除侦听器 |
是 |
否 |
是 |
支持用户界面线程上的回调 |
是 |
否 |
是 |
从自线程池获取的线程进行回调 |
是 |
是 |
否 |
支持在Windows窗体设计器中拖放 |
是 |
否 |
是 |
适合在一个多线程服务器环境中运行 |
是 |
是 |
否 |
支持将任意状态从计时器初始化传递至回调 |
否 |
是 |
否 |
实现Idisposable |
是 |
是 |
是 |
支持开关式回调和定期重复回调 |
是 |
是 |
是 |
可穿越应用程序域的边界访问 |
是 |
是 |
是 |
支持Icomponent,可容纳在一个Icontainer中 |
是 |
否 |
是 |
11.异步编程模型(Async Program Model,APM):异步编程是多线程的一种方式,APM的关键在于成对使用BeginX和EndX方法(X一般对应同步版本的方法名),而且这些方法具有完善的签名。BeginX返回一个System.IAsyncResult对象,可通过它访问异步调用的状态,以便等待完成或轮询完成。然而EndX方法获取这个返回的对象作为输入参数。这样才真正将两个方法配成一对,让我们可以清晰地判断哪个BeginX方法调用和哪个EndX方法调用配对。APM的本质要求所有BeginX调用都必须有一个(而且只能有一个)EndX调用。因此,不可能发生两个EndX调用接受同一个IAsyncResult实例的情况。我们还可以使用IAsyncResult的WaitHandle判断异步方法何时结束,IAsyncResult的WaitHandle是在回调执行之前进行通知完成。
EndX方法具有4个方面的用途。
首先,调用EndX会阻塞线程继续执行,直到请求的工作成功完成(或者发生错误并引发异常)。
其次,如果方法X要返回数据,这个数据可从EndX方法调用中访问。
再次,如果执行请求的工作时发生异常,可在调用EndX时重新引发这个异常,确保异常会被调用代码发现——好像它是在一次同步调用上发生的那样。
最后,如果任何资源需要在调用X后清理,EndX将负责清理这些资源。
BeginX方法有两个额外的参数,在同步版本的方法中是没有的,一个是回调参数,是方法结束时要调用的一个System.AsyncCallback委托,另一个是object类型的状态参数(State)。在使用回调时,可以把EndX放在其内部执行。
12.使用TPL(任务并行库)调用APM:虽然TPL大幅简化了长时间运行方法的异步调用,但通常最好是使用API提供的APM方法,而不是针对同步版本编写TPL。这是因为API开发人员知道如何编写最高效率的线程处理代码,知道同步哪些数据以及要使用什么同步类型。TPL包含FromAsync的一组重载版本,用于调用APM。
13.异步委托调用:有一个派生的APM模式,称为异步委托调用,它在所有委托数据类型上使用了特殊的、由C#编译器生成的代码。例如,给定Func<string,int>的一个委托实例,可以在这个实例上使用以下APM方法对。
System.IAsyncResult BeginInvoke(string arg,AsyncCallback callback,object obj)
Int EndInvode(IasyncResult result)
结果是可以使用C#编译器生成的方法来同步地调用任何委托(进而调用任何方法)。遗憾的是,异步委托调用模式使用的基础技术是一种不再继续开发的分布式编程技术,称为远程处理。虽然微软仍然支持异步委托调用,而且在可以预见的将来,也不会放弃对它的支持,但和其他技术相比,它的性能显得比较一般。其他技术包括Thread、ThreadPool和TPL等。因此,在开发新项目时,开发人员应尽量选用其他技术,而不要使用异步委托调用API。在TPL之前,异步委托调用模式比其他替代方案容易得多,所以假如一个API没有提供显式的异步调用模式,一般都会选用它。然而,在TPL问世之后,除非是为了与.Net3.5和早期框架版本兼容,否则异步委托调用越来越没有什么用了。
14.基于事件的异步模式(EAP):比APM更高级的一种编程模式是基于事件的异步模式。和APM一样,API开发人员为长时间运行的方法实现了EAP。其中Background Worker模式,它是EAP的一个特定的实现。
15.Background Worker模式:建立Background Worker模式的过程如下。
(1)为BackgroundWorker.DoWork事件注册长时间运行的方法。
(2)为了接受进展或状态通知,要为BackgroundWorker.ProgressChanged挂接一个侦听器,并将BackgroundWorker.WorkerReportsProgress设为true。
(3)为BackgroundWorker.RunWorkerCompleted事件注册一个方法。
(4)为WorkerSupportsCancellation属性赋值以支持取消一个操作。将true值赋给该属性以后,对BackgroundWorker.CancelAsync的调用就会设置DoWorkEventArgs.CancellationPending标志。
(5)在DoWork提供的方法内,检查DoWorkEventArgs.CancellationPending属性值,并在它为true时退出方法。
(6)一切都设置好之后,调用BackgroundWorker.RunWorkerAsync(),并提供要传给指定DoWork()方法的一个状态参数来开始工作。
分解成以上小步骤以后,Background Worker模式就显得容易理解。另外,由于它本质上是一种EAP,所以提供了对进度通知的显式支持。后台的工作者(worker)线程异步执行的时候,假如发生一个未处理的异常,RunWorkerCompleted委托的RunWorkerCompletedEventArgs.Error属性就会设置成Exception实例。因此,我们通过在RunWorkerCompleted回调内检查Error属性来提供异常处理机制。
16.Windows UI编程:使用System.Windows.Forms和System.Windows命名空间来进行用户界面开发时,也必须注意线程处理问题。Microsoft Windows系列操作系统使用的是一个单线程的、基于消息处理的用户界面。这意味着,每次只能有一个线程访问用户界面,与轮换线程的任何交互都应该通过Windows消息泵来封送。
17.Windows窗体:进行Windows窗体编程时,为了检查是否允许从一个线程中发出UI调用,需要调用一个组件的InvokeRequired属性,判断是否需要进行封送处理。如果InvokeRequired返回true,表明需要封送,并可通过一个Invoke()调用来实现。尽管Invoke()在内部无论如何都会检查InvokeRequired,但更有效率的做法是提前显示地检查这个属性。由于封送到另一个线程可能是相当慢的一个过程,所以可以通过BeginInvoke()和EndInvoke()来进行异步调用。Invoke()、BeginInvoke()、EndInvoke()和InvokeRequired构成了System.ComponentModel.ISynchronizeInvoke接口的成员。该接口已由System.Windows.Forms.Control实现,所有Windows窗体控件都是从这个Control类派生。
18.Windows Presentation Foundation(WPF):为了在WPF平台上实现相同的封送检查,需要采取稍有不同的一种方式。WPF包含一个名为Current的静态成员属性,它的类型是DispatcherObject,由System.Windows.Application类提供。在调度器(dispatcher)对象上调用CheckAccess(),作用等同于在Windows窗体中的控件上调用InvokeRequired,然后再使用Application.Current.Dispatcher.Invoke()方法封送。
19.说明:除了TPL提供的模式,还有这么多额外的模式可供选用,这造成许多人不知道应该如何选择。一般情况下,最好是选择由API提供的模式(比如APM或EAP),最后选择TPL模式。
public class Utility { private static ManualResetEventSlim firstEvent, secendEvent; private static object _data; public static void Initialize(object newValue) { Interlocked.CompareExchange(ref _data, newValue, null); } public static void EventInThread() { //不是线程安全,在检查委托对象与调用委托之间,存在其他线程对OnTemparatureChange进行赋值操作,可能会被设置为null。 /*if (OnTemparatureChange != null) { //调用订阅者 OnTemparatureChange(this, new TemparatureEventArgs()); }*/ //线程安全操作,创建一个委托变量副本,检查副本的null,再触发副本。这样即使OnTemparatureChange委托变量在其他线程中被null化,也不影响。 /*TemparatureChangedHandle localChanged = OnTemparatureChange; if (localChanged != null) { //调用订阅者 localChanged(this, new TemparatureEventArgs()); }*/ } public static void UseMutex() { bool firstApplicationInstance; string mutexName = Assembly.GetEntryAssembly().FullName; using (Mutex mutex = new Mutex(false, mutexName, out firstApplicationInstance)) { if (!firstApplicationInstance) { Console.WriteLine("This Application is already running."); } else { Console.WriteLine("Enter to shutdown."); Console.ReadLine(); } } } public static void UseManualResetEvent() { using (firstEvent = new ManualResetEventSlim()) using (secendEvent = new ManualResetEventSlim()) { Console.WriteLine("App start"); Console.WriteLine("start task"); Task task = Task.Factory.StartNew(() => { Console.WriteLine("DoWork start"); Thread.Sleep(1000); firstEvent.Set(); secendEvent.Wait(); Console.WriteLine("DoWork end"); }); firstEvent.Wait(); Console.WriteLine("Thread executing"); secendEvent.Set(); task.Wait(); Console.WriteLine("Thread completed"); Console.WriteLine("App shutdown"); } } } public class LocalVarThread { public static ThreadLocal<double> _count = new ThreadLocal<double>(() => 0.01134); public static double Count { set { _count.Value = value; } get { return _count.Value; } } public static void DoWork() { Task.Factory.StartNew(Decrement); for (int i = 0; i < short.MaxValue; i++) { Count++; } Console.WriteLine("DoWork Count = {0}", Count); } public static void Decrement() { Count = -Count; for (int i = 0; i < short.MaxValue; i++) { Count--; } Console.WriteLine("Decrement Count = {0}", Count); } }
---------------------以上内容根据《C#本质论 第三版》进行整理