第三部分 使用线程
基于事件的异步模式
基于事件的异步模式(EAP)提供了一种简单的方法,通过这些方法,类可以提供多线程功能,而使用者无需显式启动或管理线程。它还提供以下功能:
- 合作取消模型
- 工作人员完成时可以安全更新WPF或Windows Forms控件的功能
- 将异常转发到完成事件
EAP只是一种模式,因此这些功能必须由实现者编写。框架中只有少数几个类遵循此模式,最著名的是BackgroundWorker(我们将在后面介绍)以及System.Net中的WebClient。本质上,模式是这样的:一个类提供了一系列内部管理多线程的成员,类似于以下内容(突出显示的部分表示该模式的一部分代码):
// These members are from the WebClient class: publicbyte[] DownloadData (Uriaddress); // Synchronous version publicvoid DownloadDataAsync (Uriaddress); publicvoid DownloadDataAsync (Uriaddress, object userToken); publiceventDownloadDataCompletedEventHandlerDownloadDataCompleted; publicvoid CancelAsync (object userState); // Cancels an operation publicbool IsBusy { get; } // Indicates if still running
* Async方法异步执行:换句话说,它们在另一个线程上开始一个操作,然后立即返回给调用者。操作完成后,将触发* Completed事件-如果WPF或Windows Forms应用程序需要,则自动调用Invoke。此事件返回一个事件参数对象,该对象包含:
- 一个标志,指示操作是否被取消(由使用者调用CancelAsync)
- 一个Error对象,指示引发的异常(如果有)
- 调用Async方法时提供的userToken对象
以下是我们如何使用WebClient的EAP成员下载网页的方法:
var wc = new WebClient(); wc.DownloadStringCompleted += (sender, args) => { if (args.Cancelled) Console.WriteLine ("Canceled"); elseif (args.Error != null) Console.WriteLine ("Exception: " + args.Error.Message); else { Console.WriteLine (args.Result.Length + " chars were downloaded"); // We could update the UI from here... } }; wc.DownloadStringAsync (new Uri ("http://www.linqpad.net")); // Start it
遵循EAP的类可能会提供其他异步方法组。例如:
publicstring DownloadString (Uriaddress); publicvoid DownloadStringAsync (Uriaddress); publicvoid DownloadStringAsync (Uriaddress, object userToken); publiceventDownloadStringCompletedEventHandlerDownloadStringCompleted;
但是,这些将共享相同的CancelAsync和IsBusy成员。因此,一次只能执行一次异步操作。
如果EAP的内部实现遵循APM,则EAP可以节省线程(这在C#4.0的第23章中进行了简要介绍)。
我们将在第5部分中看到任务如何提供类似的功能-包括异常转发,继续,取消令牌以及对同步上下文的支持。这使得实施EAP的吸引力降低了-在BackgroundWorker可以做到的简单情况下除外。
BackgroundWorker
BackgroundWorker是System.ComponentModel命名空间中的一个帮助程序类,用于管理辅助线程。它可以被认为是EAP的通用实现,并提供以下功能:
- 合作取消模型
- 工作人员完成时可以安全更新WPF或Windows Forms控件的功能
- 将异常转发到完成事件
- 报告进度的协议
- IComponent的实现,允许将其放置在Visual Studio的设计器中
BackgroundWorker使用线程池,这意味着您永远不要在BackgroundWorker线程上调用Abort。
Using BackgroundWorker
使用BackgroundWorker
以下是使用BackgroundWorker的最少步骤:
- 实例化BackgroundWorker并处理DoWork事件。
- 调用RunWorkerAsync,可以选择使用对象参数。
然后,它开始运动。传递给RunWorkerAsync的任何参数都将通过事件参数的Argument属性转发到DoWork的事件处理程序。这是一个例子
class Program { static BackgroundWorker _bw = new BackgroundWorker(); static void Main() { _bw.DoWork += bw_DoWork; _bw.RunWorkerAsync ("Message to worker"); Console.ReadLine(); } static void bw_DoWork (object sender, DoWorkEventArgs e) { // This is called on the worker thread Console.WriteLine (e.Argument); // writes "Message to worker" // Perform time-consuming task... } }
BackgroundWorker有一个RunWorkerCompleted事件,该事件在DoWork事件处理程序完成其工作后触发。处理RunWorkerCompleted不是强制性的,但是通常这样做是为了查询DoWork中引发的任何异常。此外,RunWorkerCompleted事件处理程序中的代码无需显式编组即可更新用户界面控件。 DoWork事件处理程序中的代码不能。
要添加对进度报告的支持:
- 将WorkerReportsProgress属性设置为true。
- 从DoWork事件处理程序中定期使用“百分比完成”值以及可选的用户状态对象调用ReportProgress。
- 处理ProgressChanged事件,查询其事件参数的ProgressPercentage属性。
- 就像RunWorkerCompleted一样,ProgressChanged事件处理程序中的代码可以自由与UI控件进行交互。通常,您将在这里更新进度条。
要添加取消支持:
- 将WorkerSupportsCancellation属性设置为true。
- 从DoWork事件处理程序中定期检查CancellationPending属性。如果为true,请将事件参数的Cancel属性设置为true,然后返回。 (如果工作人员认为工作太困难并且无法继续进行,则也可以设置Cancel并退出,而CancellationPending不为true。)
- 调用CancelAsync请求取消。
这是一个实现上述所有功能的示例:
using System; using System.Threading; using System.ComponentModel; class Program { static BackgroundWorker _bw; static void Main() { _bw = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true }; _bw.DoWork += bw_DoWork; _bw.ProgressChanged += bw_ProgressChanged; _bw.RunWorkerCompleted += bw_RunWorkerCompleted; _bw.RunWorkerAsync ("Hello to worker"); Console.WriteLine ("Press Enter in the next 5 seconds to cancel"); Console.ReadLine(); if (_bw.IsBusy) _bw.CancelAsync(); Console.ReadLine(); } static void bw_DoWork (object sender, DoWorkEventArgs e) { for (int i = 0; i <= 100; i += 20) { if (_bw.CancellationPending) { e.Cancel = true; return; } _bw.ReportProgress (i); Thread.Sleep (1000); // Just for the demo... don‘t go sleeping } // for real in pooled threads! e.Result = 123; // This gets passed to RunWorkerCompleted } static void bw_RunWorkerCompleted (object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) Console.WriteLine ("You canceled!"); else if (e.Error != null) Console.WriteLine ("Worker exception: " + e.Error.ToString()); else Console.WriteLine ("Complete: " + e.Result); // from DoWork } static void bw_ProgressChanged (object sender, ProgressChangedEventArgs e) { Console.WriteLine ("Reached " + e.ProgressPercentage + "%"); } }
Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%
Reached 60%
Reached 80%
Reached 100%
Complete: 123
Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%
You canceled!
子类BackgroundWorker
在仅需要提供一个异步执行方法的情况下,对BackgroundWorker进行子类化是实现EAP的一种简便方法。
BackgroundWorker没有密封,提供了虚拟的OnDoWork方法,建议使用另一种模式。在编写可能长时间运行的方法时,您可以编写一个附加版本,以返回子类BackgroundWorker,该子类已预先配置为同时执行作业。然后,使用者只需要处理RunWorkerCompleted和ProgressChanged事件。例如,假设我们编写了一个耗时的方法,称为GetFinancialTotals:
我们可以将其重构如下:
Public class Client { public FinancialWorker GetFinancialTotalsBackground (int foo, int bar) { return new FinancialWorker (foo, bar); } } public class FinancialWorker : BackgroundWorker { public Dictionary <string,int> Result; // You can add typed fields. public readonly int Foo, Bar; public FinancialWorker() { WorkerReportsProgress = true; WorkerSupportsCancellation = true; } public FinancialWorker (int foo, int bar) : this() { this.Foo = foo; this.Bar = bar; } protected override void OnDoWork (DoWorkEventArgs e) { ReportProgress (0, "Working hard on this report..."); // Initialize financial report data // ... while (!<finishedreport>) { if (CancellationPending) { e.Cancel = true; return; } // Perform another calculation step ... // ... ReportProgress (percentCompleteCalc, "Getting there..."); } ReportProgress (100, "Done!"); e.Result = Result = <completed reportdata>; } }
调用GetFinancialTotalsBackground的人都会得到FinancialWorker:一个包装程序,用于以实际可用性管理后台操作。它可以报告进度,可以取消,对WPF和Windows Forms应用程序友好,并且可以很好地处理异常。
中断和中止
如果从未满足解除阻塞条件且未指定超时,则所有阻塞方法(例如Sleep,Join,EndInvoke和Wait)将永远阻塞。有时,过早释放阻塞的线程可能很有用;例如,在结束应用程序时。有两种方法可以做到这一点:
- 线程中断
- 线程中止
Abort方法还能够结束一个非阻塞线程-可能陷入无限循环。在特定情况下,中止有时会很有用;几乎不需要中断。
中断和中止会造成很大的麻烦:这恰恰是因为在解决一系列问题时,它们似乎是显而易见的选择,因此值得一试。
打断
在阻塞的线程上调用Interrupt会强制释放它,并抛出ThreadInterruptedException,如下所示:
static void Main() { Thread t = new Thread (delegate() { try { Thread.Sleep (Timeout.Infinite); } catch (ThreadInterruptedException) { Console.Write ("Forcibly "); } Console.WriteLine ("Woken!"); }); t.Start(); t.Interrupt(); } >> Forcibly Woken!
除非未处理ThreadInterruptedException,否则中断线程不会导致线程结束。
如果在未阻塞的线程上调用了Interrupt,则该线程将继续执行直到下一个阻塞,此时将抛出ThreadInterruptedException。这样可以避免进行以下测试:
if((worker.ThreadState&ThreadState.WaitSleepJoin)> 0) worker.Interrupt();
由于if语句和worker.Interrupt之间可能会抢占,因此这不是线程安全的。
但是,任意中断线程是危险的,因为调用堆栈中的任何框架或第三方方法都可能意外接收到中断,而不是预期的代码。线程只需要在一个简单的锁或同步资源上短暂阻塞即可,并且所有未决的中断都会触发。如果该方法的设计目的不是要中断(在finally块中带有适当的清除代码),则可以处于无法使用的状态或资源未完全释放。
此外,不需要中断:如果您正在编写阻塞代码,则可以通过信令结构(或Framework 4.0的取消令牌)更安全地获得相同的结果。而且,如果您想“解除阻止”他人的代码,则Abort几乎总是更有用。
中止
阻塞的线程也可以通过其Abort方法强制释放。这具有类似于调用Interrupt的效果,除了抛出ThreadAbortException而不是ThreadInterruptedException之外。此外,除非在catch块内调用Thread.ResetAbort,否则异常将在catch块的末尾重新抛出(以试图永久终止线程)。在此期间,线程的ThreadState为AbortRequested。
未处理的ThreadAbortException是不会导致应用程序关闭的仅有的两种异常类型之一(另一种是AppDomainUnloadException)。
中断和中止之间的最大区别是在未被阻塞的线程上调用该中断时会发生什么。而Interrupt会等到下一个线程阻塞之后再执行任何操作,而Abort在执行线程的位置向其抛出一个异常(非托管代码除外)。这是一个问题,因为.NET Framework代码可能会中止-不中止安全的代码。例如,如果在构造FileStream时发生中止,则非托管文件句柄可能会保持打开状态,直到应用程序域结束。这排除了在几乎所有无关紧要的情况下使用中止的可能性。
有关Abort不安全的原因的更多详细信息,请参阅第4部分中的中止线程。
但是,有两种情况可以安全地使用Abort。一种是如果您愿意在中止线程后拆除其应用程序域。编写单元测试框架就是一个很好的例子。可以安全调用Abort的另一种情况是在您自己的线程上(因为您确切知道自己的位置)。中止自己的线程会引发“无法吞咽”的异常:在每个catch块之后都会抛出该异常。当您调用重定向时,ASP.NET正是这样做的。
当您取消失控查询时,LINQPad会中止线程。中止后,它会拆除并重新创建查询的应用程序域,以避免可能发生的潜在污染状态。
安全取消
如上一节所述,在大多数情况下,在线程上调用Abort是危险的。然后,替代方法是实施协作模式,由此工作人员会定期检查指示其是否应中止的标志(例如,在BackgroundWorker中)。要取消,煽动者只需设置标志,然后等待工作人员遵守即可。这个BackgroundWorker帮助程序类实现了这种基于标志的取消模式,您可以轻松地自己实现一个。
明显的缺点是必须显式编写worker方法以支持取消。但是,这是少数几种安全取消模式之一。为了说明这种模式,我们首先要编写一个类来封装取消标志:
class RulyCanceler { object _cancelLocker = new object(); bool _cancelRequest; public bool IsCancellationRequested { get { lock (_cancelLocker) return _cancelRequest; } } public void Cancel() { lock (_cancelLocker) _cancelRequest = true; } public void ThrowIfCancellationRequested() { if (IsCancellationRequested) throw new OperationCanceledException(); } }
OperationCanceledException是仅用于此目的的Framework类型。但是,任何异常类都将同样有效。
我们可以如下使用它:
class Test { static void Main() { var canceler = new RulyCanceler(); new Thread (() => { try { Work (canceler); } catch (OperationCanceledException) { Console.WriteLine ("Canceled!"); } }).Start(); Thread.Sleep (1000); canceler.Cancel(); // Safely cancel worker. } static void Work (RulyCanceler c) { while (true) { c.ThrowIfCancellationRequested(); // ... try { OtherMethod (c); } finally { /* any required cleanup */ } } } static void OtherMethod (RulyCanceler c) { // Do stuff... c.ThrowIfCancellationRequested(); } }
们可以通过消除RulyCanceler类并将静态布尔字段_cancelRequest添加到Test类来简化示例。但是,这样做意味着如果多个线程一次调用Work,将_cancelRequest设置为true将取消所有工作程序。因此,我们的RulyCanceler类是有用的抽象。唯一的不足之处是,当我们查看Work方法的签名时,其意图尚不清楚:
static void Work(RulyCanceler c)
Work方法本身是否打算在RulyCanceler对象上调用Cancel?在这种情况下,答案是否定的,因此如果可以在类型系统中强制实施,那就更好了。 Framework 4.0为此目的提供了取消令牌。
取消令牌
Framework 4.0提供了两种形式化我们刚刚演示的协作取消模式:CancellationTokenSource和CancellationToken。两种类型协同工作:
- CancellationTokenSource定义了Cancel方法。
- CancellationToken定义了IsCancellationRequested属性和ThrowIfCancellationRequested方法。
在前面的示例中,这些加在一起就构成了RulyCanceler类的更复杂的版本。但是因为类型是分开的,所以您可以将取消功能与检查取消标志的功能区分开。
要使用这些类型,请首先实例化CancellationTokenSource对象:
var cancelSource = new CancellationTokenSource();
然后,将其Token属性传递到您要支持取消的方法中:
new Thread (() => Work (cancelSource.Token)).Start(); Here’s how Work would be defined: void Work (CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); ... }
要取消时,只需在cancelSource上调用Cancel。
CancellationToken实际上是一个结构,尽管您可以将其视为类。隐式复制时,副本的行为相同,并引用原始的CancellationTokenSource。
CancellationToken结构提供了两个其他有用的成员。第一个是WaitHandle,它返回一个等待句柄,该等待句柄在令牌被取消时发出信号。第二个是Register,它使您可以注册将在取消时触发的回调委托。
取消令牌在.NET Framework本身中使用,尤其是在以下类中:
ManualResetEventSlim和SemaphoreSlim
- 倒计时事件
- 屏障
- 阻止收集
- PLINQ和任务并行库
这些类中的大多数取消标记的使用都在其Wait方法中。例如,如果您在ManualResetEventSlim上等待并指定取消标记,则另一个线程可以取消其等待。这比在阻塞的线程上调用Interrupt更为整洁和安全。
延迟初始化
线程中的一个常见问题是如何以线程安全的方式延迟初始化共享字段。当您构造的字段类型很昂贵时,就会产生这种需求:
class Foo { public readonly Expensive Expensive = new Expensive(); ... } class Expensive { /* Suppose this is expensive to construct */ }
这段代码的问题在于,实例化Foo会导致实例化Expensive的性能成本-无论是否曾经访问Expensive字段。显而易见的答案是按需构造实例:
classFoo
{
Expensive _expensive;
publicExpensive Expensive // Lazily instantiate Expensive
{
get
{
if (_expensive == null) _expensive = new Expensive();
return _expensive;
}
}
...
}
随之而来的问题是,这个线程安全吗?除了我们要在没有内存屏障的锁外部访问_expensive之外,还要考虑如果两个线程同时访问此属性会发生什么情况。它们都可以满足if语句的谓词,并且每个线程最终都具有不同的Expensive实例。因为这可能会导致细微的错误,所以我们通常会说此代码不是线程安全的。
该问题的解决方案是锁定检查和初始化对象:
class Foo { Expensive _expensive; public Expensive Expensive // Lazily instantiate Expensive { get { if (_expensive == null) _expensive = new Expensive(); return _expensive; } } ... }
Lazy<T>
Framework 4.0提供了一个称为Lazy <T>的新类,以帮助进行延迟初始化。如果使用参数true实例化,它将实现刚刚描述的线程安全初始化模式。
Lazy <T>实际上实现了该模式的一种稍微有效的版本,称为双重检查锁定。如果对象已经初始化,则双重检查锁定将执行其他易失性读取,以避免获得锁定的成本。
要使用Lazy <T>,请使用值工厂委托实例化该类,该工厂告诉其如何初始化新值以及参数true。然后通过Value属性访问其值:
Lazy<Expensive> _expensive = new Lazy<Expensive> (() => new Expensive(), true); publicExpensiveExpensive { get { return _expensive.Value; } }
如果将false传递给Lazy <T>的构造函数,它将实现我们在本节开始时描述的线程不安全的惰性初始化模式-当您想在单线程上下文中使用Lazy <T>时,这很有意义。
LazyInitializer(惰性启动器)
LazyInitializer是一个静态类,其工作方式与Lazy <T>完全相同,除了:
- 它的功能通过静态方法公开,该方法直接在您自己类型的字段上运行。这样可以避免某种程度的间接访问,从而在需要进行极端优化的情况下提高了性能。
- 它提供了另一种初始化模式,该模式具有多个线程进行初始化。
要使用LazyInitializer,请在访问该字段之前调用确保初始化,传递对该字段和工厂委托的引用:
Expensive _expensive; public Expensive Expensive { get // Implement double-checked locking { LazyInitializer.EnsureInitialized (ref _expensive, () => new Expensive()); return _expensive; } }
您也可以传入另一个参数来请求竞争线程竞相初始化。这听起来与我们最初的线程不安全示例相似,不同之处在于,第一个完成的线程始终会获胜,因此最终只能获得一个实例。这种技术的优点是(在多核上)比经过双重检查的锁定甚至更快-因为它可以完全不用锁来实现。这是您很少需要的一种极端优化,并且付出了一定的代价:
- 当有更多线程竞相初始化而不是拥有内核时,速度会变慢。
- 执行冗余初始化可能会浪费CPU资源。
- 初始化逻辑必须是线程安全的(在这种情况下,例如,如果Expensive的构造函数将其写入静态字段,那么它将是线程不安全的)。
- 如果初始化程序实例化了需要处置的对象,那么没有其他逻辑就不会处置“浪费”的对象。
供参考,以下是双重检查锁定的实现方式:
volatile Expensive _expensive; public Expensive Expensive { get { if (_expensive == null) // First check (outside lock) lock (_expenseLock) if (_expensive == null) // Second check (inside lock) _expensive = new Expensive(); return _expensive; } }
这就是初始化竞态模式的实现方式:
volatile Expensive _expensive; public Expensive Expensive { get { if (_expensive == null) { var instance = new Expensive(); Interlocked.CompareExchange (ref _expensive, instance, null); } return _expensive; } }
线程本地存储
本文的大部分内容都集中在同步结构以及线程同时访问同一数据所引起的问题上。但是,有时您希望保持数据隔离,以确保每个线程都有一个单独的副本。局部变量恰好可以实现这一点,但是它们仅对瞬态数据有用。
解决方案是线程本地存储。您可能很难想到一个需求:您想要隔离到线程的数据在本质上往往是瞬态的。它的主要应用程序用于存储“带外”数据,该数据支持执行路径的基础结构,例如消息传递,事务和安全令牌。在方法参数中传递此类数据非常笨拙,并且会疏远除您自己的方法之外的所有方法。将此类信息存储在普通的静态字段中意味着在所有线程之间共享它。
线程本地存储在优化并行代码中也很有用。它允许每个线程以独占方式访问其自己的线程不安全对象版本,而无需锁-且无需在方法调用之间重新构造该对象。
有三种方法可以实现线程本地存储。
[ThreadStatic]
线程本地存储的最简单方法是使用ThreadStatic属性标记一个静态字段:
[ThreadStatic] static int _x;
然后,每个线程都会看到_x的单独副本。
不幸的是,[ThreadStatic]不适用于实例字段(它什么也不做)。它也不能与字段初始化程序一起很好地使用,它们只能在静态构造函数执行时在正在运行的线程上执行一次。如果需要使用实例字段(或以非默认值开头),则ThreadLocal <T>提供了更好的选择。
ThreadLocal <T>
ThreadLocal <T>是Framework 4.0的新增功能。它为静态字段和实例字段提供线程本地存储-并允许您指定默认值。
以下是创建每个线程的默认值为3的ThreadLocal <int>的方法:
static ThreadLocal<int> _x = new ThreadLocal<int> (() => 3);
然后,您可以使用_x的Value属性获取或设置其线程本地值。使用ThreadLocal的好处是懒惰地评估值:工厂函数在第一次调用时评估(对于每个线程)。
ThreadLocal <T>和实例字段
ThreadLocal <T>对于实例字段和捕获的局部变量也很有用。例如,考虑在多线程环境中生成随机数的问题。 Random类不是线程安全的,因此我们必须使用Random(限制并发性)锁定或为每个线程生成一个单独的Random对象。 ThreadLocal <T>使后者变得容易:
var localRandom = new ThreadLocal <Random>(()=> new Random()); Console.WriteLine(localRandom.Value.Next());
不过,我们用于创建Random对象的工厂函数有点简单,因为Random的无参数构造函数依赖于系统时钟来获取随机数种子。对于彼此之间在大约10毫秒内创建的两个随机对象,这可能是相同的。这是一种解决方法:
var localRandom = new ThreadLocal<Random> ( () => new Random (Guid.NewGuid().GetHashCode()) );
我们将在第5部分中使用它(请参阅“ PLINQ”中的并行拼写检查示例)。
GetData and SetData
第三种方法是在Thread类中使用两种方法:GetData和SetData。这些将数据存储在特定于线程的“插槽”中。 Thread.GetData从线程的隔离数据存储中读取; Thread.SetData写入它。这两种方法都需要一个LocalDataStoreSlot对象来标识插槽。可以在所有线程上使用相同的插槽,但它们仍将获得单独的值。这是一个例子:
class Test { // The same LocalDataStoreSlot object can be used across all threads. LocalDataStoreSlot _secSlot = Thread.GetNamedDataSlot ("securityLevel"); // This property has a separate value on each thread. int SecurityLevel { get { object data = Thread.GetData (_secSlot); return data == null ? 0 : (int) data; // null == uninitialized } set { Thread.SetData (_secSlot, value); } } ...
在这种情况下,我们称为Thread.GetNamedDataSlot,它创建了一个命名插槽-允许在应用程序中共享该插槽。另外,您可以通过调用Thread.AllocateDataSlot获得的未命名的插槽来自己控制插槽的作用域:
class Test { LocalDataStoreSlot _secSlot = Thread.AllocateDataSlot();
Thread.FreeNamedDataSlot将在所有线程中释放一个命名的数据槽,但是仅在对该LocalDataStoreSlot的所有引用都超出范围并被垃圾回收之后。这样可确保只要线程在需要时仍引用适当的LocalDataStoreSlot对象,就不会从其脚下拉出数据插槽。
计时器
如果您需要定期执行某些方法,最简单的方法是使用计时器。与以下技术相比,计时器在使用内存和资源方面既方便又高效:
new Thread (delegate() { while (enabled) { DoSomeAction(); Thread.Sleep (TimeSpan.FromHours (24)); } }).Start();
这不仅会永久占用线程资源,而且无需额外的编码,DoSomeAction将在每天的稍后时间发生。计时器解决了这些问题。
.NET Framework提供了四个计时器。其中两个是通用多线程计时器:
System.Threading.Timer
System.Timers.Timer
另外两个是专用的单线程计时器:
- System.Windows.Forms.Timer(Windows窗体计时器)
- System.Windows.Threading.DispatcherTimer(WPF计时器)
多线程计时器更加强大,准确和灵活。单线程计时器在运行更新Windows Forms控件或WPF元素的简单任务时更安全,更方便。
多线程计时器
System.Threading.Timer是最简单的多线程计时器:它只有一个构造函数和两个方法(极简主义者和本书作者都非常高兴!)。在下面的示例中,计时器调用Tick方法,该方法在经过五秒钟后会写“ tick ...”,然后每隔一秒钟写一次,直到用户按下Enter键为止:
using System; using System.Threading; class Program { static void Main() { // First interval = 5000ms; subsequent intervals = 1000ms Timer tmr = new Timer (Tick, "tick...", 5000, 1000); Console.ReadLine(); tmr.Dispose(); // This both stops the timer and cleans up. } static void Tick (object data) { // This runs on a pooled thread Console.WriteLine (data); // Writes "tick..." } }
您可以稍后通过调用其Change方法来更改计时器的间隔。如果您希望计时器仅触发一次,请在构造函数的最后一个参数中指定Timeout.Infinite。
.NET Framework在System.Timers命名空间中提供了另一个同名的计时器类。这只是包装System.Threading.Timer,在使用相同的基础引擎时提供了更多的便利。以下是其新增功能的摘要:
- 组件实现,可以将其放置在Visual Studio的设计器中
- 一个Interval属性而不是Change方法
- 一个Elapsedevent而不是回调委托
- 用于启动和停止计时器的Enabled属性(其默认值为false)
- 如果您对Enabled感到困惑,请使用Start和Stop方法
- 一个AutoReset标志,用于指示重复发生的事件(默认值为true)
- 具有Invoke和BeginInvoke方法的SynchronizingObject属性,用于安全地调用WPF元素和Windows Forms控件上的方法
这是一个例子:
Uses an event instead of a delegate tmr.Start(); // Start the timer Console.ReadLine(); tmr.Stop(); // Stop the timer Console.ReadLine(); tmr.Start(); // Restart the timer Console.ReadLine(); tmr.Dispose(); // Permanently stop the timer } static void tmr_Elapsed (object sender, EventArgs e) { Console.WriteLine ("Tick"); } }
多线程计时器使用线程池来允许几个线程为多个计时器提供服务。这意味着每次调用回调方法或Elapsed事件都可能在不同的线程上触发。此外,无论过去的Elapsed是否已完成执行,Elapsed总是会按时触发(大约)。因此,回调或事件处理程序必须是线程安全的。
多线程计时器的精度取决于操作系统,通常在10–20 ms范围内。如果需要更高的精度,则可以使用本机互操作并调用Windows多媒体计时器。精度低至1毫秒,并且在winmm.dll中定义。首先调用timeBeginPeriod通知操作系统您需要较高的计时精度,然后调用timeSetEvent启动多媒体计时器。完成后,调用timeKillEvent停止计时器,并调用timeEndPeriod通知操作系统,您不再需要较高的计时精度。通过搜索关键字dllimport winmm.dll timesetevent,可以在Internet上找到使用多媒体计时器的完整示例。
单线程计时器
.NET Framework提供的计时器旨在消除WPF和Windows Forms应用程序的线程安全问题:
- System.Windows.Threading.DispatcherTimer(WPF)
- System.Windows.Forms.Timer(Windows窗体)
单线程计时器并非设计为在各自的环境之外工作。例如,如果您在Windows服务应用程序中使用Windows Forms计时器,则不会触发Timer事件!
它们所公开的成员(Interval,Tick,Start和Stop)都类似于System.Timers.Timer,并且以类似的方式使用。但是,它们在内部工作方式上有所不同。 WPF和Windows Forms计时器不是使用线程池生成计时器事件,而是依靠其基础用户界面模型的消息泵送机制。这意味着Tick事件始终在最初创建计时器的同一线程上触发-在正常应用程序中,它是用于管理所有用户界面元素和控件的同一线程。这有很多好处:
- 您可以忘记线程安全性。
- 在之前的刻度线完成处理之前,不会触发新的刻度线。
- 您可以直接从Tick事件处理代码更新用户界面元素和控件,而无需调用Control.Invoke或Dispatcher.Invoke。
在您意识到使用这些计时器的程序不是真正的多线程之前,这听起来实在是太好了—没有并行执行。一个线程服务所有计时器以及处理UI事件。这给我们带来了单线程计时器的缺点:
- 除非Tick事件处理程序快速执行,否则用户界面将无响应。
这使得WPF和Windows Forms计时器仅适用于小型作业,通常是那些涉及更新用户界面某些方面(例如时钟或倒数显示)的作业。否则,您需要一个多线程计时器。
在精度方面,单线程计时器类似于多线程计时器(数十毫秒),尽管它们通常精度较低,因为在处理其他用户界面请求(或其他计时器事件)时,它们可能会延迟。
Interrupt and Abort
原文地址:https://www.cnblogs.com/wxs121/p/12545354.html