C#学习笔记之线程 - 使用线程

时间:2021-09-08 08:32:10

基于事件的异步模式 (EAP -- The Event-Based Asynchronous Pattern)


EAP提供了一个简单的办法,通过它的类能够提供多线程能力,而不需显式的开启或管理线程。它还提供了以下特性:

  • 取消合作模式
  • 当工作线程完成时,安全更新WPF或WF的能力
  • 转发异常给完成的事件

EAP只是一种模式,因此它的这些特性必须由实现的人来完成。框架中只有少数几个类遵循了这个模式,最明显的是BackgroundWorker和WebClient。这个模式的本质:一个类提供了一系列家族成员内部管理多线程,类似于下面:

  • public byte[] DownloadData(Uri address); // Synchronous version
  • public void DownloadDataAsync(Uri address);
  • public void DownloadDataAsync(Uri address, object userToken);
  • public event DownloadDataCompletedEventHandler DownloadDataCompleted;
  • ...

这些Async函数异步执行:也就是,在一个线程上开始并立即返回到调用者。当操作完成时,*Completed事件被激活--自动调用Invoke如果被WPF或WF程序要求。这些事件返回一个事件参数包含了:

  • 一个标记来指示操作是否被取消(客户调用了CancelAsync)
  • 一个错误对象来指示是否有异常抛出(如果有错误)。
  • 如果调用Async函数时提供了userToken对象,那么还包含了userToken对象。

下面是一个如何使用WebClient的EAP成员来下载一个web页面的例子:

var wc = new WebClient();
wc.DownloadStringCompleted += (sender, args) =>
{
if (args.Cancelled)
Console.WriteLine ("Canceled");
else if (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的类可能提供了额外的异步函数。如:

public string DownloadString (Uri address);
public void DownloadStringAsync (Uri address);
public void DownloadStringAsync (Uri address, object userToken);
public event DownloadStringCompletedEventHandler DownloadStringCompleted;

然而,这些函数共享CancelAsync和IsBusy函数。因此,一次只能执行一个异步操作。

EAP提供了减少了使用线程的可能性,如果它的内部遵循APM(后面讲解)。

Task提供类似的功能,包括异常转发,延续,取消令牌;并且支持同步上下文。这使得自己实现EAP的没有吸引力--除非在简单的情况下,BackgroundWorker能够做到的。

BackgroundWorker


该类是一个帮助类位于System.ComponentModel命名空间中去管理一个工作线程。它被认为是一个EAP的通用的实现,并提供了下面的特性:

  • 取消合作模式
  • 安全更新WPF或WF控件的能力
  • 转发异常到完成事件中
  • 报告进度的协议
  • 实现了IComponent允许它出现在Visual Studio的designer中。

BackgroundWorker使用线程池,所以你从不应该调用Abort。

使用BackgroundWorker

这是使用它的最少步骤:

  1. 实例化BackgroundWorker并且处理DoWork事件
  2. 调用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是不强制的,但是你应该去处理它,去看看是否有异常发生。而且,RunWorkerCompleted里的代码可以更新用户界面而不需要列集;而DoWork中的代码却不能。

为了增加报告进度,你可以:

  1. 设置WorkerReportsProgress属性为true
  2. 从DoWork函数中周期性的调用ReportProgress去处理百分比,和一个可选的用户状态对象
  3. 处理ProgressChanged事件,查询它的事件参数中的ProgressPercent属性
  4. ProgressChanged中的代码可以*地与UI控件互动,就像RunWorkerCompleted中一样。通常用来更新进度条。

为了能够取消,你可以:

  1. 设置WorkerSupportsCancellation属性为true
  2. 在DoWork代码中周期性地检查CancellationPending属性。如果为true,设置事件参数的Cancel属性为true且返回(工作线程也可以设置Cancel并退出即使CancellationPending不是true,如果它觉得任务太困难并且不能继续)。
  3. 调用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 = ; i <= ; i += )
{
if (_bw.CancellationPending) { e.Cancel = true; return; }
_bw.ReportProgress (i);
Thread.Sleep (); // Just for the demo... don't go sleeping
} // for real in pooled threads!
e.Result = ; // 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 + "%");
}
}
// Output:
Press Enter in the next seconds to cancel
Reached %
Reached %
Reached %
Reached %
Reached %
Reached %
Complete:
Press Enter in the next seconds to cancel
Reached %
Reached %
Reached %
You canceled!

子类化BackgroundWorker

 子类化BackgroundWorker是一种实现EAP简单的办法,你只需提供一个异步执行方法。

BackgroundWorker不是密封的,它提供了一个虚拟的OnDoWork函数。写一个长时间运行的函数时,预配置并发执行。然后客户只需处理RunWorkerCompleted和ProgressChanged事件。举个例子,假设有一个耗时的函数GetFinancialTotals:

public class Client

{

Dictionary<string,int> GetFinancialTotals(int foo,int bar){...}

...

}

我们对它进行重构:

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 (, "Working hard on this report...");
// Initialize financial report data
// ...
while (!<finished report>)
{
if (CancellationPending) { e.Cancel = true; return; }
// Perform another calculation step ...
// ...
ReportProgress (percentCompleteCalc, "Getting there...");
}
ReportProgress (, "Done!");
e.Result = Result = <completed report data>;
}
}

无论谁调用GetFinancialTotalsBackground都得到一个FinancialWorker对象:在真实世界中管理后台操作的一个封装。它能报告进度也能被取消,对于WPF和WF程序来说非常友好,并且也能很好地处理异常。

中断和放弃 (Interrupt and Abort)


所有阻塞的函数(如Sleep,Join,EndInvoke及Wait)将永远阻塞如果非阻塞条件不满足且没有指定超时。偶尔,这可能是有用的去释放一个永久阻塞的线程;如,当一个进程结束时,下面两个方法可以完成:

  • Thread.Interrupt
  • Thread.Abort

Abort函数也能结束一个非阻塞的线程(卡住,或者陷入了无限循环的)。Abort偶尔有用,但是Interrupt几乎从来都不需要。

Interrupt和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();
}
// Output:
Forcibly Woken!

中断一个线程并不会引起线程结束,除非ThreadInterruptedException没有被处理。如果在非阻塞的线程上调用Interrupt,这个线程继续执行直到下一次阻塞,此时将扔出一个ThreadInterruptedException异常。这避免了用于下面测试的必要:

if( (worker.ThreadState & ThreadState.WaitSleepJoin)>0 ) worker.Interrupt();这不是线程安全的,因为if语句和worker.Interrupt之间可能被强占。

随意中断一个线程是危险的,因为框架或第三方函数在调用栈中可能没有预期到会接收到中断。它所做的可能只是在一个简单的锁上或同步资源上阻塞线程,并且任何未决的中断将被踢开。如果函数不打算被中断(在finally块中带有适当的清理),对象可能留在无法使用或者资源没有完全释放的状态。

而且,Interrupt也是不必要的:如果你写了一段阻塞的代码,那么你也可以以更安全的方式完成同样的功能,使用通知来完成或者4.0的取消令牌。如果你想要不阻塞某人的其它代码,那么Abort几乎总是更有用。

Abort

阻塞线程也可以通过Abort来强制释放。这有点类似Interrupt,除了它扔出一个ThreadAbortException而不是ThreadInterruptedException之外。而且,异常能在catch块末重新扔出(为了更好的终止线程)除非在catch块中调用了Thread.ResetAbort。这时,线程的状态变成了AbortRequested。

一个未处理的ThreadAbortException异常是仅有的2个不会引起程序shutdown的异常之一(另外一个是AppDomainUnloadException)。

Interrupt和Abort更大的不同是当它在线程上被调用时不管发生什么都是不阻塞的。因此,Interrupt在做任何事情之前将等待直到下一次阻塞,Abort将在执行的地方直接扔出一个异常(非托管代码除外)。这是因为框架代码(代码不是abort-safe的)可能被放弃。如,当正在构造一个FileStream时调用了Abort,一个未托管的文件句柄可能仍然open直到应用程序域结束。这排除了在任何重要地方使用Abort。

这有2个例子,你可以安全使用Abort。其一,如果你打算在放弃之后tear down线程的应用程序域。如,你正在写的单元测试中。另外一个是在你自己的线程中调用Abort(因为你知道自己在干什么)。

放弃你自己的线程扔出一个异常:在catch后重新扔出。ASP.NET当你调用Redirect时就是这么做的。

安全取消 (Safe Cancellation)


就像我们前面看到的,调用Abort在大多数情况下是危险的。替代办法就是实现一个协作模式,工作线程周期性地检查它是否应该放弃。为了取消,客户只需简单地设置一个标记,然后等待工作线程结束。BackgroundWorker类实现了这样一个取消模式,你自己也可以很轻松的来实现。

最大的缺点是工作线程必须支持取消。尽管如此,这也是几个少数实现安全取消的方式之一。下面的例子演示了这个模式:

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就是用于这个目的的。尽管任何异常也一样工作。

下面是测试程序:

class Test
{
static void Main()
{
var canceler = new RulyCanceler();
new Thread (() => {
try { Work (canceler); }
catch (OperationCanceledException)
{
Console.WriteLine ("Canceled!");
}
}).Start();
Thread.Sleep ();
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类并添加一个静态bool字段_cancelRequest到Test类中。那么如果几个线程同时调用Work,设置_cancelRequest为true将同时取消所有工作线程。因此,RunlyCanceler类是非常有用的。只不过它有些粗糙,当我们看Work函数时它的意图不是很清晰。static void Work(RulyCanceler c)。

看起来Work函数想要在RulyCanceler对象上调用Cancel。这个例子中其实不是这样的,所以如果使用4.0中的取消令牌更加清晰。

Cancellation Tokens 取消令牌

.NET 4.0提供了2种方式的取消模式:CancellationTokenSource和CancellationToken。

  • CancellationTokenSource定义个一个Cancel的方法
  • CancellationToken定义一个IsCancellationRequested属性和ThrowIfCancellationRequested方法。

总之,这些是前面RulyCanceler类更复杂的版本。但是因为这些类型是独立的,因此你可以隔离取消与检查取消标记的能力。

为了使用它,你首先要实例化一个CancellationTokenSource对象:

  var cancelSource = new CancellationTokenSource();

然后传递它的Token属性到一个你想要支持取消的函数里:

  new Thread(()=>Work(cancelSource.Token)).Start();

下面是Work函数的定义

void Work(CancellationToken cancelToken)

{

  cancelToken.ThrowIfCancellationRequested();

  ...

}

当你想取消时,只需简单调用cancelSource的Cancel函数。

CancellationToken实际上是一个结构体,尽管它看起来像一个类。当隐式拷贝时,拷贝行为等同与且引用了原始的对象。

CancellationToken提供了2个非常有用的成员。第一个是WaitHandle,它返回被取消的对象的句柄。第二个是Register,让你注册一个回调函数,当取消被激活时执行。

取消令牌被框架本身使用,最明显的就是这些类:

  • ManualResetEventSlim & SemaphoreSlim
  • CountdownEvent
  • Barrier
  • BlockingCollection
  • PLINQ & Task Parallel Library

这些类大多数在Wait函数中使用了取消令牌。如果你在ManualResetEventSlim上等待并且指定了一个取消令牌,另外一个线程可以调用Cancel取消等待。这样比起Interrupt更加整洁和安全。

Lazy Initialization 延迟初始化


如何以线程安全的方式来延迟初始化一个共享的字段是线程编程中非常常见的问题。当一个类型的字段构造非常昂贵时就需要延迟初始化。

class Foo
{
public readonly Expensive Expensive = new Expensive();
...
}
class Expensive { /* Suppose this is expensive to construct */ }

这段代码的问题是实例化Foo导致非常大的性能下降--不管是否Expensive字段有没有被访问。很明显应该按需构造:

class Foo
{
Expensive _expensive;
public Expensive Expensive // Lazily instantiate Expensive
{
get
{
if (_expensive == null) _expensive = new Expensive();
return _expensive;
}
}
...
}

答案已经提出,可是它是线程安全的吗?考虑一下如果不加锁2个线程同时访问这个属性。如果2个线程都满足if语句,那么它将得到不同的Expensive对象。这可能导致难以察觉的错误,所以说通常情况下这不是线程安全的。

下面的解决方案是围绕检查和初始化加锁:

Expensive _expensive;
readonly object _expenseLock = new object();
public Expensive Expensive
{
get
{
lock (_expenseLock)
{
if (_expensive == null) _expensive = new Expensive();
return _expensive;
}
}
}

Lazy<T>

.NET 4.0提供了一个新的类Lazy<T>帮助延迟初始化。如果参数为true,那么它的实现是线程安全的。

Lazy<T>实际上实现的比这个版本稍微高效一点,叫double-checked locking。Double-checked locking为了避免当一个对象已经初始化时再次获取锁的成本,执行了一个额外的Volatile读。

为了使用它,用带有值工厂委托实例化一个类,告诉它如何初始化一个新值,并且使参数为true。然后通过Value属性来访问它的值。

Lazy<Expensive> _expensive = new Lazy<Expensive>
(() => new Expensive(), true);
public Expensive Expensive { get { return _expensive.Value; } }

如果你传递false给它,那么它不是线程安全的,这在单例模式中有意义。

LayInitializer

它是一个静态类,与Lazy类似,除了:

  • 它的功能是通过静态函数暴露出来,直接在你自己类型的字段上操作。这避免了间接层,在极端情况下改善了性能。
  • 它提供了另外一种模式的初始化--多线程竞争。

为了使用LazyInitializer,在访问字段之前调用EnsureInitialized,并传递一个引用给字段和工厂委托:

Expensive _expensive;
public Expensive Expensive
{
get // Implement double-checked locking
{
LazyInitializer.EnsureInitialized (ref _expensive,
() => new Expensive());
return _expensive;
}
}

你可以传递另外一个参数要求线程竞争去初始化。这听起来是线程不安全的,除了第一个线程总是抢先完成,所以你也只会得到一个实例。这种技术的好处是,他在多核下比double-checked locking更快--因为它完全不加锁。这只在极端的情况下使用,一般也很少使用。它的成本来自于:

  • 当线程数多于内核数时会变慢。
  • 它潜在地浪费了CPU资源去执行冗余的初始化。
  • 初始化逻辑必须是安全的(这个情况下,它是不安全的,如果Expensive的构造函数写了一个静态字段)
  • 如果实例了一个要求disposal的对象,那么浪费的对象如果没有额外的逻辑将不会调用disposed。

作为参考,下面是Double-Checked Locking的实现:

volatile Expensive _expensive;
public Expensive Expensive
{
get
{
if (_expensive == null)
{
var expensive = new Expensive();
lock (_expenseLock) if (_expensive == null) _expensive = 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;
}
}

线程的本地存储 Thread-Local Storage


这篇文章大多数致力于构造同步对象和并发访问数据引发的问题上。然而,有时你想要保持数据孤立,确保每一个线程有一份独立的副本。本地变量实际上就能完成这个功能,但是它们只在临时数据上有用。

你可能迫切地有这样的需求:你想保持数据孤立。这样用于存储带外数据(out-of-band),如消息,事务及安全令牌等。在函数间传递这样的数据是非常笨拙的,存储这些信息到静态字段,则又能被其它线程共享。

本地存储在优化并发代码上是有用的。它允许线程唯一地访问线程不安全对象的自己的版本而不需要加锁--不需要在函数调用之间重新构造对象。

有三种方法来实现线程本地存储。

[ThreadStatic]

最简单的办法是用TreadStatic属性来标记一个静态字段。

[ThreadStatic] static int _x;每一个线程都有一份_x的独立副本。

不幸的是,[ThreadStatic]在非静态字段上不工作(什么也不做);如果你想要实例化字段也工作或者开启一个非默认的值,使用ThreadLocal<T>是更好地选项。

ThreadLocal<T>

该类型是.NET 4.0新增的。为静态和实例化字段提供了线程本地存储,并且允许你指定默认值。

下面这行代码演示了如何为每个线程创建一个默认值为3的类型:

static ThreadLocal<int> _x = new ThreadLocal<int>(()=>3);

你可以使用_x的Value来设置和获取线程本地值。使用ThreadLocal的好处是这些值只在第一次使用它时创建。

ThreadLocal<T>和实例化字段

ThreadLocal<T>对于实例化字段和捕获本地变量也是有用的。比如,在多线程的环境下考虑一下如何产生一个随机值。Random类不是线程安全的,所以我们要么锁住它,要么为每个线程产生一个独立的Random对象。

ThreadLocal<T>使得后者很容易:

var localRandom = new ThreadLocal<Random>(()=>new Random());

Console.WriteLine(localRandom.Value.Next());

创建Random对象的工厂非常简单,尽管Random的无参构造函数依赖系统时钟。这对于在10ms之内产生的2个Random对象,值可能是一样的。下面的办法可以修复这个缺陷:

var localRandom = new ThreadLocal<Random>(()=>new Random(Guid.NewGuid().GetHashCode()));

GetData和SetData

第三种方法是在Thread的类上使用GetData和SetData。这些数据存储在特定的slot里。GetData返回这些数据,SetData写入这些数据。两个函数都要求LocalDataStoreSlot对象。同一个slot可以跨线程使用,但是它们仍然得到独立的值。如:

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 ? : (int) data; // null == uninitialized
}
set { Thread.SetData (_secSlot, value); }
}

这个例子中我们调用了Thread.GetNamedDataSlot来创建一个命名的slot,允许在不同的进程之间共享。你也可以通过实例化一个LocalDataStoreSlot来控制它的作用域,而不需要任何名字。

Thread.FreeNamedDataSlot可以释放一个跨线程的slot,但是只有在所有引用都超出作用域时才会被丢弃或回收。这确保线程不会离开自己的足迹,只要它保持了对一个正确的LocalDataStoreSlot的引用。

Timer 定时器


如果你要周期性的执行代码,最简单的办法就是使用Timer。定时器很方便,在使用内存和资源方面也很高效--相对于下面的代码而言:

new Thread(delegate(){
while(enabled)
{
DoSomeAction();
Thread.Sleep(TimeSpan.FromHours();
}
}).Start();

不仅它永久占用了线程,而且没有额外的代码,DoSomeAction将在每天的某个时候发生。定时器能解决这些问题。

.NET提供了4种定时器。其中2种常用于多线程中:

  • System.Threading.Timer
  • System.Timers.Timer

另外2个在单线程中有特殊用途:

  • System.Windows.Forms.Timer (WF Timer)
  • System.Windows.Threading.DispatcherTimer (WPF Timer)

多线程定时器非常强大,准确和灵活;单线程定时器很安全且在更新WF或WPF的UI元素时更方便。

多线程定时器

System.Threading.Timer是一个最简单的定时器:它有一个构造函数和2个方法。下面的例子中,定时器在5秒之后调用了Tick函数来写“tick...”,然后每秒写一次直到用户按下Enter:

using System;
using System.Threading; class Program
{
static void Main()
{
Timer tmr = new Timer(Tick,"tick...",,);
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);
}
}

你可以在以后通过Change函数来修改定时器的间隔。如果你只想定时器被激活一次,你可以传递Timeout.Infinite给构造函数的最后一个参数。

.NET提供了同名的另外一个定时器,在命名空间System.Timers中。这个只是简单地封装了System.Threading.Timer而已,提供了额外的便利。下面是它添加的特性:

  • 实现了Component接口,使它可以出现在Visual Studio的Designer中
  • Interval属性替换了Change函数
  • Elapsed事件替代了回调委托
  • Enabled属性来启动和停止定时器
  • Start和Stop函数可能会被Enabled混淆
  • AutoReset标记来指示是否循环调用(默认false,只调用一次)
  • 带有Invoke和BeginInvoke的SynchronizingObject属性用于WPF或WF上安全调用。
using System;
using System.Timers; // Timers namespace rather than Threading
class SystemTimer
{
static void Main()
{
Timer tmr = new Timer(); // Doesn't require any args
tmr.Interval = ;
tmr.Elapsed += tmr_Elapsed; // 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毫秒误差。如果你需要更精确,你可以使用本地方法(native interop)来调用多媒体定时器(Windows multimedia timer)。它可以精确到1ms,定义在winmm.dll。首先调用timeBeginPeriod通知操作系统需要一个精确的定时器,然后调用timeSetEvent开启一个多媒体定时器。使用完之后调用timeKillEvent来停止定时器并且timeEndPeriod去通知OS你不需要高精度定时器。你可以发现完整的例子,通过在internet上查询关键字dllimport winmm.dll timesetevent。

单线程定时器 

.NET提供单线程定时器是为了消除WPF和WF程序的线程安全问题。

  • System.Windows.Threading.DispatcherTimer (WPF)
  • System.Windows.Forms.Timer (WF)

单线程定时器别工作在各自的环境之外。如果你使用个WF的定时器在Windows Service应用程序上,那么Timer将不会被激活。

两者都类似于System.Timers.Timer,暴露了Interval,Tick,Start和Stop,用法也类似。但是,它们的内部工作方式是不同的。不同于多线程定时器使用线程池,它们使用UI线程的消息pump。这意味着Tick总是在创建它们的线程中被激活,一般,这个线程与管理所有UI元素和控件的线程是同一个线程。这有下面的好处:

  • 可以不考虑线程安全的问题
  • 新的Tick永远不会被激活,除非前面的Tick完成。
  • 你可以直接在Tick函数中更新UI,不需要调用Dispatcher或Control的Invoke。

这听起来仿佛非常好。但是如果你有多个定时器,那么这些定时器将在用一个线程中完成,也就是处理UI事件的那个线程。这带来了一些坏处:

  • 除非Tick事件处理的很快,否则UI将无法及时响应用户输入。

这使得WF和WPF的定时器只能用在小的任务中,尤其是更新用户界面这些方面。否则,你还是需要多线程定时器。

在精确性方面,单线程定时器类似于多线程定时器(10分之1毫秒),然而,它们要差一些,因为它们很容易被UI的请求(或其它定时器)所延迟。