任务同步
线程问题
用多个线程编程并不容易。在启动访问相同数据的多个线程时,会间歇性地遇到难以发现的问题。如果使用任务、并行LINQ或Parallel类,也会遇到这些问题。为了避免这些问题,必须特别注意同步问题和多个线程可能发生的其他问题。下面探讨与线程相关的问题:争用条件和死锁。
可以使用命令行参数启动Threadinglssues示例应用程序,来模拟争用条件或死锁。
争用条件
如果两个或多个线程访问相同的对象,并且对共享状态的访问没有同步,就会出现争用条件。
为了说明争用条件,下面的例子定义一个StateObject类,它包含一个int字段和一个Changestate()方法。在Changestate()方法的实现代码中,验证状态变量是否为5。如果它包含,就递增其值。下一条语句是Trace.Assert,它立刻验证state现在是否为6。
在给包含5的变量递增了1后,可能认为该变量的值就是6。但事实不一定是这样。例如,如果一个线程刚刚执行完 if (_state == 5) 语句,它就被其他线程抢占,调度器运行另一个线程。第二个线程现在进入 if 体,因为的值仍是5,所以将它递增到6。第一个线程现在再次被调度,在下一条语句中,递增到7。这时就发生了争用条件,并显示断言消息。
public class StateObject
{
private int _state = 5;
public void ChangeState(int loop)
{
if (_state == 5)
{
_state++;
Trace.Assert(_state == 6, $"在循环{loop}了,发生了争用条件");
}
}
}
public class SampleTask
{
private StateObject _s1;
private StateObject _s2;
public SampleTask() {
}
public SampleTask(StateObject s1, StateObject s2)
{
_s1 = s1;
_s2 = s2;
}
/// <summary>
/// 将一个StateObject类作为其参数。在一个无限while循环中,调用ChangeState()方法。
/// </summary>
/// <param name="o"></param>
public void RaceCondition(object o)
{
Trace.Assert(o is StateObject, "o必须是StateObject类型");
StateObject state = o as StateObject;
int i = 0;
while (true)
{
state.ChangeState(i++);
}
}
}
/// <summary>
/// 此方法新建了一个StateObject对象,它由所有任务共享。
/// 通过使用传递给Task的Run方法的lambda表达式调用Racecondition方法来创建Task对象。
/// 然后,主线程等待用户输入。但是,因为可能出现争用,所以程序很有可能在读取用户输入前就挂起:
/// </summary>
public static void RaceCondition()
{
var state = new StateObject();
for (int i = 0; i < 50000; i++)
{
Task.Run(() => new SampleTask().RaceCondition(state));
}
}
有可能会出现以下结果
解决争用
可以锁定共享的对象。这可以在线程中完成:用lock语句锁定在线程中共享的state变量。只有一个线程能在锁定块中处理共享的state对象。由于这个对象在所有的线程之间共享,因此,如果一个线程锁定了state,另一个线程就必须等待该锁定的解除。一旦接受锁定,线程就拥有该锁定,直到该锁定块的末尾才解除锁定。如果改变state变量引用的对象的每个线程都
使用一个锁定,就不会出现争用条件。
public void RaceCondition(object o)
{
Trace.Assert(o is StateObject, "o必须是StateObject类型");
StateObject state = o as StateObject;
int i = 0;
while (true)
{
lock (state) // 如想发生争用,请注释掉
{
state.ChangeState(i++);
}
}
}
在使用共享对象时,除了进行锁定之外,还可以将共享对象设置为线程安全的对象。在下面的代码中,ChangeState()方法包含一条lock语句。由于不能锁定state变量本身(只有引用类型才能用于锁定),因此定义一个曲类型的变量sync,将它用于lock语句。如果每次state的值更改时,都使用同一个同步对象来锁定,就不会出现争用条件。
public class StateObject
{
private int _state = 5;
private object _sync = new object();
public void ChangeState(int loop)
{
lock (_sync)// 如想发生争用,请注释掉
{
if (_state == 5)
{
_state++;
Trace.Assert(_state == 6, $"在循环{loop}了,发生了争用条件");
}
}
}
}
死锁
过多的锁定也会有麻烦。在死锁中,至少有两个线程被挂起,并等待对方解除锁定。由于两个线程都在等待对方,就出现了死锁,线程将无限等待下去。
为了说明死锁,下面实例化StateObject类型的两个对象,并把它们传递给SampleTask类的构造函数。创建两个任务,其中一个任务运行础Deallock1()方法,另一个任务运行Deallock2()方法。
var state1 = new StateObject();
var state2 = new StateObject();
new Task(new SampleTask(state1, state2).Deallock1).Start();
new Task(new SampleTask(state1, state2).Deallock2).Start();
Deallock1()和Deallock2()方法现在改变两个对象 state1 和 state2的状态,所以生成了两个锁。Deallock1()方法先锁定s1,接着锁定s2。Deallock2()方法先锁定s2,再锁定s1。现在,有可能Deallock1()方法中s1的锁定会被解除。接着,出现一次线程切换,佚ad厩k20方法开始运行,并锁定s2。第二个线程现在等待s1锁定的解除。因为它需要等待,所以线程调度器再次调度第一个线程,但第一个线程在等待s2锁定的解除。这两个线程现在都在等待,只要锁定块没有结束,就不会解除锁定。这是一个典型的死锁。
private StateObject _s1;
private StateObject _s2;
public SampleTask(StateObject s1, StateObject s2)
{
_s1 = s1;
_s2 = s2;
}
public void Deallock1()
{
int i = 0;
while (true)
{
lock (_s1)
{
lock (_s2)
{
_s1.ChangeState(i);
_s2.ChangeState(i++);
Console.WriteLine($"正在运行{i}");
}
}
}
}
public void Deallock2()
{
int i = 0;
while (true)
{
lock (_s2)
{
lock (_s1)
{
_s1.ChangeState(i);
_s2.ChangeState(i++);
Console.WriteLine($"正在运行{i}");
}
}
}
}
死锁问题并不总是像这样那么明显。一个线程锁定了s1,接着锁定s2;另一个线程锁定了s2,接着锁定s1。在本例中只需要改变锁定顺序,这两个线程就会以相同的顺序进行锁定。但是,在较大的应用程序中,锁定可能隐藏在方法的深处。为了避免这个问题,可以在应用程序的体系架构中,从一开始就设计好锁定顺序,也可以为锁定定义超时时间。
lock语句和线程安全
C#为多个线程的同步提供了自己的关键字:lock语句。lock语句是设置锁定和解除锁定的一种简单方式。
public string DealLockSample()
{
StringBuilder sb = new StringBuilder();
int numTasks = 20;
var state = new ShareState();
var tasks = new Task[numTasks];
for (int i = 0; i < numTasks; i++)
{
tasks[i] = Task.Run(() => new Job(state).DoTheJob());
}
Task.WaitAll(tasks);
sb.Append($"总共循环次数{state.State}\r\n");
return sb.ToString();
}
结果
总共循环次数467537
总共循环次数631044
总共循环次数308436
总共循环次数246768
总共循环次数358334
总共循环次数313225
每次运行的结果都不同,但没有一个结果是正确的。如前所述,调试版本和发布版本的区别很大。根据使用的CPU类型,结果也不一样。如果将循环次数改为比较小的值,就会多次得到正确的值,但不是每次都正确。这个应用程序非常小,很容易看出问题,但该问题的原因在大型应用程序中就很难确定。
必须在这个程序中添加同步功能,这可以用lock关键字实现。用lock语句定义的对象表示,要等待指定对象的锁定。只能传递引用类型。锁定值类型只是锁定了一个副本,这没有什么意义。如果对值类型使用了lock语句,C#编译器就会发出一个错误。进行了锁定后一一只锁定了一个线程,就可以运行lock语句块。在lock语句块的最后,对象的锁定被解除,另一个等待锁定的线程就可以获得该锁定块了。
lock(obj)
{
}
要锁定静态成员,可以把锁放在object类型或静态成员上:
lock(typeof(StaticClass))
{
}
使用lock关键字可以将类的实例成员设置为线程安全的。这样,一次只有一个线程能访问相同实例的DoThis()和DoThat()方法。
public class Demo
{
public void DoThis()
{
lock(this)
{
}
}
public void DoThat()
{
lock(this)
{
}
}
}
但是,因为实例的对象也可以用于外部的同步访问,而且我们不能在类自身中控制这种访问,所以应采用SyncRoot模式。通过SyncRoot模式,创建一个私有对象_syncRoot,将这个对象用于lock语句。
public class Demo
{
private Object _syncRoot = new object();
public void DoThis()
{
lock(_syncRoot)
{
}
}
public void DoThat()
{
lock(_syncRoot)
{
}
}
}
使用锁定需要时间,且并不总是必须的。可以创建类的两个版本,一个同步版本,一个异步版本。下一个示例通过修改Demo类来说明。Demo类本身并不是同步的,这可以在DoThis()和DoThat()方法的实现中看出。该类还定义了IsSynchronized属性,客户可以从该属性中获得类的同步选项信息。为了获得该类的同步版本,可以使用静态方法Synchronized()传递一个非同步对象,这个方法会返回SynchronizedDemo类型的对象。SynchronizedDemo实现为派生自基类Demo的一个内部类,并重写基类的虚成员。重写的成员使用了SyncRoot模式。
public class Demo
{
public virtual bool IsSynchronized => false;
public static Demo Synchronized(Demo d)
{
if (!d.IsSynchronized)
{
return new SynchronizationDemo(d);
}
return d;
}
public virtual void DoThis()
{
}
public virtual void DoThat()
{
}
private class SynchronizationDemo : Demo
{
private Object _syncRoot = new object();
private Demo _d;
public override bool IsSynchronized => true;
public SynchronizationDemo(Demo d)
{
_d = d;
}
public override void DoThis()
{
lock (_syncRoot)
{
_d.DoThis();
}
}
public override void DoThat()
{
lock (_syncRoot)
{
_d.DoThat();
}
}
}
}
必须注意,在使用SynchronizedDemo类时,只有方法是同步的。对这个类的两个成员的调用并没有同步。
首先修改异步的SharedState类,以使用SyncRoot模式。如果试图用SyncRoot模式锁定对属性的访问,使SharedState类变成线程安全的,就仍会出现前面描述的争用条件。
public class ShareState
{
private int _state = 0;
private object _syncRoot = new object();
public int State
{
get { lock (_syncRoot) { return _state; } }
set { lock (_syncRoot) { _state = value; } }
}
}
调用方法DoTheJob()的线程访问SharedState类的get存取器,以获得state的当前值,接着get存取器给state设置新值。在调用对象的get和set存取器期间,对象没有锁定,另一个线程可以获得临时值
public void DoTheJob()
{
for (int i = 0; i < 50000; i++)
{
_shareState.State += 1;
}
}
所以,最好不改变SharedState类,让它依旧没有线程安全性
public class ShareState
{
public int State { get; set; }
}
然后在DoTheJob方法中,将lock语句添加到合适的地方
private ShareState _shareState;
public Job(ShareState shareState)
{
_shareState = shareState;
}
/// <summary>
/// ShareState,不锁定
/// Job中方法DoTheJobByJobMethodLock,锁定
/// </summary>
public void DoTheJobByJobMethodLock()
{
for (int i = 0; i < 50000; i++)
{
lock (_shareState)
{
_shareState.State += 1;
}
}
}
这样,应用程序的结果就总是正确的:
总共循环次数1000000
注意:在一个地方使用lock语句并不意味着,访问对象的其他线程都正在等待。必须对每个访问共享状态的线程显式地使用同步功能。
当然,还必须修改SharedState类的设计,并作为一个原子操作提供递增方式。这是一个设计问题一一把什么实现为类的原子功能?下面的代码片段锁定了递增操作。
public class ShareStateMethodLock
{
private int _state = 0;
private object _syncRoot = new object();
public int State => _state;
public int IncrementState()
{
lock (_syncRoot)
{
return ++_state;
}
}
}
目前最好的方式
public class ShareState
{
public int State { get; set; }
}
public class Job
{
private ShareState _shareState;
public Job(ShareState shareState)
{
_shareState = shareState;
}
/// <summary>
/// ShareState,不锁定
/// Job中方法DoTheJobByJobMethodLock,锁定
/// </summary>
public void DoTheJobByJobMethodLock()
{
for (int i = 0; i < 50000; i++)
{
lock (_shareState)
{
_shareState.State += 1;
}
}
}
}
锁定状态的递增还有一种更快的方式,如下节所示。
Interlocked类
Interlocked类用于使变量的简单语句原子化。i++不是线程安全的,它的操作包括从内存中获取一个值,给该值递增 1,再将它存储回内存。这些操作都可能会被线程调度器打断。Interlocked提供了以线程安全的方式递增、递减、交换和读取值的方法。
与其他同步技术相比,使用Interlocked类会快得多。但是,它只能用于简单的同步问题。
Interlocked.CompareExchange()方法
原始代码
lock(this)
{
if(_someState == null)
{
_someState = newState;
}
}
使用了Interlocked.CompareExchange()方法:完成的功能相同,但更快
Interlocked.CompareExchange<SomeState>(ref someState,newState,null);
进行递归操作
public int State
{
get
{
lock(this)
{
return ++_state;
}
}
}
而使用较快的Interlocked.Increment()方法:
public int State
{
get
{
return Interlocked.Increment(ref _State);
}
}
Monitor类
lock语句由C#编译器解析为使用Monitor类。下面的lock语句:
lock(Obj)
{
//同步区域
}
被解析为调用Enter()方法,该方法会一直等待,直到线程锁定对象为止。一次只有一个线程能锁定对象。只要解除了锁定,线程就可以进入同步阶段。Monitor类的Exit()方法解除了锁定。编译器把Exit()方法放在块的finally处理程序中,所以如果抛出了异常,就会解除该锁定。
Monitor.Enter(obj);
try
{
//同步区域
}finally
{
Monitor.Exit(obj);
}
与C#的lock语句相比,Monitor类的主要优点是:可以添加一个等待被锁定的超时值。这样就不会无限期地等待被锁定,而可以像下面的例子那样使用TryEnter()方法,其中给它传递一个超时值,指定等待被锁定的最长时间。如果obj被锁定,TryEnter()方法就把布尔型的引用参数设置为true,并同步地访问由对象obj锁定的状态,如果另一个线程锁定obj的时间超过了500毫秒,TryEnter()方法就把变量lockTaken设置为flase,线程不再等待,而是用于执行其他操作。也许在以后,该线程会尝试再次获得锁定。
bool _lockToken = false;
Monitor.TryEnter(_obj,500,ref _lockTaken);
if(_lockTaken)
{
try
{
//需要lock+
//同步区域
}
finally
{
Monitor.Exit(obj);
}
}else
{
//不要lock,做其他的事情
}
SpinLook结构
如果基于对象的锁定对象(Monitor)的系统开销由于垃圾回收而过高,就可以使用SpinLook结构。如果有大量的锁定(例如,列表中的每个节点都有一个锁定),且锁定的时间总是非常短,SpinLock结构就很有用。应避免使用多个SpinLock结构,也不要调用任何可能阻塞的内容。
除了体系结构上的区别之外,SpinLock结构的用法非常类似于Monitor类。使用Enter()或TryEnter()方法获得锁定,使用Exit()方法释放锁定。SpinLock结构还提供了属性IsHeld和IsHeldByCurrentThread,指定它当前是否是锁定的。
传送SpinLock实例时要小心·因为SpinLock定义为结构,把一个变量赋予另一个变量会创建一个副本。总是通过引用传送SptnLock实例。
Waithandle基类
WaitHandIe是一个抽象基类,用于等待一个信号的设置。可以等待不同的信号,因为WaitHandle是一个基类,可以从中派生一些类。
等待句柄也由简单的异步委托使用,如下:
public delegate int TakesAWhileDelegate(int x, int ms);
异步委托的BeginInvoke()方法返回一个实现了IAsycResult接口的对象。使用IAsycResult接口,可以用AsycWaitHandle属性访问WaitHandle基类。在调用WaitOne()方法或者超时发生时,线程会等待接收一个与等待句柄相关的信号。调用Endlnvoke方法,线程最终会阻塞,直到得到结果为止:
public class AsyncDelegate
{
public string StartDemo() {
StringBuilder sb = new StringBuilder();
try
{
TakesAWhileDelegate d1 = TakesAWhile;
IAsyncResult ar = d1.BeginInvoke(1, 3000, null, null);
while (true)
{
sb.Append(".");
if (ar.AsyncWaitHandle.WaitOne(50))
{
sb.Append("现在不能返回结果\r\n");
break;
}
}
int result = d1.EndInvoke(ar);
sb.Append($"结果: {result}\r\n");
return sb.ToString();
}
catch (PlatformNotSupportedException)
{
return "PlatformNotSupported exception - with async delegates please use the full .NET Framework";
}
}
private int TakesAWhile(int x, int ms)
{
Task.Delay(ms).Wait();
return 42;
}
}
结果
.................................................现在不能返回结果
结果: 42
使用WaitHandle基类可以等待一个信号的出现(WaitOne()方法)、等待必须发出信号的多个对象(WaitAll()方法),或者等待多个对象中的一个(WaitAny()方法)。WaitAll()和WaitAny()是WaitHandle类的静态方法,接收一个WaitHandle参数数组。
WaitHandle基类有一个SafeWaitHandle属性,其中可以将一个本机句柄赋予一个操作系统资源,并等待该句柄。例如,可以指定一个等待文件I/O操作的完成。
因为Mutex、EventWaitHandle和Semaphore类派生自WaitHandle基类,所以可以在等待时使用它们。
Mutes类
Mutex(mutual exclusion,互斥)是.NET Framework中提供跨多个进程同步访问的一个类。它非常类似于Monitor类,因为它们都只有一个线程能拥有锁定。只有一个线程能获得互斥锁定,访问受互斥保护的同步代码区域。
在Mutex类的构造函数中,可以指定互斥是否最初应由主调线程拥有,定义互斥的名称,获得互斥是否己存在的信息。在下面的示例代码中,第3个参数定义为输出参数,接收一个表示互斥是否为新建的布尔值。如果返回的值是false,就表示互斥己经定义。互斥可以在另一个进程中定义,因为操作系统能够识别有名称的互斥,它由不同的进程共享。如果没有给互斥指定名称,互斥就是未命名的,不在不同的进程之间共享。
bool createdNew;
var mutex = new Mutex(false, "ProCharpMutex", out createdNew);
要打开己有的互斥,还可以使用Mutex.OpenExisting()方法,它不需要用构造函数创建互斥时需要的相同 .NET权限。
由于Mutex类派生自基类WaitHandle,因此可以利用WaitOne()方法获得互斥锁定,在该过程中成为该互斥的拥有者。通过调用ReleaseMutex()方法,即可释放互斥。
if(mutex.WaitOne())
{
try
{
//同步区域
}
finally
{
mutex.ReleaseMutex()
}
}
else
{
//当等待发生时的一些问题
}
由于系统能识别有名称的互斥,因此可以使用它禁止应用程序启动两次。在下面的WPF应用程序中,调用了Mutex对象的构造函数。接着,验证名称为SingletonWinAppMutex的互斥是否存在。如果存在,应用程序就退出。
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
bool mutexCreated;
var mutex = new Mutex(false, "SingletonWinAppMutex", out mutexCreated);
if (mutexCreated)
{
MessageBox.Show("你只能开启一个应用实例");
Application.Current.Shutdown();
}
base.OnStartup(e);
}
}
Semaphore类
信号量非常类似于互斥,其区别是,信号量可以同时由多个线程使用。信号量是一种计数的互斥锁定。使用信号量,可以定义允许同时访问受旗语锁定保护的资源的线程个数。如果需要限制可以访问可用资源的线程数,信号量就很有用。例如,如果系统有3个物理端口可用,就允许3个线程同时访问I/0端口,但第4个线程需要等待前3个线程中的一个释放资源。
.NET Core 1.0为信号量功能提供了两个类Semaphore和SemaphoreSlim。SemaphoreSlim类可以命名,使用系统范围内的资源,允许在不同进程之间同步。SemaphoreSlim类是对较短等待时间进行了优化的轻型版本。
在下面的示例应用程序中,在Main()方法中创建了6个任务和一个计数为3的信号量。在Semaphore类的构造函数中,定义了锁定个数的计数,它可以用信号量(第二个参数)来获得,还定义了最初释放的锁定数(第一个参数)。如果第一个参数的值小于第二个参数,它们的差就是已经分配线程的计数值。与互斥一样,也可以给信号量指定名称,使之在不同的进程之间共享。这里定义信号量时没有指定名称,所以它只能在这个进程中使用。在创建了SemaphoreSlim对象之后,启动6个任务,它们都获得了相同的信号量。
public class SemaphoreDemo
{
public string SemaphoreStart() {
int taskCount = 6;
int SemaphoreCount = 3;
var Semaphore = new SemaphoreSlim(SemaphoreCount, SemaphoreCount);
StringBuilder sb = new StringBuilder();
var tasks = new Task[taskCount];
for (int i = 0; i < taskCount; i++)
{
tasks[i] = Task.Run(() => sb.Append(TaskMain(Semaphore)));
}
Task.WaitAll(tasks);
sb.Append("所有任务已完成");
return sb.ToString();
}
private string TaskMain(SemaphoreSlim semaphore)
{
StringBuilder sb = new StringBuilder();
bool isCompleted = false;
while (!isCompleted)
{
if (semaphore.Wait(600))
{
try
{
sb.Append($"Task{Task.CurrentId}锁住了semaphore\r\n");
Task.Delay(2000).Wait();
}
finally
{
sb.Append($"Task{Task.CurrentId}释放了semaphore\r\n");
semaphore.Release();
isCompleted = true;
}
}
else {
sb.Append($"Task{Task.CurrentId}超时,请等待\r\n");
}
}
return sb.ToString();
}
}
结果
Task330锁住了semaphore
Task330释放了semaphore
Task331锁住了semaphore
Task331释放了semaphore
Task329锁住了semaphore
Task329释放了semaphore
Task332超时,请等待
Task332超时,请等待
Task332超时,请等待
Task332超时,请等待
Task332锁住了semaphore
Task332释放了semaphore
Task333超时,请等待
Task333超时,请等待
Task333超时,请等待
Task333锁住了semaphore
Task333释放了semaphore
Task334超时,请等待
Task334锁住了semaphore
Task334释放了semaphore
所有任务已完成
Events类
与互斥和信号量对象一样,事件也是一个系统范围内的资源同步方法。为了从托管代码中使用系统事件,.NET Framework在System.Threading名称空间中提供了ManualResetEvent、AutoResetEvent、ManualResetEventSIim和CountdownEvent类。
注意:之前介绍了c#中的event关键字,它与System.Threading称空间中的event类没有关系。event关键字基于委托,而上述两个event类是.NET封装器,用于系统范围内的本机事件资源的同步。
可以使用事件通知其他任务:这里有一些数据,并完成了一些操作等。事件可以发信号,也可以不发信号。使用前面介绍的WaitHandle类,任务可以等待处于发信号状态的事件。
调用Set()方法,即可向ManualResetEventSlim发信号。调用Reset()方法,可以使之返回不发信号的状态。如果多个线程等待向一个事件发信号,并调用了set()方法,就释放所有等待的线程。另外,如果一个线程刚刚调用了WaitOne()方法,但事件己经发出信号,等待的线程就可以继续等待。
也通过调用Set()方法向AutoResetEvent发信号。也可以使用Reset()方法使之返回不发信号的状态。但是,如果一个线程在等待自动重置的事件发信号,当第一个线程的等待状态结束时,该事件会自动变为不发信号的状态。这样,如果多个线程在等待向事件发信号,就只有一个线程结束其等待状态,它不是等待时间最长的线程,而是优先级最高的线程。
为了说明ManualResetEventSlim类的事件,如下代码:
public class EventDemo
{
/// <summary>
/// 程序的EventDemoStart()方法定义了包含4个ManualResetEventSlim对象的数组和包含4个Calculator对象的数组。
/// 每个Calculator在构造函数中用一个ManualResetEventSlim对象初始化,这样每个任务在完成时都有自己的事件对象来发信号。
/// 现在使用Task类,让不同的任务执行计算任务。
/// </summary>
public string EventDemoStart()
{
const int taskCount = 4;
var mEvents = new ManualResetEventSlim[taskCount];
var waitHandles = new WaitHandle[taskCount];
var calcs = new Calculator[taskCount];
StringBuilder sb = new StringBuilder();
for (int i = 0; i < taskCount; i++)
{
int j = i;
mEvents[i] = new ManualResetEventSlim(false);
waitHandles[i] = mEvents[i].WaitHandle;
calcs[i] = new Calculator(mEvents[i]);
Task.Run(() => sb.Append(calcs[j].Calculation(j + 1, j + 3)));
}
// WaitHandle类现在用于等待数组中的任意一个事件。
// WaitAny()方法等待向任意一个事件发信号。
// 与ManualResetEvent对象不同,ManualResetEventSlim对象不派生自WaitHandle类。
// 因此有一个WaitHandle对象的集合,它在ManualResetEventSlim类的WaitHandle属性中填充。
// 从WaitAny()方法返回的index值匹配传递给Wainy()方法的事件数组的索引,
// 以提供发信号的事件的相关信息,使用该索引可以从这个事件中读取结果。
for (int i = 0; i < taskCount; i++)
{
int index = WaitHandle.WaitAny(waitHandles);
if (index == WaitHandle.WaitTimeout)
{
sb.Append("超时\r\n");
}
else {
mEvents[index].Reset();
sb.Append($"索引{index}任务已完成,结果:{calcs[index].Result}\r\n");
}
}
return sb.ToString();
}
}
结果
Task816开始计算
Task816已经准备好
索引3任务已完成,结果:10
Task815开始计算
Task815已经准备好
索引2任务已完成,结果:8
Task813开始计算
Task813已经准备好
索引0任务已完成,结果:4
Task814开始计算
Task814已经准备好
索引1任务已完成,结果:6
在一个类似的场景中,为了把一些工作分支到多个任务中,并在以后合并结果,使用新的CountdownEvent类很有用。不需要为每个任务创建一个单独的事件对象,而只需要创建一个事件对象。CountdownEvent类为所有设置了事件的任务定义一个初始数字,在到达该计数后,就向CountdownEvent类发信号。
修改Calculator类,以使用CountdownEvent类替代ManualResetEvent类。不使用Set()方法设置信号,而使用CountdownEvent类定义Signal()方法。
public class Calculator
{
private CountdownEvent _cEvent;
public int Result { get; private set; }
public Calculator(CountdownEvent cv)
{
this._cEvent = cv;
}
public String Calculation(int x, int y)
{
StringBuilder sb = new StringBuilder();
sb.Append($"Task{Task.CurrentId}开始计算\r\n");
Task.Delay(new Random().Next(3000)).Wait();
Result = x + y;
//信号完成事件
sb.Append($"Task{Task.CurrentId}已经准备好\r\n");
_cEvent.Signal();
return sb.ToString();
}
}
public class EventDemoWithCountdownEvent
{
/// <summary>
/// 简化的EventDemo,使它只需要一个等待。如果不像EventDemo那样单独处理结果,这个版本就可以了。
/// </summary>
public string EventDemoWithCountdownEventStart()
{
const int taskCount = 4;
var cEvent = new CountdownEvent(taskCount);
var calcs = new Calculator[taskCount];
StringBuilder sb = new StringBuilder();
for (int i = 0; i < taskCount; i++)
{
calcs[i] = new Calculator(cEvent);
int j = i;
Task.Run(() => sb.Append(calcs[j].Calculation(j + 1, j + 3)));
}
cEvent.Wait();
sb.Append($"所有已完成\r\n");
for (int i = 0; i < taskCount; i++)
{
sb.Append($"任务{i},结果:{calcs[i].Result}\r\n");
}
return sb.ToString();
}
}
结果
Task298开始计算
Task298已经准备好
Task300开始计算
Task300已经准备好
Task299开始计算
Task299已经准备好
Task297开始计算
Task297已经准备好
所有已完成
任务0,结果:4
任务1,结果:6
任务2,结果:8
任务3,结果:10
Barrier类
对于同步,Barrier类非常适用于其中工作有多个任务分支且以后又需要合并工作的情况。Barrier类用于需要同步的参与者。激活一个任务时,就可以动态地添加其他参与者,例如,从父任务中创建子任务。参与者在继续之前,可以等待所有其他参与者完成其工作。
BarrierSample有点复杂,但它展示了Ban•ier类型的功能。
下面的代码使用一个包含2000个字符串的集合。使用多个任务遍历该集合,并统计以a、b、c等开头的字符串个数。工作不仅分布在不同的任务之间,也放在一个任务中。毕竟所有的任务都迭代字符串的第一个集合,汇总结果,以后任务会继续处理下一个集合。
public static class BarrierDemo
{
/// <summary>
/// BarrierDemoStart()方法创建一个Barrier实例。在构造函数中,可以指定参与者的数量。
/// 在该示例中,这个数量是3(numberTasks+1),因为该示例创建了两个任务,BarrierDemoStart()方法本身也是一个参与者。
/// 使用Task.Run创建两个任务,把遍历集合的任务分为两个部分。
/// 启动该任务后,使用SignalAndWait()方法,BarrierDemoStart()方法在完成时发出信号,
/// 并等待所有其他参与者或者发出完成的信号,或者从Barrier类中删除它们。
/// 一旦所有的参与者都准备好,就提取任务的结果,并使用Zip()扩展方法把它们合并起来。接着进行下一次迭代,等待任务的下一个结果。
/// </summary>
public static void BarrierDemoStart()
{
const int numberTasks = 2;
const int partitionSize = 1000000;
const int loops = 5;
var taskResults = new Dictionary<int, int[][]>();
var data = new List<string>[loops];
for (int i = 0; i < loops; i++)
{
data[i] = new List<string>(FillData(partitionSize * numberTasks));
}
var barrier = new Barrier(1);
LogBarrierInformation("在barrier中初始化Participant", barrier);
for (int i = 0; i < numberTasks; i++)
{
barrier.AddParticipant();
int jobNumber = i;
taskResults.Add(i, new int[loops][]);
for (int loop = 0; loop < loops; loop++)
{
taskResults[i][loop] = new int[26];
}
Console.WriteLine($"BarrierDemoStart - 开始Task job{jobNumber}");
Task.Run(() => CalculationInTask(jobNumber, partitionSize, barrier, data, loops, taskResults[jobNumber]));
}
for (int loop = 0; loop < 5; loop++)
{
LogBarrierInformation("BarrierDemoStart task,开始发信号和等待", barrier);
barrier.SignalAndWait();
LogBarrierInformation("BarrierDemoStart task,等待完成", barrier);
// var resultCollection = tasks[0].Result.Zip(tasks[1].Result, (c1, c2) => c1 + c2);
int[][] resultCollection1 = taskResults[0];
int[][] resultCollection2 = taskResults[1];
var resultCollection = resultCollection1[loop].Zip(resultCollection2[loop], (c1, c2) => c1 + c2);
char ch = 'a';
int sum = 0;
foreach (var x in resultCollection)
{
Console.WriteLine($"{ch++},count:{x}");
sum += x;
}
LogBarrierInformation($"BarrierDemoStart task已完成loop{loop},sum:{sum}", barrier);
}
Console.WriteLine("已完成所有迭代");
Console.ReadLine();
}
/// <summary>
/// 创建一个集合,并用随机字符串填充它。
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
public static IEnumerable<string> FillData(int size)
{
var r = new Random();
return Enumerable.Range(0, size).Select(x => GetString(r));
}
private static string GetString(Random r)
{
var sb = new StringBuilder(6);
for (int i = 0; i < 6; i++)
{
sb.Append((char)(r.Next(26) + 97));
}
return sb.ToString();
}
/// <summary>
/// 义一个辅助方法,来显示Barrier的信息:
/// </summary>
/// <param name="info"></param>
/// <param name="barrier"></param>
private static void LogBarrierInformation(string info, Barrier barrier)
{
Console.WriteLine($"Task{Task.CurrentId}:{info} | 屏障中参与者总数:{barrier.ParticipantCount} | 屏障中未在当前阶段发出信号的参与者数{barrier.ParticipantsRemaining} | 屏障的当前阶段的编号:{barrier.CurrentPhaseNumber}");
}
/// <summary>
/// 定义了任务执行的作业。通过参数接收一个包含4项的元组。
/// 第3个参数是对Barrier实例的引用。用于计算的数据是数组IList<string>。
/// 最后一个参数是int锯齿数组,用于在任务执行过程中写出结果。
/// 任务把处理放在一个循环中。每一次循环中,都处理化IList<string>[] 的数组元素。每个循环完成
/// 后,任务通过调用SignalAndWait方法,发出做好了准备的信号,并等待,直到所有的其他任务也准备好处理为止。
/// 这个循环会继续执行,直到任务完全完成为止。
/// 接着,任务就会使用RemoveParticipant()方法从Barrier类中删除它自己。
/// </summary>
/// <param name="jobNumber"></param>
/// <param name="partitionSize"></param>
/// <param name="barrier"></param>
/// <param name="coll"></param>
/// <param name="loops"></param>
/// <param name="results"></param>
private static void CalculationInTask(int jobNumber, int partitionSize, Barrier barrier, IList<string>[] coll, int loops, int[][] results)
{
LogBarrierInformation("CalculationInTask 开始", barrier);
for (int i = 0; i < loops; i++)
{
var data = new List<string>(coll[i]);
int start = jobNumber * partitionSize;
int end = start + partitionSize;
Console.WriteLine($"Task{Task.CurrentId}在loop{i}:从{start}到{end}分割");
for (int j = start; j < end; j++)
{
char c = data[j][0];
results[i][c - 97]++;
}
Console.WriteLine($"计算已经完成,Task{Task.CurrentId}在loop{i}。{results[i][0]}次字符a,{results[i][25]}次字符z");
LogBarrierInformation("发送信号并等待所有任务", barrier);
barrier.SignalAndWait();
LogBarrierInformation("等待完成", barrier);
}
barrier.RemoveParticipant();
LogBarrierInformation("完成任务,删除Participant", barrier);
}
}
结果
Task:在barrier中初始化Participant | 屏障中参与者总数:1 | 屏障中未在当前阶段发出信号的参与者数1 | 屏障的当前阶段的编号 :0
BarrierDemoStart - 开始Task job0
BarrierDemoStart - 开始Task job1
Task:BarrierDemoStart task,开始发信号和等待 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前 阶段的编号:0
Task1:CalculationInTask 开始 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:0
Task2:CalculationInTask 开始 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:0
Task1在loop0:从0到1000000分割
Task2在loop0:从1000000到2000000分割
计算已经完成,Task1在loop0。38466次字符a,38525次字符z
Task1:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:0
计算已经完成,Task2在loop0。38590次字符a,38615次字符z
Task2:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数1 | 屏障的当前阶段的编号:0
Task2:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:1
Task1:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:1
Task:BarrierDemoStart task,等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编 号:1
a,count:77056
b,count:76741
c,count:76889
d,count:77111
e,count:77080
f,count:77157
g,count:77345
h,count:77118
i,count:76708
j,count:76825
k,count:77459
l,count:76495
m,count:76775
n,count:77095
o,count:77242
p,count:76505
q,count:77106
r,count:76925
s,count:76784
t,count:76756
u,count:76912
v,count:76634
w,count:76595
x,count:77023
y,count:76524
z,count:77140
Task:BarrierDemoStart task已完成loop0,sum:2000000 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏 障的当前阶段的编号:1
Task:BarrierDemoStart task,开始发信号和等待 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前 阶段的编号:1
Task1在loop1:从0到1000000分割
Task2在loop1:从1000000到2000000分割
计算已经完成,Task1在loop1。38579次字符a,38700次字符z
Task1:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:1
计算已经完成,Task2在loop1。38460次字符a,38384次字符z
Task2:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数1 | 屏障的当前阶段的编号:1
Task2:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:2
Task1:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:2
Task:BarrierDemoStart task,等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编 号:2
a,count:77039
b,count:76973
c,count:76950
d,count:77257
e,count:76463
f,count:76782
g,count:76839
h,count:76997
i,count:76843
j,count:76651
k,count:77485
l,count:76451
m,count:76513
n,count:76794
o,count:76947
p,count:76631
q,count:77181
r,count:76651
Task1在loop2:从0到1000000分割
s,count:76887
t,count:77113
u,count:77103
Task2在loop2:从1000000到2000000分割
v,count:77329
w,count:77236
x,count:76784
y,count:77017
z,count:77084
Task:BarrierDemoStart task已完成loop1,sum:2000000 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏 障的当前阶段的编号:2
Task:BarrierDemoStart task,开始发信号和等待 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前 阶段的编号:2
计算已经完成,Task1在loop2。38523次字符a,38411次字符z
Task1:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:2
计算已经完成,Task2在loop2。38501次字符a,38695次字符z
Task2:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数1 | 屏障的当前阶段的编号:2
Task2:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:3
Task:BarrierDemoStart task,等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编 号:3
a,count:77024
b,count:76726
c,count:77012
Task1:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:3
d,count:77107
e,count:76727
f,count:77261
g,count:77104
h,count:76840
i,count:76880
j,count:76773
k,count:76793
l,count:76438
m,count:76851
n,count:77263
o,count:76682
p,count:76952
q,count:76500
r,count:76955
s,count:76649
t,count:77139
u,count:76595
v,count:77149
w,count:77371
x,count:77148
y,count:76955
z,count:77106
Task:BarrierDemoStart task已完成loop2,sum:2000000 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏 障的当前阶段的编号:3
Task:BarrierDemoStart task,开始发信号和等待 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前 阶段的编号:3
Task2在loop3:从1000000到2000000分割
Task1在loop3:从0到1000000分割
计算已经完成,Task2在loop3。38313次字符a,38638次字符z
Task2:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:3
计算已经完成,Task1在loop3。38718次字符a,38297次字符z
Task1:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数1 | 屏障的当前阶段的编号:3
Task:BarrierDemoStart task,等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编 号:4
a,count:77031
b,count:77161
c,count:77497
d,count:77092
e,count:76925
f,count:77037
g,count:77048
h,count:77085
i,count:77129
Task1:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:4
j,count:76853
k,count:77010
l,count:76703
m,count:76316
n,count:76913
o,count:76315
p,count:76993
q,count:77185
r,count:76792
s,count:76534
t,count:77221
u,count:77235
v,count:76694
w,count:76946
x,count:76551
y,count:76799
z,count:76935
Task:BarrierDemoStart task已完成loop3,sum:2000000 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏 障的当前阶段的编号:4
Task:BarrierDemoStart task,开始发信号和等待 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前 阶段的编号:4
Task2:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:4
Task1在loop4:从0到1000000分割
Task2在loop4:从1000000到2000000分割
计算已经完成,Task1在loop4。38536次字符a,38708次字符z
Task1:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:4
计算已经完成,Task2在loop4。38432次字符a,38594次字符z
Task2:发送信号并等待所有任务 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数1 | 屏障的当前阶段的编号:4
Task2:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:5
Task2:完成任务,删除Participant | 屏障中参与者总数:2 | 屏障中未在当前阶段发出信号的参与者数2 | 屏障的当前阶段的编号:5
Task:BarrierDemoStart task,等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编 号:5
a,count:76968
b,count:77098
c,count:76501
d,count:76920
e,count:76912
f,count:76706
g,count:76800
h,count:76805
i,count:76638
j,count:76818
k,count:77041
l,count:76814
m,count:77296
n,count:77126
o,count:76920
p,count:76990
q,count:76812
r,count:76501
s,count:77323
t,count:77012
u,count:76615
v,count:77116
w,count:76860
x,count:77138
y,count:76968
z,count:77302
Task:BarrierDemoStart task已完成loop4,sum:2000000 | 屏障中参与者总数:2 | 屏障中未在当前阶段发出信号的参与者数2 | 屏 障的当前阶段的编号:5
已完成所有迭代
Task1:等待完成 | 屏障中参与者总数:3 | 屏障中未在当前阶段发出信号的参与者数3 | 屏障的当前阶段的编号:5
Task1:完成任务,删除Participant | 屏障中参与者总数:1 | 屏障中未在当前阶段发出信号的参与者数1 | 屏障的当前阶段的编号:5
ReaderWriterLockSlim
为了使锁定机制允许锁定多个读取器(而不是一个写入器)访问某个资源,可以使用ReaderWriterLockSlim类。这个类提供了一个锁定功能,如果没有写入器锁定资源,就允许多个读取器访问资源,但只能有一个写入器锁定该资源。
ReaderWriterLockSlim类有阻塞或不阻塞的方法来获取读取锁,如阻塞EnterReadLock()和不阻塞的TryEnterReadLock()方法,还可以使用阻塞的EnterWriteLock()和不阻塞的TryEnterWriteLock()方法获得写入锁定。如果任务先读取资源,之后写入资源,它就可以使用EnterUpgradableReadLock()或TryEnterUpgradableReadLock()方法获得可升级的读取锁定。有了这个锁定,就可以获得写入锁定,而无须释放读取锁定。
这个类的几个属性提供了当前锁定的相关信息,如CurrentReadCount、WaitingReadCount、WaitingUpgmdableReadCount和WaitingWriteCount。
下面的示例程序创建了一个包含6项的集合和一个ReaderWriterLockSlim对象。ReaderMethod方法获得一个读取锁定,读取列表中的所有项,并把它们写到控制台中。WriterMethod()方法试图获得一个写入锁定,以改变集合的所有值。在Main()方法中,启动6个任务,以调用ReaderMethod()或WriterMethod()方法。
public static class ReaderWriterLockSlimDemo
{
private static List<int> _items = new List<int>() { 0, 1, 2, 3, 4, 5 };
private static ReaderWriterLockSlim _rwl =
new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
public static void ReaderWriterLockSlimDemoStart()
{
var taskFactory = new TaskFactory(TaskCreationOptions.LongRunning,
TaskContinuationOptions.None);
var tasks = new Task[6];
tasks[0] = taskFactory.StartNew(WriterMethod, 1);
tasks[1] = taskFactory.StartNew(ReaderMethod, 1);
tasks[2] = taskFactory.StartNew(ReaderMethod, 2);
tasks[3] = taskFactory.StartNew(WriterMethod, 2);
tasks[4] = taskFactory.StartNew(ReaderMethod, 3);
tasks[5] = taskFactory.StartNew(ReaderMethod, 4);
Task.WaitAll(tasks);
Console.ReadLine();
}
public static void ReaderMethod(object reader)
{
try
{
_rwl.EnterReadLock();
for (int i = 0; i < _items.Count; i++)
{
Console.WriteLine($"reader {reader}, loop: {i}, item: {_items[i]}");
Task.Delay(40).Wait();
}
}
finally
{
_rwl.ExitReadLock();
}
}
public static void WriterMethod(object writer)
{
try
{
while (!_rwl.TryEnterWriteLock(50))
{
Console.WriteLine($"Writer {writer} waiting for the write lock");
Console.WriteLine($"current reader count: {_rwl.CurrentReadCount}");
}
Console.WriteLine($"Writer {writer} acquired the lock");
for (int i = 0; i < _items.Count; i++)
{
_items[i]++;
Task.Delay(50).Wait();
}
Console.WriteLine($"Writer {writer} finished");
}
finally
{
_rwl.ExitWriteLock();
}
}
}
结果
Writer 1 acquired the lock
Writer 2 waiting for the write lock
current reader count: 0
Writer 2 waiting for the write lock
current reader count: 0
Writer 2 waiting for the write lock
current reader count: 0
Writer 2 waiting for the write lock
current reader count: 0
Writer 2 waiting for the write lock
current reader count: 0
Writer 2 waiting for the write lock
current reader count: 0
Writer 1 finished
Writer 2 acquired the lock
Writer 2 finished
reader 4, loop: 0, item: 2
reader 3, loop: 0, item: 2
reader 2, loop: 0, item: 2
reader 1, loop: 0, item: 2
reader 1, loop: 1, item: 3
reader 4, loop: 1, item: 3
reader 3, loop: 1, item: 3
reader 2, loop: 1, item: 3
reader 3, loop: 2, item: 4
reader 4, loop: 2, item: 4
reader 1, loop: 2, item: 4
reader 2, loop: 2, item: 4
reader 1, loop: 3, item: 5
reader 4, loop: 3, item: 5
reader 3, loop: 3, item: 5
reader 2, loop: 3, item: 5
reader 2, loop: 4, item: 6
reader 1, loop: 4, item: 6
reader 3, loop: 4, item: 6
reader 4, loop: 4, item: 6
reader 1, loop: 5, item: 7
reader 4, loop: 5, item: 7
reader 3, loop: 5, item: 7
reader 2, loop: 5, item: 7
Timer类
使用计时器,可以重复调用方法。本节介绍两个计时器:System.Threading名称空间中的Timer类和用于基于XAML应用程序的DispatcherTimer。
使用System.Threading.Timer类,可以把要调用的方法作为构造函数的第一个参数传递。这个方法必须满足Timecallback委托的要求,该委托定义一个void返回类型和一个object参数。通过第二个参数,可以传递任意对象,用回调方法中的object参数接收对应的对象。例如,可以传递Event对象,向调用者发送信号。第3个参数指定第一次调用回调方法时的时间段。最后一个参数指定回调的重复时间间隔。如果计时器应只触发一次,就把第4个参数设置为值-1。
如果创建Timer对象后应改变时间间隔,就可以用Change()方法传递新值。