2线程
2.1 简单线程实现方式
简单的线程实现方式有几种
l 采用beginInvoke的方式;
l System.Timers.Timer类
l System.Threading.Timer类
l System.Windows.Forms.Timer类
l System.Web.UI.Timer类
后面的三个都可以归为一个,那就是timer的不同实现.其实这个timer的不同实现是针对不同环境所设定的.
下面简单说说Timer类的区别:
System.Timers.Timer: 它在一个或多个事件接收器中以定期间隔触发事件并执行相关的代码. 该类旨在供使用时为基于服务器或多线程环境中的服务组件,它没有用户界面并不是在运行时可见.
System.Threading.Timer:它按定期间隔在线程池线程上执行的单个回调方法. 当计时器实例化后不能更改定义的回调方法.
System.Windows.Forms.Timer以固定的间隔时间触发一个或多个事件,执行相应的代码的 Windows 窗体组件. 该组件没有用户界面,专用于在单线程环境中;它在 UI 线程上执行.
System.Web.UI.Timer: 一种 ASP.NET 组件,它按定期间隔执行异步或同步 web 页回发.
2.1.1采用BeginInvoke
线程最简单的实现方式就是创建一个委托然后使用异步的方式调用.异步委托其实是采用线程池的方式来发出委托请求.调用的时候采用BeginInvoke的方法即可.使用起来相当简单.同时委托是线程安全的.
例子如下:
classtestDelgate
{
delegatevoidDoXXOO();
DoXXOO doxxoo =newDoXXOO(() =>
{
Thread.Sleep(2000);
Console.WriteLine("doover ThreadID=" + Thread.CurrentThread.ManagedThreadId);
});
publicvoid TestDo()
{
Console.WriteLine("mainthread ThreadID=" + Thread.CurrentThread.ManagedThreadId);
doxxoo.BeginInvoke(null,null);
Console.WriteLine("mainover");
}
}
可以看出异步委托的执行其实是单独的线程.
不过有一点需要注意:主线程在没有等到委托线程执行完毕就提前结束,那么委托线程也会结束.
2.1.2 System.Timers.Timer类
这个System.timers.Timer这个类是一个定时触发事件的一个常用工具.起内部是封装了Threading下面的Timer
以下是Framework中的Timer类的Enabled属性的源码(此类的Start方法调用的是Enabled属性):
/// <devdoc>
/// <para>Gets or sets a value indicating whether the <see cref='System.Timers.Timer'/>
/// is able
/// to raise events at a defined interval.</para>
/// </devdoc>
//Microsoft - The default value by design is false, don't change it.
[Category("Behavior"), TimersDescription(SR.TimerEnabled), DefaultValue(false)]
public bool Enabled {
get {
return this.enabled;
}
set {
if (DesignMode) {
this.delayedEnable = value;
this.enabled = value;
}
else if (initializing)
this.delayedEnable = value;
else if (enabled != value) {
if (!value) {
if( timer != null) {
cookie = null;
timer.Dispose();
timer = null;
}
enabled = value;
}
else {
enabled = value;
if( timer == null) {
if (disposed) {
throw new ObjectDisposedException(GetType().Name);
}
int i = (int)Math.Ceiling(interval);
cookie = new Object();
timer = new System.Threading.Timer(callback, cookie, i, autoReset? i:Timeout.Infinite);
}
else {
UpdateTimer();
}
}
}
}
}
Timer 组件是基于服务器的计时器,它使您能够指定在应用程序中以周期性的间隔引发 Elapsed 事件. 然后可通过处理这个事件来提供常规处理.基于服务器的 Timer 是为在多线程环境中用于辅助线程而设计的. 服务器计时器可以在线程间移动来处理引发的 Elapsed 事件,这样就可以比 Windows 计时器更精确地按时引发事件.Interval 属性的值可以设置Timer引发Elapse的事件的间隔时间. 在Timer中设置Enabled=true和调用Start()方法是一样的.Stop()方法和Enabled=false是一样的. 只有当AutoReset设置为true的时候,Elapsed事件才会在每次Interval时间间隔到达后引发. 当 AutoReset 设置为 false 时,Timer 只在第一个 Interval 过后引发一次 Elapsed 事件.后面将不再引发事件.在Elapsed事件中如果要访问UI需要使用UI线程的invoke的方法.
例子:
classTestTimer
{
private System.Timers.Timer aTimer;
public void Test()
{
aTimer = newSystem.Timers.Timer(10000);
aTimer.Elapsed += newElapsedEventHandler(OnTimedEvent);
aTimer.Interval = 2000;
aTimer.Enabled = true;
Console.WriteLine("start.....");
Console.ReadLine();
GC.KeepAlive(aTimer);
Console.WriteLine("over....");
}
private void OnTimedEvent(object source,ElapsedEventArgs e)
{
Console.WriteLine("TheElapsed event was raised at {0}", e.SignalTime);
}
}
输出结果:
2.1.3 Threading.Timer类
此Timer提供用于在指定时间间隔在线程池线程上执行一种方法的机制. System.Threading.Timer 是一个简单, 轻型,它使用回调方法的计时器.但这个类也是所有timer中内部实现最为复杂的一个.这里就不解释具体实现,有兴趣的人可以直接去下载源码查看(https://referencesource.microsoft.com/#mscorlib/system/threading/timer.cs).
Timer的继承层次:
由此可以看出继承层次是很低级别的.
当创建一个计时器时,可以指定要在该方法的第一次执行之前等待的时间和下次执行的间隔时间. Timer 类和系统时钟频率一样.如果间隔时间小于大约15毫秒那么在Windows7/8中将采用系统所定义的时钟频率间隔来执行TimerCallback 委托.更改到期时间和间隔时间或禁用该计时器,通过使用 Change 方法完成.使用 Dispose方法可以来释放Timer对象持有的资源.请注意,回调可能发生在 Dispose() 已调用之后,因为计时器是使用线程池的线程来执行回调.此时可以使用 Dispose(WaitHandle) 方法重载来等待,直到所有回调都已都完成.
Timer对象在没有被引用的时候可能会被垃圾收集器回收.所以在定义的时候请保持对此对象的引用.
例子:
classtestThreadTimer
{
publicvoid test()
{
AutoResetEvent autorest =newAutoResetEvent(false);
int invokeCount = 0;
var t =new System.Threading.Timer((x) =>
{
AutoResetEvent autoWait =(AutoResetEvent)x;
Console.WriteLine("{0} count {1,2}.",
DateTime.Now.ToString("h:mm:ss.fff"),
(++invokeCount).ToString());
if(invokeCount % 10 == 0)
{
autorest.Set();
}
}, autorest, 500, 500);
autorest.WaitOne();
Console.WriteLine("change");
t.Change(1000, 1000);
autorest.WaitOne();
t.Dispose();
Console.WriteLine("over");
}
}
输出结果:
从上面也可以看出其实计时不是很精确的.
2.1.4System.Windows.Forms.Timer类
此Timer是Windows 计时器专为使用 UI 线程来执行处理的单线程的环境. 它要求代码有一个可用的用户界面消息泵,并且始终在同一个线程操作或到另一个线程的调用封送.当您使用此计时器时,使用 Tick 事件以执行轮询操作也可为指定的时间段显示一个初始屏幕. 每当 Enabled 属性设置为 true 和 Interval 属性大于零时, Tick 根据Interval 属性设置的时间间隔引发事件.
注意:Windows 窗体计时器组件是单线程,并仅限于精度为 55 毫秒.
由于此Timer可以直接拖放到窗体界面然后进行属性的设置,使用起来相对简单不再单独写例子.
2.1.5 System.Web.UI.Timer类
Timer 控件使您能够指定的时间间隔执行回发. 当您使用 Timer 为触发器控制 UpdatePanel 控件, UpdatePanel 控件更新通过使用局部更新. 由于使用了UpdatePanel因此必须包括 ScriptManager 对象在网页上.
注意现在不建议在网页上使用此类来做定时操作.建议使用Ajax的方式做局部更新.
2.2 Thread类
使用Thread类创建线程是最常见的方式,但也是最不好控制的方式.Thread类创建的线程模块不是后台线程,即: IsBackground属性为false.这个属性有什么用呢?当主线程结束时:如果IsBackground属性为true则线程自动结束,否则子线程将继续运行.
线程有优先级,默认情况下线程具有默认优先级,由操作系统来负责调度和分配.在某些情况下可以单独设置线程的优先级,以保证当前线程能优先执行.操作系统在调度线程的时候会根据优先级来选择优先级较高的线程优先执行.当线程不再占有CPU的时候就释放线程,比如线程在等待磁盘操作结果,等待网络IO结果等等.
如果线程不是主动释放CPU,那么线程调度器就会抢占该线程.如果线程有一个时间量,它就可以继续使用CPU.如果优先级相同的多个线程等待使用 CPU,线程调度器就会使用一个循环调度规则,将CPU逐个交给线程使用.如果线程被其他线程抢占,它就会排在队列的最后:只有优先级相同的多个线程在运行,才用得上时间量和循环规则.优先级是动态的. 如果线程是运算(CPU)密集型的 (一直需要 CPU,且不等待资源)其优先级就低于用该线程定义的基本优先级.如果线程在等待资源,随着优先级的提高,它的优先级就会增加.由于优先级的 提高,线程才有可能在下次等待结束时获得CPU.
在Thread类中,可以设置Priority属性,以影响线程的基本优先级Priority属性是一个ThreadPriority枚举定义的一个值. 定义的级别有Lowest,BelowNormal,Normal,AboveNormal,Highest = 4.
线程使用的例子:
classTestThread
{
Thread t1 =null;
Thread t2 =null;
publicvoid CreateThread()
{
Console.WriteLine("Startthread");
t1 = newThread(newThreadStart(() =>
{
Console.WriteLine("i amthread one id=" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("i amthread one fuck over");
}));
t1.IsBackground = true;
t2 = newThread(newParameterizedThreadStart((x) =>
{
Console.WriteLine("i amthread tow id=" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("i amfuck " + x.ToString());
Thread.Sleep(1000);
Console.WriteLine("i amthread tow fuck over");
}));
t1.Start();
t2.Start("gril");
}
}
输出结果:
这里有一点需要注意:线程参数的传入,上面的两个线程初始化的方法是不一样的.Thread类的构函数有好几个重载.
public Thread(ThreadStart start);
public Thread(ParameterizedThreadStart start);
这两个构函数是最常用的.第一个是调用无参的方法.要求方法返回一个void类型.第二个构造函数需要传入一个返回void类型的具有一个object类型的参数的方法.在调用的时候这个参数直接通过start的重载传入.
关于线程参数,还有一种方式,创建一个类将类中属性或者其他变量用来保存线程所需参数,在线程执行体中直接使用对应的变量.
2.3 线程的控制
调用Thread对 象的Start方法,可以创建线程.但是,在调用Start()法后,新线程仍不是马上处于Running状态,而是处于Unstarted状态. 直到线程调度器调用了该线程,然后才会处于Running状态.获取线程状态可以用ThreadState属性来获取.
使用Thread.Sleep()方法,会使线程处于停止(准确的说是WaitSleepJoin)状态,在等待Sleep方法指定的时间后线程就会等待再次被唤醒.(不建议使用Sleep放来让线程处于等待状态,这种其实没有释放CPU,同时也无法做更多的控制.)
要停止另一个线程,可以调用Abort方法. 调用这个方法时,会在接到终止命令的线程中抛出一个ThreadAbortException类型的异常. 用一个处理程序捕获这个异常,线程可以在结束前完成一些清理工作.如果需要等待线程的结束,就可以调用Join方法.Join方法会停止当前线程,并把它设置为WaitSleepJoin状态,直到加入的线程完成为止.
Abort方法特别说明:
1 当在一个线程上调用此方法时,系统会在其中抛出一个ThreadAbortException这是一个可以由应用程序代码捕获的特殊异常,但除非调用 ResetAbort,否则会在 catch 块的结尾再次引发它. ResetAbort 取消中止请求,并防止 ThreadAbortException 终止该线程. 未执行的 finally 块将在线程终止前执行.
2 如果正在中止的线程是在受保护的代码区域,如 catch 块、finally 块或受约束的执行区域,可能会阻止调用 Abort 的线程. 如果调用 Abort 的线程持有中止的线程所需的锁定,则会发生死锁.
3如果对尚未启动的线程调用 Abort,则当调用 Start 时该线程将中止. 如果对被阻止或正在休眠的线程调用 Abort,则该线程被中断然后中止.
4 如果在已挂起的线程上调用 Abort,则将在调用 Abort 的线程中引发 ThreadStateException,并将 AbortRequested 添加到被中止的线程的 ThreadState 属性中. 直到调用 Resume 后,才在挂起的线程中引发ThreadAbortException.
5如果在正在执行非托管代码的托管线程上调用 Abort,则直到线程返回到托管代码才引发 ThreadAbortException.
6如果同时出现两次对 Abort 的调用,则可能一个调用设置状态信息,而另一个调用执行 Abort. 但是,应用程序无法检测到此情况.
对线程调用了 Abort 后,线程状态包括 AbortRequested. 成功调用 Abort 而使线程终止后,线程状态更改为 Stopped. 如果有足够的权限,作为 Abort 目标的线程就可以使用 ResetAbort 方法取消中止操作.
特别说明:
线程中的代码如果要想保证在线程终止的时候得到执行,例如清理一些必要的内存等等操作那么必须将这些代码放到 finally块中;可以采用如下的写法:
classtestTry
{
publicvoid test()
{
Thread t =newThread(x =>
{
try
{
}
finally
{
Thread.Sleep(2000);
Console.WriteLine("executethread");
}
});
t.Start();
Thread.Sleep(500);
Console.WriteLine("startabort");
t.Abort();
Console.WriteLine("endabort");
}
}
输出结果:
这个代码没唯一比较特殊的地方就是try块中不包括任何逻辑.这样写的好处就是当线程被终止的时候finally块中的代码可以阻止调用Abort来终止线程直到finally块执行结束,线程才会终止.
上面的代码中如果没有这个try{}finally{},而直接执行那么结果将会如下:
用一个形象的比喻:当你作为一个杀手去杀人的时候,被杀的对象正在泡妞,当你正要杀他的时候他拿出了一个延迟死的金牌,他的要求等我干完了你在杀.这里杀手就是另一线程,被杀的人就是需要终止的线程,泡妞就是需要终止的线程正在干的事情.延迟死的金牌就是这个try{}finally{}块.
另外在使用线程的时候不建议使用直接使用Abort来结束线程.建议让线程内部的代码执行完毕后自然释放.
2.4 线程池
由于创建线程是一个很耗费资源和时间的操作,因此很有必要减少这种因为创建线程所浪费的资源.线程池ThreadPool类就是这样的一个工具. 这个类会在需要时增减池中线程的线程数,直到最大的线程数. 池中的最大线程数是可配置的.通常默认单核CPU最大线程数量设置为1023个(不同CPU的内核数量这个结果不一样,具体可以使用GetMaxThreads来获取)工作线程和 1000个I/O线程.也可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数.如果有更多的任务要处理,线程池中线程的个数也到了极限,最新的任务就要排队,且必须等待线程完成其任务.如果要自己做线程池一般建议初始化的线程一般为CPU内核数量*2+2.
线程池的使用很简单:
classTestThreadPool
{
publicvoid ThreadPoolTest()
{
int maxThread, ioMaxThread;
ThreadPool.GetMaxThreads(out maxThread, outioMaxThread);
Console.WriteLine("Workthread max count=" + maxThread + ",IO thread max count =" +ioMaxThread);
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(x=>
{
Console.WriteLine("thisis thread pool thread fuck my id=" + Thread.CurrentThread.ManagedThreadId);
});
}
}
}
执行结果:
从上面的输出中也可以看出有线程已经是重用了的.
线程池的使用直接调用ThreadPool的QueueUserWorkItem方法传入一个带有一个object参数的返回void的方法即可.
线程池的使用有一些限制:
l 线程池中的所有线程都是后台线程.如果进程的所有前台线程都结束了,所有的后台线程就会停止.不能把入池的线程改为前台线程.
l 不能给入池的线程设置优先级或名称.
l 对于COM对象,入池的所有线程都是多线程单元(Multi ThreadedApartments,MTA线程).许多CoM对象都需要单线程单元(Single Threaded Apartment,STA,线程.
l 入池的线程只能用于时间较短的任务. 如果线程要一直运行就应使用Thread类创建一个线程.
2.5 Task
Task类的表示单个操作不返回一个值,通常以异步方式执行. Task 对象的一个中心思想是基于任务的异步模式. 因为Task 对象的执行工作通常以异步方式在线程池线程上执行,而不是以同步方式在主应用程序线程上执行.可以使用Status属性,以IsCanceled,IsCompleted,和 IsFaulted 属性以确定任务的状态. 大多数情况下可以使用lambda 表达式用于指定具体执行任务内容.
Task的构造函数有多种重载.可以直接指定任务,但是使用Task类一般不直接调用构造函数来创建Task对象,而是调用TaskFactory.StartNew或者Task.Run(注意Run需要4.5以上的Framework才能够使用,4.0的版本中可以使用Start或者Task.Facorty.StartNew)的来创建.创建完成Task任务之后其实不是立即执行,在内部其实有一个队列排队调用线程池的线程来执行.如果需要Task返回执行结果那么可以使用Task<TResult>类来完成.
因为任务通常运行以异步方式在线程池线程上执行, 一旦该任务已实例化,创建并启动任务的线程将继续执行. 在某些情况下,当调用线程的主应用程序线程,在实际开始执行任务之前可能会终止任务. 其他情况下,应用程序的逻辑可能需要调用此线程继续执行直到一个或多个任务执行完毕. 您可以同步调用线程的执行,以及异步任务它启动通过调用 Wait 方法来等待要完成的一个或多个任务.这段话看起来有点拗口,简单来说就是第一创建Task的线程可能会终止task的任务同时主创线程可能需要等待task完成来获取其结果.第二就是其他线程需要与task来协同完成一个任务,那么这就需要等待所有线程同时完成.
若要等待完成一项任务,可以调用其 Task.Wait 方法.调用 Wait 方法将一直阻塞调用线程直到单一类实例都已完成执行.也可以使用Wait的其他重载,让Task有条件的等待.
classTestTask
{
publicvoid test()
{
Action<object> action = (object obj) =>
{
Console.WriteLine("Task={0},obj={1}, Thread={2}",
Task.CurrentId,obj,
Thread.CurrentThread.ManagedThreadId);
};
Task t1 =newTask(action,"alpha");
Task t2 =Task.Factory.StartNew(action,"beta");
t2.Wait();
t1.Start();
Console.WriteLine("t1 hasbeen launched. (Main Thread={0})",
Thread.CurrentThread.ManagedThreadId);
t1.Wait();
String taskData ="delta";
Task t3 =Task.Factory.StartNew(() =>
{
Console.WriteLine("Task={0},obj={1}, Thread={2}",
Task.CurrentId,taskData,
Thread.CurrentThread.ManagedThreadId);
});
t3.Wait();
Task t4 =newTask(action,"gamma");
t4.RunSynchronously();
t4.Wait();
}
}
输出结果:
Task可以执行连续任务,利用task的ContinueWith()方法.此方法有多个重载.注意在取消任务的时候连续任务也会跟着被取消.
classTestTaskContinue
{
publicvoid Test()
{
Task t =Task.Factory.StartNew(() =>
{
Console.WriteLine("task Astart id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskA execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t1 = t.ContinueWith(x =>
{
Console.WriteLine("taskt1 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt1 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t2 = t.ContinueWith(x =>
{
Console.WriteLine("taskt2 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt2 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t3 = t1.ContinueWith(x =>
{
Console.WriteLine("taskt3 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt3 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t4 = t3.ContinueWith(x =>
{
Console.WriteLine("taskt4 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt4 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
}
}
输出结果:
从这个结果可以看出其实t1和t2是并行执行的,t,t1/t2,t3,t4是串行执行的.
2.6 Parallel
此类的主要作用是提供并行循环运算和区域性的支持.此类只提供了三个方法For,Foreach和Invoke,这三个方法有多种重载.为集合等提供并行迭代.其内部实现是调用Task来实现.因此如果在有顺序要求的迭代中不能使用此方法.Invoke方法可以并行执行多个方法.
classTestParallel
{
publicvoid Test()
{
List<int> lst = newList<int>();
Parallel.For(0, 10, i =>
{
lst.Add(i);
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId);
});
Parallel.ForEach(lst, x =>
{
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId +" Value=" + x);
});
Parallel.Invoke(() =>
{
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId);
}, () =>
{
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId);
});
}
}
输出结果:
并行任务在执行的过程中其实是可以取消的.在For和foreach的部分重载中有一个ParallelOptions的参数,此参数可以指定循环.这点和Task的实现是一样的(内部其实就调用的Task来实现).
实例如下:
publicvoid TestCancel()
{
var cancel =newCancellationTokenSource();
cancel.Token.Register(()=>
{
Console.WriteLine("老子不干了...");
});
Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
cancel.Cancel();
});
try
{
var result =Parallel.For(1,10000,newParallelOptions() { CancellationToken = cancel.Token }, x =>
{
Thread.Sleep(30);
Console.WriteLine("干了" + x + "次" +" thread id =" +Thread.CurrentThread.ManagedThreadId);
});
}catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
输出结果:
这里有点需要注意的,就是当循环操作被取消之后会抛出一个”已取消该操作” TaskCanceledException的异常信息.可以直接try..catch来捕获即可.
2.7 volatile
关键字volatile申明的内容是告诉编译器这些内容是给多线程使用的. volatile关键字具有原子特性,所以线程间无法对其占有,它的值永远是最新的.
(以下为MSDN的定义)
l volatile 关键字指示一个字段可以由多个同时执行的线程修改. 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制. 这样可以确保该字段在任何时间呈现的都是最新的值.
l volatile 修饰符通常用于由多个线程访问但不使用 lock 语句对访问进行序列化的字段.
volatile 关键字可应用于以下类型的字段:
l 引用类型.
l 指针类型(在不安全的上下文中). 请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的. 换句话说,您无法声明“指向可变对象的指针”.
l 值类型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool.
l 具有以下基类型之一的枚举类型:byte、sbyte、short、ushort、int 或 uint.
l 已知为引用类型的泛型类型参数.
l IntPtr 和 UIntPtr.
l 可变关键字仅可应用于类或结构字段. 不能将局部变量声明为 volatile.
具体怎么说这个关键字呢,其实很简单这个关键字的主要用途就是保证多线程的情况下变量数据是最新的,因为在获取某个变量的值的时会先从缓存中获取,但这在多线程中这个变量的值可能被其他线程修改,但是当前线程得不到通知,因此当前线程从缓存中获取的值其实是修改之前的,加了这个关键字之后将不会在从缓存中获取.
2.8 线程同步
2.8.1 Interlocked 锁
此类为多个线程共享的变量提供原子操作.什么是原子操作呢?举个简单的例子,i++这个操作在il代码中其实还是经过了好几个步骤的.在这个过程中变量的值其实是可能被其他线程改变的.Interlocked可以保证这个操作在完成之前是有效的.
原子操作定义: 如果一个语句执行一个单独不可分割的指令,那么它是原子的.严格的原子操作排除了任何抢占的可能性,更方便的理解是这个值永远是最新的.其实要符合原子操作必须满足以下条件c#中如果是32位cpu的话,为一个少于等于32位字段赋值是原子操作,其他(自增,读,写操作)的则不是.对于64位cpu而言,操作32或64位的字段赋值都属于原子操作其他读写操作都不能属于原子操作.
此类的方法帮助保护免受计划程序切换上下文时某个线程正在更新可以由其他线程访问的变量或者在单独的处理器上同时执行两个线程就可能出现的错误. 此类的成员不会引发异常.
Increment和 Decrement 方法递增或递减的变量并将所得到的值存储在单个操作. 在大多数计算机上并递增一个变量不是一个原子操作,需要执行下列步骤:
1 实例变量的值加载到寄存器.
2 递增或递减值.
3 将值存储在实例变量.
如果不使用 Increment 和 Decrement,线程可以优先执行前两个步骤之后,另一个线程可以执行所有三个步骤. 在第一个线程继续执行时,它将覆盖实例变量中的值并且递增或递减执行的第二个线程的值将丢失.
Exchange方法以原子方式交换指定的变量的值. CompareExchange 方法组合了两个操作︰ 将两个值进行比较和存储第三个值中的某个变量,根据比较的结果. 作为一个原子操作执行比较和交换操作.
例子如下:
class TestInterlock
{
private static intusingResource = 0;
private const intnumThreadIterations = 5;
private const int numThreads= 10;
public void test()
{
Thread myThread;
Random rnd = newRandom();
for (int i = 0; i <numThreads; i++)
{
myThread = newThread(new ThreadStart(MyThreadProc));
myThread.Name =String.Format("Thread{0}", i + 1);
Thread.Sleep(rnd.Next(0, 1000));
myThread.Start();
}
}
private void MyThreadProc()
{
for (int i = 0; i <numThreadIterations; i++)
{
UseResource();
Thread.Sleep(1000);
}
}
bool UseResource()
{
if (0 ==Interlocked.Exchange(ref usingResource, 1))
{
Console.WriteLine("{0}acquired the lock", Thread.CurrentThread.Name);
Thread.Sleep(500);
Console.WriteLine("{0} exiting lock",Thread.CurrentThread.Name);
Interlocked.Exchange(ref usingResource, 0);
return true;
}
else
{
Console.WriteLine(" {0} wasdenied the lock", Thread.CurrentThread.Name);
return false;
}
}
}
输出结果:
从输出中可以看出其实在对变量usingResource进行赋值操作的时候其实也不是一个原子操作.内部还是经过了很多步骤,在这个步骤中其他线程是完全有机会去修改这个变量的值,从而导致错误.
2.8.2 Lock锁
Lock语句是在编程的过程中使用较多的一个同步工具类. Lock关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁.Lock关键字可以保证被保护的代码位于零界区内,不被其他线程干扰(其他线程无法进入该零界区).如果其他线程需要进入此零界区那么必须等待,直到当前线程退出零界区.
使用Lock关键字的时候应避免一下方式使用:
1 避免使用Lock(this)这样的方式.
this表示当前类的实例,如果锁定的类是共有类,当lock(this)锁定本身后其他外部类访问此实例的时候会遇到麻烦.同时this为当前对象的实例,如果遇到需要全局独占对象的锁定(比如打开串口,文件等等)那么这样写是毫无用处的.
2 避免使用lock(typeof(mytype))这类方式的锁定.
如果锁定类型mytype是共有类型那么整个类型将会被锁定.
3 避免使用lock(“aaa”)这类方式的锁定.
这样将会导致进程中所有使用同一字符的代码共享此锁.
4 锁定对象必须使用引用类型.
值类型的对象在锁定的时候有可能是真实对象的副本而不是真实对象,因此无法得到正确的保护.
在使用lock的时候建议使用 private 或者private static的方式定义对象来进行锁定.
注意在lock语句中无法使用await关键字.
下面是一个经常面试遇到的问题例子:
publicvoid test(int i)
{
lock (this)
{
if (i > 10)
{
Console.WriteLine(i);
i--;
test(i);
}
}
Console.WriteLine("Over" + i);
}
这个程序的输出结果如下:
这个程序的目的是采用递归的方式将传入的数据(当前传入的是18)依次减小到10.而且没有发生死锁,原因很简单传入的值是int类型的不是引用类型.另外这里还有一个需要注意的地方,就是在一个线程内不论怎么锁定这个方法都不会死锁(死锁是发生在跨线程共享资源的情况下).
privatestaticobject lck = newobject();
privateint count = 0;
publicvoid test()
{
Thread t1 =newThread(() =>
{
for (int i = 0; i< 5; i++)
add();
});
Thread t2 =newThread(() =>
{
for (int i = 0; i< 5; i++)
add();
});
Thread t3 =newThread(() =>
{
for (int i = 0; i< 5; i++)
add();
});
t1.Start();
t2.Start();
t3.Start();
}
privatevoid add()
{
lock (lck)
{
count++;
Console.WriteLine(count);
}
}
输出结果
左边这个结果是加锁的结果,右边这个是不加锁的结果.而且右边这个每次运行的结果都不一样的.
在使用lock的时候是需要考虑真实情况.对代码进行加锁是比较耗费资源和时间的.
在做一个线程安全的类的时候可以采用如下方法:
privateobject olck =newobject();
publicvoid add()
{
lock(olck)
{
Do…..
}
}
不建议在公共类中使用lock(this)来保证线程安全.
Lock语句在编译的时候会将lock语句编译成Monitor.Enter 和Monitor.exit的结构(其实就是Monitor的一个简写).
Monitor.Enter()
Try{
Do…
}
Finally{
Monitor.exit();
}
2.8.3 Monitor 锁
Monitor是采用零界区的方式来提供同步访问对象的机制. Monitor 类通过向单个线程授予对象锁来控制对象的访问. 对象锁提供限制访问零界区的能力. 当一个线程拥有对象的锁时,其他任何线程都不能获取该锁. 还可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码,除非另一个线程正在使用其他的锁定对象执行该代码.Monitor锁定的对象是引用类型而不是值类型(这点和lock一样,其实lock就是Monitor的简写).Monitor是一个静态类,无法创建实例.可以直接调用Monitor的Enter(或者重载)或者TryEnter(或者重载)方法进行加锁,通过exit的方法释放锁.Wait方法可以释放当前线程持有的锁,让线程进入等待状态.
使用Enter 和 Exit 方法标记临界区的开头和结尾. 如果临界区是一个连续指令集,则由 Enter 方法获取的锁将保证只有一个线程可以使用锁定对象执行所包含的代码. 在这种情况下,将这些指令放在 try块中,并将 Exit 指令放在 finally 块中. 此功能通常用于同步对类的静态或实例方法的访问. Enter 和 Exit 方法提供的功能与lock 语句提供的功能相同,区别在于lock 将 Enter(Object, Boolean) 方法重载和 Exit 方法封装在 try…finally块中以确保释放锁.
Monitor类有两组用于 Enter 和 TryEnter 方法的重载. 一组重载具有一个 ref Boolean 参数,在获取锁定时自动设置为 true,即使在获取锁定时引发了异常. 如果释放在所有实例中的锁定这点非常重要,即使在该锁定保护的资源的状态可能不一致时,也应该使用这些重载.
当选择要同步的对象时,应只锁定私有或内部对象. 锁定外部对象可能导致死锁,这是因为不相关的代码可能会出于不同的目的而选择锁定相同的对象.
例子:
classtestMonitor
{
publicvoid test()
{
List<Task> tasks = newList<Task>();
Random rnd =newRandom();
long total = 0;
int n = 0;
for (int taskCtr = 0; taskCtr < 10;taskCtr++)
tasks.Add(Task.Factory.StartNew(()=>
{
int[] values =newint[10000];
int taskTotal =0;
int taskN = 0;
int ctr = 0;
try
{
Monitor.Enter(rnd);
for (ctr = 0;ctr < 10000; ctr++)
values[ctr] = rnd.Next(0, 1001);
}
finally
{
Monitor.Exit(rnd);
}
taskN = ctr;
foreach (var value in values)
taskTotal +=value;
Console.WriteLine("task{0,2}total: {1:N2} (N={2:N0})",
Task.CurrentId,(taskTotal * 1.0) / taskN,
taskN);
Interlocked.Add(ref n, taskN);
Interlocked.Add(ref total,taskTotal);
}));
try
{
Task.WaitAll(tasks.ToArray());
Console.WriteLine("\nalltasks: {0:N2} (N={1:N0})",
(total * 1.0) / n, n);
}
catch (AggregateException e)
{
foreach (var ie ine.InnerExceptions)
Console.WriteLine("{0}:{1}", ie.GetType().Name, ie.Message);
}
}
}
输出结果:
2.8.4 SpinLock结构
SpinLock结构通常称为自旋锁结构. 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名.
MSDN定义:自旋锁提供一个相互排斥锁的基元,在该基元中,尝试获取锁的线程将在循环中检测并等待,直至该锁变为可用为止.
由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁.如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以.但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁.
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的.自旋锁只有在内核可抢占或SMP(对称式多处理器(Symmetric Multi-Processor),缩写为SMP,是一种计算机系统结构)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作.
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁.如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁.
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁.
C#中自旋锁可用于叶级锁定,此时在大小方面或由于垃圾回收压力,使用Monitor 所隐含的对象分配消耗过多.自旋锁非常有助于避免阻塞,但是如果预期有大量阻塞,由于旋转过多可能不应该使用自旋锁.当锁是细粒度的并且数量巨大(例如链接的列表中每个节点一个锁)时以及锁保持时间总是非常短时,旋转可能非常有帮助.通常,在保持一个旋锁时,应避免任何这些操作:
l 阻塞
l 调用本身可能阻塞的任何内容,
l 一次保持多个自旋锁,
l 进行动态调度的调用(接口和虚方法)
l 在某一方不拥有的任何代码中进行动态调度的调用,或分配内存.
SpinLock仅当您确定这样做可以改进应用程序的性能之后才能使用.另外,务必请注意 SpinLock 是一个值类型(出于性能原因).因此,您必须非常小心,不要意外复制了 SpinLock 实例,因为两个实例(原件和副本)之间完全独立,这可能会导致应用程序出现错误行为.如果必须传递 SpinLock 实例,则应该通过引用而不是通过值传递.
不要将SpinLock 实例存储在只读字段中.
例子:
public voidtest()
{
SpinLock sl = newSpinLock();
StringBuilder sb = newStringBuilder();
Action action = () =>
{
bool gotLock =false;
for (int i = 0; i< 10000; i++)
{
gotLock = false;
try
{
sl.Enter(refgotLock);
sb.Append((i% 10).ToString());
}
finally
{
if (gotLock)sl.Exit();
}
}
};
Parallel.Invoke(action,action, action);
Console.WriteLine("sb.Length = {0} (should be 30000)",sb.Length);
Console.WriteLine("number of occurrences of '5' in sb: {0} (shouldbe 3000)",
sb.ToString().Where(c => (c == '5')).Count());
}
输出结果:
上面这个例子中如果去掉自旋锁结果输出长度可能就不是3000了.(这个例子用lock等也可以实现)
SpinLock对于很长时间都不会持有共享资源上的锁的情况可能很有用. 对于此类情况,在多核计算机上,一种有效的做法是让已阻塞的线程旋转几个周期,直至锁被释放. 通过旋转,线程将不会进入阻塞状态(这是一个占用大量 CPU 资源的过程). 在某些情况下,SpinLock将会停止旋转,以防止出现逻辑处理器资源不足的现象,或出现系统上超线程的优先级反转的情况.
2.8.5 WaitHandle 锁
WaitHandle是一个抽象基类,用于等待一个信号的设置. 可以等待不同的信号,因 WaitHandle是一个基类,可以从中派生一些子类.从下图中也可以看出.
此类有一个重要的属性:SafeWaitHandle,此属性可以将一个本机句柄赋予一个操作系统对象.并等待该句柄.比如常见的I/O操作既可以指定一个SafeWaitHandle来等待I/O操作的完成.
2.8.6 Semaphore 锁
这个锁属于信号量类型的锁,是WaitHandle的子类.他的主要职责是协调各线程之间的关系,河里使用共享资源.举个简单例子,比如有一个餐馆里面有五张餐桌,可以同时供5个客人就餐.那么现在来了6个客人,这是老板会让前面的5个人客人先就餐,然最后到达的客人等待,直到前面的5位客人中有人离去.在这个过程中老板其实就是相当于一个信号量,客人其实就是线程,餐桌就是共享资源.
Semaphore信号量是不保证线程进入顺序的.线程的唤醒是带有一定随机性质.因此在有顺序的要求的请求中请谨慎考虑使用此类.
以下来自MSDN的解释(英文版自动翻译的结果,有些其实自动翻译是错误的):
使用 Semaphore 类来控制对资源池的访问. 线程进入信号量,通过调用 WaitOne 方法,继承自 WaitHandle 类,并通过调用释放信号量 Release 方法.上一个信号量计数会减少在每次一个线程进入信号量,并当一个线程释放信号量时递增. 当该计数为零时,后续请求阻止,直到其他线程释放信号量.如果所有线程都已都释放信号量,计数是最大值,是创建信号量的数.重复调用 WaitOne 方法可以让信号量进入多次. 若要释放部分或所有这些项,线程可以调用无参数 Release() 多次,也可以调用的方法重载 Release(Int32) 方法重载来指定要释放的项数.
Semaphore 类并不强制线程标识在调用 WaitOne 或 Release. 它是程序员有责任确保线程不释放信号量次数过多. 例如,假定信号量的最大计数为 2 并且线程 A 和线程 B 都进入了该信号量. 如果线程 B 中的编程错误导致它来调用 Release 两次,这两个调用都成功. 信号量计数已满,并且当线程A最终调用Release()将引发SemaphoreFullException异常.
信号量有两种类型︰ 本地信号量和已命名的系统信号量. 如果您创建 Semaphore 对象使用的构造函数接受一个名称,该名称的操作系统的信号量将与相关联. 已命名的系统信号量可以看到在整个操作系统,也可用于同步进程间的活动. 您可以创建多个 Semaphore 对象来表示同一个已命名系统信号量,并且你可以使用 OpenExisting 方法以打开一个现有的已命名系统信号量.
您的进程中仅存在了本地信号量. 它可以由具有对本地引用的过程中的任何线程使用 Semaphore 对象. 每个Semaphore 对象是单独的本地信号量.
Semaphore类有几个重要方法:
l 构造函数
构造函数中有几个重载,一般需要指定最大线程数量和初始化线程数量.
l WaitOne和重载
表示等待,无参的形式表示无限等待.有参数表示有条件的等待.
l Release方法
释放信号量同时返回上一次信号量.注意调用WaitOne的时候Semaphore会进行计数.Release方法会释放一次(带参数的表示可以释放参数指定的次数).
例子:
classTestSemaphore
{
privateSemaphore _pool;
privateint _padding;
publicvoid Test()
{
_pool = newSemaphore(0, 3);
for (int i = 1; i <= 5; i++)
{
Thread t =newThread(newParameterizedThreadStart(x =>
{
Console.WriteLine("Thread{0} begins and waits for the semaphore.", x);
_pool.WaitOne();
int padding =Interlocked.Add(ref _padding,100);
Console.WriteLine("Thread{0} enters the semaphore.", x);
Thread.Sleep(1000+ padding);
Console.WriteLine("Thread{0} releases the semaphore.", x);
Console.WriteLine("Thread{0} previous semaphore count: {1}",
x,_pool.Release());
}));
t.Start(i);
}
Thread.Sleep(500);
Console.WriteLine("Mainthread calls Release(3).");
_pool.Release(3);
Console.WriteLine("Mainthread exits.");
}
}
输出结果:
注意,此示例创建的是默认0个线程可以进入的信号量,因此程序一开始就5个线程就全部等待.然后主线程释放了3(此处最大为3个)个信号量,这样其他的线程就可以进入,然后线程执行完毕之后,会主动调用Release方法释放信号量,其他等待的两个线程便可进入.
注意:
1 如果Release方法引起 SemaphoreFullException 异常,不一定表示调用线程有问题.另一个线程中的编程错误可能导致该线程退出更多的计数,从而超过它进入的信号量最大值.
2 如果当前Semaphore 对象都表示一个已命名的系统信号量(用OpenExiting打开),用户必须具有 SemaphoreRights.Modify 权限和信号量必须具有已打开的 SemaphoreRights.Modify 权限.
2.8.7 mutex 锁
Mutex类是一个互斥体,可以提供跨进程的同步.mutex和Monitor有点相似,他们都是只能同时有一个线程拥有锁,能够访问同步代码.mutex可以让一个带有名称的锁成为进程间的互斥锁.在服务器端这里有点不好弄,如果mutex被标记为Global那么具有相同名称的锁将在服务器的所有终端共享,如果被标记为Local那么此锁仅仅是在服务端的本次会话中有效.默认情况为Local. Local和Global只针对服务器会话有效,在进程的作用域中不受影响.
Mutex最常见的用法就是限制进程重复启动.例如:
classtestMutex
{
publicvoid test()
{
bool createdNew =false;
Mutex mutex=newMutex(false,"aaaa",outcreatedNew);
if(createdNew)
{
Console.WriteLine("第一次启动");
}
else
{
Console.WriteLine("第二次启动");
}
}
}
上述代码所在的进程启动两次结果如下:
注意:mutex在加锁某一部分的代码之后需要明确的调用ReleaseMutex(),如果没有释放互斥快代码将一直被锁定.在使用完毕mutex之后需要释放此类,调用close方法,或者采用Using的结构让其自动调用释放.建议采用Using的方式.
2.8.8 Event 锁
注意这个Event不是事件定义的Event关键字,这个Event是只常用的四个用于同步的类. ManualResetEvent, AutoResetEvent, ManualResetEventSlim, CountdownEvent这几个类.
可以使用事件通知其他任务:这里有一些数据,并完成了 一些操作等.事件可以发信号,也可以不发信号.
2.8.8.1 ManualResetEvent:
调用 set()方法,等待对象发唤醒信号.调用 Reset()方法,可以使之返回不发信号的状态.如果多个线程等待向一个事件信号量发送信号,并调用了set()方法,就释放所有等待的线程.另外,如果一个线程刚刚调用了WaitOne()方法,但事件已经发出信号,等待的线程就可以继续等待.
一旦它被终止, ManualResetEvent 手动重置之前一直保持终止状态. 也就是说,调用 WaitOne 立即返回.可以控制的初始状态 ManualResetEvent 通过将一个布尔值传递给构造函数中, true 如果初始状态终止状态和 false 否则为.
2.8.8.2 AutoResetEvent:
线程通过调用 AutoResetEvent 上的 WaitOne 来等待信号. 如果AutoResetEvent 为未触发状态,则线程会被阻止,并等待当前控制资源的线程通过调用 Set 来通知资源可用.调用 Set 向 AutoResetEvent 发信号以释放等待线程. 当AutoResetEvent被设置为已触发状态时,它将一直保持已触发状态直到一个等待的线程被激活,然后它将自动变成未触发状态. 如果没有任何线程在等待,则状态将无限期地保持为已触发状态.当 AutoResetEvent为已触发状态时线程调用 WaitOne,则线程不会被阻止.AutoResetEvent 将立即释放线程并返回到未触发状态.但是,如果一个线程在等待自动重置的事件发信号,当第一个线程的等待状态结束时,该事件会自动变为不发信号的状态.这样,如果多个线程在等待向事件发信号,就只有一个线程结束其等待状态,它不是等待时间最长的线程,而是优先级最高的线程.
注意: Set() 方法不是每次调用都释放线程. 如果两次调用十分接近,以致在线程释放之前便已发生第二次调用,则只释放一个线程.就像第二次调用并未发生一样. 另外,如果在调用 Set 时不存在等待的线程且 AutoResetEvent 已终止,则该调用无效.也就是说多次调用set()不会产生副作用.
2.8.8.3 ManualResetEventSlim:
此类其实是ManualResetEvent的一个轻量级版本.在功能和使用上面基本上和ManualResetEvent一样.但在性能上比ManualResetEvent要好,适合于那些等待时间较短的同步.注意ManualResetEventSlim此类在等待时间较短的情况下采用的是旋转的方式(在较短的等待中旋转比等待句柄开销要小的多),在等待时间较长的情况下采用的是等待句柄的方式.
2.8.8.4 AutoResetEvent和ManualResetEvent区别
AutoResetEvent和ManualResetEvent的区别在于当AutoResetEvent调用Set()之后会自动重置调用Reset()方法.而ManualResetEvent方法在调用了Set()之后需要手动调用Reset()方法否则只要在ManualResetEvent上调用WaitOne()方法将立即返回.
例子:
classtestAutoEvent
{
AutoResetEvent autoEvent =newAutoResetEvent(false);
List<string> que = newList<string>();
int count = 0;
publicvoid test()
{
Task.Factory.StartNew(() =>
{
while (count <100)
{
if(que.Count() > 0)
{
lock (que)
{
foreach (var xin que)
{
Console.WriteLine("removestring :" + x);
}
que.Clear();
}
count++;
}
else
{
autoEvent.WaitOne();
}
}
});
Task.Factory.StartNew(() =>
{
Random r =newRandom();
for (int i = 0; i< 500; i++)
{
if(r.Next(100) < 50)
{
autoEvent.Set();
}
Thread.Sleep(100);
lock (que)
{
que.Add(i.ToString());
Console.WriteLine("addstring:" + i.ToString());
}
}
});
}
}
结果:
这个例子有点类似生产者和消费者的例子.
2.8.8.5 CountdownEvent
表示在计数变为零时会得到信号通知的同步基元.它在收到一定次数的信号之后,将会解除对其等待线程的锁定. CountdownEvent 专门用于以下情况:您必须使用 ManualResetEvent 或 ManualResetEventSlim,并且必须在用信号通知事件之前手动递减一个变量. 例如,在分叉/联接方案中,您可以只创建一个信号计数为 5 的CountdownEvent,然后在线程池上启动五个工作项,并且让每个工作项在完成时调用 Signal. 每次调用 Signal 时,信号计数都会递减 1. 在主线程上,对 Wait 的调用将会阻塞,直至信号计数为零.
例子:
classtestCountdownEvent
{
publicvoid test()
{
CountdownEvent cdevent =newCountdownEvent(4);
Action<object> p = newAction<object>(i =>
{
var c = (int)i;
Thread.Sleep(c *1000);
cdevent.Signal(1);
Console.WriteLine(string.Format("threadid={0} wait {1} over CurrentCount={2}"
, Thread.CurrentThread.ManagedThreadId,c, cdevent.CurrentCount));
});
for (int i = 0; i < 4; i++)
{
Task.Factory.StartNew(p,i + 1);
}
Console.WriteLine("waitall thread over initcount="
+cdevent.InitialCount +",CurrentCount="
+cdevent.CurrentCount +",IsSet="
+ cdevent.IsSet);
cdevent.Wait();
Console.WriteLine("allthread over initcount="
+ cdevent.InitialCount+",CurrentCount="
+cdevent.CurrentCount +",IsSet="
+ cdevent.IsSet);
cdevent.Dispose();
}
}
输出结果:
注意此类的Wait(CancellationTokencancellationToken)方法的重载中是可以传入一个取消等待的对象.
2.8.9 ReaderWriterLock/ReaderWriterLockSlim锁
这两个类都是用于定义一个写和多个读的加锁方式.这两个类都适用于对一个不经常发生变化的资源进行加锁.在性能上使用这两个类加锁一个不经常变化的资源比直接使用monitor,mutex等要高出很多.ReaderWriterLockSlim在性能上比ReaderWriterLock要高,他简化了递归,升级和降级的锁定规则,同时可以避免潜在的死锁.因此在实际的项目中建议使用ReaderWriterLockSlim类.下面就只介绍ReaderWriterLockSlim类.
ReaderWriterLockSlim类可以利用EnterReadLock, TryEnterReadLock, EnterWriteLock和TryEnterWriteLock获取当前是读写的锁.还可以使用 EnterUpgradeableReadLock或者TryEnterUpgradeableReadLock函数类进行先读后写的操作(MSDN中把这个叫做升级或者降级,这点比较复杂不做深入说明,喜欢的朋友可以自己去MSDN看).这个函数可以让读锁升级,从而不需要释放读锁.注意在调用了Entry…这类方法之后必须调用对应的exit的方法,例如调用了EnterReadLock之后需要调用ExitReadLock;调用了EnterWriteLock之后需要调用ExitWriteLock方法来释放锁.这个读写锁在读的情况比写的情况多很多的情况下比lock性能要高.
例子(这个例子代码有点多):
classtestReadWriteLockSlim
{
privateList<string> lst = newList<string>();
privateReaderWriterLockSlim lckSlim =newReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
publicvoid test()
{
Task[] t =newTask[6];
t[0] = Task.Factory.StartNew(()=> { Write(); });
t[1] = Task.Factory.StartNew(()=> { Read(); });
t[2] = Task.Factory.StartNew(()=> { Read(); });
t[3] = Task.Factory.StartNew(()=> { Write(); });
t[4] = Task.Factory.StartNew(()=> { Read(); });
t[5] = Task.Factory.StartNew(() => {Read(); });
Task.WaitAll(t);
lckSlim.Dispose();
}
privatevoid Read()
{
try
{
lckSlim.EnterReadLock();
foreach (var x in lst)
{
Console.WriteLine("读取到:" + x);
}
}
finally
{
lckSlim.ExitReadLock();
}
}
privatevoid Write()
{
try
{
while(!lckSlim.TryEnterWriteLock(-1))
{
Console.WriteLine(string.Format("其他线程(id={0})正在艹,读就排队等排队等待艹的数量:{1}"
, Thread.CurrentThread.ManagedThreadId,lckSlim.WaitingWriteCount));
Console.WriteLine("等待读的排队数量:" + lckSlim.WaitingReadCount);
}
Console.WriteLine(string.Format("当前线程{0}获取到写锁:",Thread.CurrentThread.ManagedThreadId));
for (var r = 0; r< 5; r++)
{
lst.Add("艹你妹 " + r + "次");
Console.WriteLine(string.Format("线程{0}正在操第{1}次",Thread.CurrentThread.ManagedThreadId,r));
Thread.Sleep(50);
}
}
finally
{
Console.WriteLine(string.Format("当前线程{0}干完收工:",Thread.CurrentThread.ManagedThreadId));
lckSlim.ExitWriteLock();
}
}
}
输出结果:
注意:此类中的从读模式升级的时候是一个复杂的过程,我没搞明白就不在多说.
2.8.10 Barrier 锁
使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作.这个是什么意思呢,就是说比如abcd四个任务分别有1,2,3这个三个阶段来.每个任务必须等到其他任务完成当前阶段的之后才能够继续下一阶段的任务.
例子如下:
classtestBarrier
{
Barrier _Barrier =newBarrier(11);
publicvoid test()
{
Task[] _Tasks =newTask[4];
_Barrier = newBarrier(_Tasks.Count(),(barrier) =>
{
Console.WriteLine("=================Over" + barrier.CurrentPhaseNumber);
});
for (var r = 0; r < _Tasks.Length;r++)
{
_Tasks[r] = Task.Factory.StartNew(()=>
{
DoA();
_Barrier.SignalAndWait();
DoB();
_Barrier.SignalAndWait();
DoC();
_Barrier.SignalAndWait();
});
}
var finalTask =Task.Factory.ContinueWhenAll(_Tasks,(tasks) =>
{
Task.WaitAll(_Tasks);
_Barrier.Dispose();
});
finalTask.Wait();
Console.WriteLine("===============Overall");
}
void DoA()
{
Console.WriteLine("firstA ");
Thread.Sleep(50);
}
void DoB()
{
Console.WriteLine("secondB ");
Thread.Sleep(50);
}
void DoC()
{
Console.WriteLine("threeC ");
Thread.Sleep(50);
}
}
输出结果:
注意在调用Barrier类的时候必须指定参与线程的数量.
在使用Barrier类的时候有一个稍微不好弄的就是异常处理. 如果进入屏障后,工作的代码出现了异常,这个异常会被放在BarrierPostPhaseException中,而且所有任务都能够捕捉到这个异常.原始的异常可以通过NarrierPostPhaseException 对象的InnerException进行访问.
2.9 线程总结
线程在实际的开发中使用相当的多.在这个过程中使用锁的时候也非常的多.在选则使用线程的时候可以优先考虑使用Task提供功能来完成(微软Framework自身的代码中很多多线程的功能都是调用的Task来完成).其次是直接使用线程池.至于最简单的Timer其实是最不可靠的(可能是我不太会使用).
线程同步稍微复杂一点.这个复杂的表现有几个方面:1同步的时会影响性能.这点在加锁的时候就需要考虑在不通的环境使用不通的锁.在没有必要加锁的地方就不要使用锁.2 同步中容易发生死锁.这个问题一旦发现而且不容易被发现.有些时候代码跑几天都不出现问题.因此查找问题的难度增加.一旦出现这样的问题请先梳理整个流程.检查有没有互相挣锁的情况.第二可以写日志来查找.在使用锁的时候尽量的小粒度加锁.
文章pdf下载地址:
http://download.csdn.net/detail/shibinysy/9712624