一、基本概念
进程:当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。
线程:线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
静态属性:这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。
二、多线程的优劣
优点:可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
缺点:线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要CPU时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
线程太多会导致控制太复杂,最终可能造成很多Bug;
三、控制线程的类和方法
类:usingSystem.Threading; Thread类
Thread类常用属性:
Name:线程的名称
CurrentThread:当前线程
Priority:线程优先级。在C#应用程序中,用户可以设定5个不同的优先级,由高到低分别是 Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为ThreadPriority.Normal。
ThreadState:当前线程状态。这个属性代表了线程运行时状态,在不同的情况下有不同的值,我们有时候可以通过对该值的判断来设计程序流程。C# ThreadState属性的取值如下:
◆Aborted:线程已停止;
◆AbortRequested:线程的Thread.Abort()方法已被调用,但是线程还未停止;
◆Background:线程在后台执行,与属性Thread.IsBackground有关;
◆Running:线程正在正常运行;
◆Stopped:线程已经被停止;
◆StopRequested:线程正在被要求停止;
◆Suspended:线程已经被挂起(此状态下,可以通过调用Resume()方法重新运行);
◆SuspendRequested:线程正在要求被挂起,但是未来得及响应;
◆Unstarted:未调用Thread.Start()开始线程的运行;
◆WaitSleepJoin:线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态;
上面提到了Background状态表示该线程在后台运行,那么后台运行的线程有什么特别的地方呢?其实后台线程跟前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将通过调用任意一个存活中的后台进程的Abort()方法来彻底终止进程。
Thread类常用方法:
Start():开始执行线程
Suspend():挂起线程
Resume():恢复被挂起的线程
Sleep():暂停线程
Interrupt():中断线程
Join():阻塞调用线程,直到某个线程终止时为止;一个阻塞调用,直到线程的确是终止了才返回。
Abort():中止线程
创建一个线程一般包含三个步骤:
1、创建入口函数
2、创建入口委托
3、创建线程
四、线程同步
多个线程的同步技术:
lock语句
Interlocaked类
Monitor类
等待句柄
Mutex类
Semaphore类
Event类
ReaderWriterLockSlim
lock语句、Interlocaked类、Monitor类可用于进程内部的同步。Mutex类、Semaphore类、Event类、ReaderWriterLockSlim类提供了多个进程中的线程同步。
.NET Framework的CLR提供了三种方法来完成对共享资源,诸如全局变量域,特定的代码段,静态的和实例化的方法和域。
(1) 代码域同步:使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段。不支持静态域的同步。在实例化的方法中,this指针用于同步;而在静态的方法中,类用于同步,这在后面会讲到。
(2) 手工同步:使用不同的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent,AutoResetEvent 和Interlocked等)创建自己的同步机制。这种同步方式要求你自己手动的为不同的域和方法同步,这种同步方式也可以用于进程间的同步和对共享资源的等待而造成的死锁解除。
(3) 上下文同步:使用SynchronizationAttribute为ContextBoundObject对象创建简单的,自动的同步。这种同步方式仅用于实例化的方法和域的同步。所有在同一个上下文域的对象共享同一个锁。
Monitor Class
在给定的时间和指定的代码段只能被一个线程访问,Monitor 类非常适合于这种情况的线程同步。这个类中的方法都是静态的,所以不需要实例化这个类。下面一些静态的方法提供了一种机制用来同步对象的访问从而避免死锁和维护数据的一致性。
Monitor.Enter方法:在指定对象上获取排他锁。
Monitor.TryEnter方法:试图获取指定对象的排他锁。
Monitor.Exit方法:释放指定对象上的排他锁。
Monitor.Wait方法:释放对象上的锁并阻塞当前线程,直到它重新获取该锁。
Monitor.Pulse方法:通知等待队列中的线程锁定对象状态的更改。
Monitor.PulseAll方法:通知所有的等待线程对象状态的更改。
通过对指定对象的加锁和解锁可以同步代码段的访问。Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用来对指定对象的加锁和解锁。一旦获取(调用了Monitor.Enter)指定对象(代码段)的锁,其他的线程都不能获取该锁。举个例子来说吧,线程X获得了一个对象锁,这个对象锁可以释放的(调用Monitor.Exit(object) or Monitor.Wait)。当这个对象锁被释放后,Monitor.Pulse方法和 Monitor.PulseAll方法通知就绪队列的下一个线程进行和其他所有就绪队列的线程将有机会获取排他锁。线程X释放了锁而线程Y获得了锁,同时调用Monitor.Wait的线程X进入等待队列。当从当前锁定对象的线程(线程Y)受到了Pulse或PulseAll,等待队列的线程就进入就绪队列。线程X重新得到对象锁时,Monitor.Wait才返回。如果拥有锁的线程(线程Y)不调用Pulse或PulseAll,方法可能被不确定的锁定。Pulse, PulseAll andWait必须是被同步的代码段被调用。对每一个同步的对象,你需要有当前拥有锁的线程的指针,就绪队列和等待队列(包含需要被通知锁定对象的状态变化的线程)的指针。
你也许会问,当两个线程同时调用Monitor.Enter会发生什么事情?无论这两个线程地调用Monitor.Enter是多么地接近,实际上肯定有一个在前,一个在后,因此永远只会有一个获得对象锁。既然Monitor.Enter是原子操作,那么CPU是不可能偏好一个线程而不喜欢另外一个线程的。为了获取更好的性能,你应该延迟后一个线程的获取锁调用和立即释放前一个线程的对象锁。对于private和internal的对象,加锁是可行的,但是对于external对象有可能导致死锁,因为不相关的代码可能因为不同的目的而对同一个对象加锁。
如果你要对一段代码加锁,最好的是在try语句里面加入设置锁的语句,而将Monitor.Exit放在finally语句里面。对于整个代码段的加锁,你可以使用MethodImplAttribute(在System.Runtime.CompilerServices命名空间)类在其构造器中设置同步值。这是一种可以替代的方法,当加锁的方法返回时,锁也就被释放了。如果需要要很快释放锁,你可以使用Monitor类和C# lock的声明代替上述的方法。
让我们来看一段使用Monitor类的代码:
public void some_method()
{
int a=100;
int b=0;
Monitor.Enter(this);
//say we do something here.
int c=a/b;
Monitor.Exit(this);
}
上面的代码运行会产生问题。当代码运行到int c=a/b; 的时候,会抛出一个异常,Monitor.Exit将不会返回。因此这段程序将挂起,其他的线程也将得不到锁。有两种方法可以解决上面的问题。第一个方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后一定会释放锁。第二种方法是:利用C#的lock()方法。调用这个方法和调用Monitoy.Enter的作用效果是一样的。但是这种方法一旦代码执行超出范围,释放锁将不会自动的发生。见下面的代码:
public void some_method()
{
int a=100;
int b=0;
lock(this);
//say we do something here.
int c=a/b;
}
C# lock申明提供了与Monitoy.Enter和Monitoy.Exit同样的功能,这种方法用在你的代码段不能被其他独立的线程中断的情况。
WaitHandle Class
WaitHandle类作为基类来使用的,它允许多个等待操作。这个类封装了win32的同步处理方法。WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。下面是从它继承来的几个类:
Mutex 类:同步基元也可用于进程间同步。
AutoResetEvent:通知一个或多个正在等待的线程已发生事件。无法继承此类。
ManualResetEvent:当通知一个或多个正在等待的线程事件已发生时出现。无法继承此类。
这些类定义了一些信号机制使得对资源排他性访问的占有和释放。他们有两种状态:signaled 和 nonsignaled。Signaled状态的等待句柄不属于任何线程,除非是nonsignaled状态。拥有等待句柄的线程不再使用等待句柄时用set方法,其他的线程可以调用Reset方法来改变状态或者任意一个WaitHandle方法要求拥有等待句柄,这些方法见下面:
WaitAll:等待指定数组中的所有元素收到信号。
WaitAny:等待指定数组中的任一元素收到信号。
WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。
这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。
WaitHandle对象封装等待对共享资源的独占访问权的操作系统特定的对象无论是收管代码还是非受管代码都可以使用。但是它没有Monitor使用轻便,Monitor是完全的受管代码而且对操作系统资源的使用非常有效率。
Mutex Class
Mutex是另外一种完成线程间和跨进程同步的方法,它同时也提供进程间的同步。它允许一个线程独占共享资源的同时阻止其他线程和进程的访问。Mutex的名字就很好的说明了它的所有者对资源的排他性的占有。一旦一个线程拥有了Mutex,想得到Mutex的其他线程都将挂起直到占有线程释放它。Mutex.ReleaseMutex方法用于释放Mutex,一个线程可以多次调用wait方法来请求同一个Mutex,但是在释放Mutex的时候必须调用同样次数的Mutex.ReleaseMutex。如果没有线程占有Mutex,那么Mutex的状态就变为signaled,否则为nosignaled。一旦Mutex的状态变为signaled,等待队列的下一个线程将会得到Mutex。Mutex类对应与win32的CreateMutex,创建Mutex对象的方法非常简单,常用的有下面几种方法:
一个线程可以通过调用WaitHandle.WaitOne或WaitHandle.WaitAny 或 WaitHandle.WaitAll得到Mutex的拥有权。如果Mutex不属于任何线程,上述调用将使得线程拥有Mutex,而且WaitOne会立即返回。但是如果有其他的线程拥有Mutex,WaitOne将陷入无限期的等待直到获取Mutex。你可以在WaitOne方法中指定参数即等待的时间而避免无限期的等待Mutex。调用Close作用于Mutex将释放拥有。一旦Mutex被创建,你可以通过GetHandle方法获得Mutex的句柄而给WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。
下面是一个示例:
public void some_method()
{
int a=100;
int b=20;
Mutex firstMutex = new Mutex(false);
FirstMutex.WaitOne();
//some kind of processing can be done here.
Int x=a/b;
FirstMutex.Close();
}
在上面的例子中,线程创建了Mutex,但是开始并没有申明拥有它,通过调用WaitOne方法拥有Mutex。
Synchronization Events
同步时间是一些等待句柄用来通知其他的线程发生了什么事情和资源是可用的。他们有两个状态:signaled and nonsignaled。AutoResetEvent 和 ManualResetEvent就是这种同步事件。
AutoResetEvent Class
这个类可以通知一个或多个线程发生事件。当一个等待线程得到释放时,它将状态转换为signaled。用set方法使它的实例状态变为signaled。但是一旦等待的线程被通知时间变为signaled,它的转台将自动的变为nonsignaled。如果没有线程侦听事件,转台将保持为signaled。此类不能被继承。
ManualResetEvent Class
这个类也用来通知一个或多个线程事件发生了。它的状态可以手动的被设置和重置。手动重置时间将保持signaled状态直到ManualResetEvent.Reset设置其状态为nonsignaled,或保持状态为nonsignaled直到ManualResetEvent.Set设置其状态为signaled。这个类不能被继承。
Interlocked Class
它提供了在线程之间共享的变量访问的同步,它的操作时原子操作,且被线程共享.你可以通过Interlocked.Increment或Interlocked.Decrement来增加或减少共享变量.它的有点在于是原子操作,也就是说这些方法可以代一个整型的参数增量并且返回新的值,所有的操作就是一步.你也可以使用它来指定变量的值或者检查两个变量是否相等,如果相等,将用指定的值代替其中一个变量的值.
ReaderWriterLock class
它定义了一种锁,提供唯一写/多读的机制,使得读写的同步.任意数目的线程都可以读数据,数据锁在有线程更新数据时将是需要的.读的线程可以获取锁,当且仅当这里没有写的线程.当没有读线程和其他的写线程时,写线程可以得到锁.因此,一旦writer-lock被请求,所有的读线程将不能读取数据直到写线程访问完毕.它支持暂停而避免死锁.它也支持嵌套的读/写锁.支持嵌套的读锁的方法是ReaderWriterLock.AcquireReaderLock,如果一个线程有写锁则该线程将暂停;
支持嵌套的写锁的方法是ReaderWriterLock.AcquireWriterLock,如果一个线程有读锁则该线程暂停.如果有读锁将容易倒是死锁.安全的办法是使用ReaderWriterLock.UpgradeToWriterLock方法,这将使读者升级到写者.你可以用ReaderWriterLock.DowngradeFromWriterLock方法使写者降级为读者.调用ReaderWriterLock.ReleaseLock将释放锁, ReaderWriterLock.RestoreLock将重新装载锁的状态到调用ReaderWriterLock.ReleaseLock以前。
五、线程池和定时器——多线程的自动管理
在多线程的程序中,经常会出现两种情况。一种情况下,应用程序中的线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应;而另外一种情况则是线程平常都处于休眠状态,只是周期性地被唤醒。在.net framework里边,我们使用ThreadPool来对付第一种情况,使用Timer来对付第二种情况。
ThreadPool类提供一个由系统维护的线程池——可以看作一个线程的容器,该容器需要Windows 2000以上版本的系统支持,因为其中某些方法调用了只有高版本的Windows才有的API函数。你可以使用ThreadPool.QueueUserWorkItem()方法将线程安放在线程池里,该方法的原型如下:
//将一个线程放进线程池,该线程的Start()方法将调用WaitCallback代理对象代表的函数
publicstatic bool QueueUserWorkItem(WaitCallback);
//重载的方法如下,参数object将传递给WaitCallback所代表的方法
publicstatic bool QueueUserWorkItem(WaitCallback, object);
要注意的是,ThreadPool类也是一个静态类,你不能也不必要生成它的对象,而且一旦使用该方法在线程池中添加了一个项目,那么该项目将是没有办法取消的。在这里你无需自己建立线程,只需把你要做的工作写成函数,然后作为参数传递给ThreadPool.QueueUserWorkItem()方法就行了,传递的方法就是依靠WaitCallback代理对象,而线程的建立、管理、运行等等工作都是由系统自动完成的,你无须考虑那些复杂的细节问题,线程池的优点也就在这里体现出来了,就好像你是公司老板——只需要安排工作,而不必亲自动手。
下面的例程演示了ThreadPool的用法。首先程序创建了一个ManualResetEvent对象,该对象就像一个信号灯,可以利用它的信号来通知其它线程,本例中当线程池中所有线程工作都完成以后,ManualResetEvent的对象将被设置为有信号,从而通知主线程继续运行。它有几个重要的方法:Reset(),Set(),WaitOne()。初始化该对象时,用户可以指定其默认的状态(有信号/无信号),在初始化以后,该对象将保持原来的状态不变直到它的Reset()或者Set()方法被调用,Reset()方法将其设置为无信号状态,Set()方法将其设置为有信号状态。WaitOne()方法使当前线程挂起直到ManualResetEvent对象处于有信号状态,此时该线程将被激活。然后,程序将向线程池中添加工作项,这些以函数形式提供的工作项被系统用来初始化自动建立的线程。当所有的线程都运行完了以后,ManualResetEvent.Set()方法被调用,因为调用了ManualResetEvent.WaitOne()方法而处在等待状态的主线程将接收到这个信号,于是它接着往下执行,完成后边的工作。
usingSystem;
usingSystem.Collections;
usingSystem.Threading;
//这是用来保存信息的数据结构,将作为参数被传递
public classSomeState
{
public intCookie;
publicSomeState(int iCookie)
{
Cookie =iCookie;
}
}
public classAlpha
{
publicHashtable HashCount;
publicManualResetEvent eventX;
publicstatic int iCount = 0;
publicstatic int iMaxCount = 0;
publicAlpha(int MaxCount)
{
HashCount= new Hashtable(MaxCount);
iMaxCount= MaxCount;
}
file://线程池里的线程将调用Beta()方法
public voidBeta(Object state)
{
//输出当前线程的hash编码值和Cookie的值
Console.WriteLine("{0} {1} :", Thread.CurrentThread.GetHashCode(),
((SomeState)state).Cookie);
Console.WriteLine("HashCount.Count=={0},Thread.CurrentThread.GetHashCode()=={1}", HashCount.Count,Thread.CurrentThread.GetHashCode());
lock(HashCount)
{
file://如果当前的Hash表中没有当前线程的Hash值,则添加之
if(!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
HashCount.Add(Thread.CurrentThread.GetHashCode(), 0);
HashCount[Thread.CurrentThread.GetHashCode()]=
((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
}
int iX =2000;
Thread.Sleep(iX);
//Interlocked.Increment()操作是一个原子操作,具体请看下面说明
Interlocked.Increment(refiCount);
if (iCount== iMaxCount)
{
Console.WriteLine();
Console.WriteLine("SettingeventX ");
eventX.Set();
}
}
}
public classSimplePool
{
publicstatic int Main(string[] args)
{
Console.WriteLine("ThreadPool Sample:");
bool W2K =false;
intMaxCount = 10;//允许线程池中运行最多10个线程
//新建ManualResetEvent对象并且初始化为无信号状态
ManualResetEventeventX = new ManualResetEvent(false);
Console.WriteLine("Queuing{0} items to Thread Pool", MaxCount);
AlphaoAlpha = new Alpha(MaxCount); file://创建工作项
//注意初始化oAlpha对象的eventX属性
oAlpha.eventX= eventX;
Console.WriteLine("Queueto Thread Pool 0");
try
{
file://将工作项装入线程池
file://这里要用到Windows 2000以上版本才有的API,所以可能出现NotSupportException异常
ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),
newSomeState(0));
W2K =true;
}
catch(NotSupportedException)
{
Console.WriteLine("TheseAPI's may fail when called on a non-Windows 2000 system.");
W2K =false;
}
if (W2K)//如果当前系统支持ThreadPool的方法.
{
for (intiItem=1;iItem < MaxCount;iItem++)
{
//插入队列元素
Console.WriteLine("Queueto Thread Pool {0}", iItem);
ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),new SomeState(iItem));
}
Console.WriteLine("Waitingfor Thread Pool to drain");
file://等待事件的完成,即线程调用ManualResetEvent.Set()方法
eventX.WaitOne(Timeout.Infinite,true);
file://WaitOne()方法使调用它的线程等待直到eventX.Set()方法被调用
Console.WriteLine("ThreadPool has been drained (Event fired)");
Console.WriteLine();
Console.WriteLine("Loadacross threads");
foreach(objecto in oAlpha.HashCount.Keys)
Console.WriteLine("{0}{1}", o, oAlpha.HashCount[o]);
}
Console.ReadLine();
return 0;
}
}
程序中有些小地方应该引起我们的注意。SomeState类是一个保存信息的数据结构,在上面的程序中,它作为参数被传递给每一个线程,你很容易就能理解这个,因为你需要把一些有用的信息封装起来提供给线程,而这种方式是非常有效的。程序出现的InterLocked类也是专为多线程程序而存在的,它提供了一些有用的原子操作,所谓原子操作就是在多线程程序中,如果这个线程调用这个操作修改一个变量,那么其他线程就不能修改这个变量了,这跟lock关键字在本质上是一样的。
我们应该彻底地分析上面的程序,把握住线程池的本质,理解它存在的意义是什么,这样我们才能得心应手地使用它。下面是该程序的输出结果:
Thread PoolSample:
Queuing 10items to Thread Pool
Queue toThread Pool 0
Queue toThread Pool 1
...
...
Queue toThread Pool 9
Waiting forThread Pool to drain
98 0 :
HashCount.Count==0,Thread.CurrentThread.GetHashCode()==98
100 1 :
HashCount.Count==1,Thread.CurrentThread.GetHashCode()==100
98 2 :
...
...
SettingeventX
Thread Poolhas been drained (Event fired)
Load acrossthreads
101 2
100 3
98 4
102 1
与ThreadPool类不同,Timer类的作用是设置一个定时器,定时执行用户指定的函数,而这个函数的传递是靠另外一个代理对象TimerCallback,它必须在创建Timer对象时就指定,并且不能更改。定时器启动后,系统将自动建立一个新的线程,并且在这个线程里执行用户指定的函数。下面的语句初始化了一个Timer对象:
Timer timer= new Timer(timerDelegate, s,1000, 1000);
第一个参数指定了TimerCallback代理对象;第二个参数的意义跟上面提到的WaitCallback代理对象的一样,作为一个传递数据的对象传递给要调用的方法;第三个参数是延迟时间——计时开始的时刻距现在的时间,单位是毫秒;第四个参数是定时器的时间间隔——计时开始以后,每隔这么长的一段时间,TimerCallback所代表的方法将被调用一次,单位也是毫秒。这句话的意思就是将定时器的延迟时间和时间间隔都设为1秒钟。
定时器的设置是可以改变的,只要调用Timer.Change()方法,这是一个参数类型重载的方法,一般使用的原型如下:
public boolChange(long, long);
下面这段代码将前边设置的定时器修改了一下:
timer.Change(10000,2000);
很显然,定时器timer的时间间隔被重新设置为2秒,停止计时10秒后生效。
下面这段程序演示了Timer类的用法。
usingSystem;
usingSystem.Threading;
classTimerExampleState
{
public intcounter = 0;
public Timertmr;
}
class App
{
publicstatic void Main()
{
TimerExampleStates = new TimerExampleState();
//创建代理对象TimerCallback,该代理将被定时调用
TimerCallbacktimerDelegate = new TimerCallback(CheckStatus);
//创建一个时间间隔为1s的定时器
Timertimer = new Timer(timerDelegate, s,1000, 1000);
s.tmr =timer;
//主线程停下来等待Timer对象的终止
while(s.tmr!= null)
Thread.Sleep(0);
Console.WriteLine("Timerexample done.");
Console.ReadLine();
}
file://下面是被定时调用的方法
static voidCheckStatus(Object state)
{
TimerExampleStates =(TimerExampleState)state;
s.counter++;
Console.WriteLine("{0}Checking Status {1}.",DateTime.Now.TimeOfDay, s.counter);
if(s.counter== 5)
{
file://使用Change方法改变了时间间隔
(s.tmr).Change(10000,2000);
Console.WriteLine("changed...");
}
if(s.counter== 10)
{
Console.WriteLine("disposingof timer...");
s.tmr.Dispose();
s.tmr =null;
}
}
}
程序首先创建了一个定时器,它将在创建1秒之后开始每隔1秒调用一次CheckStatus()方法,当调用5次以后,在CheckStatus()方法中修改了时间间隔为2秒,并且指定在10秒后重新开始。当计数达到10次,调用Timer.Dispose()方法删除了timer对象,主线程于是跳出循环,终止程序。程序执行的结果如下:
上面就是对ThreadPool和Timer两个类的简单介绍,充分利用系统提供的功能,可以为我们省去很多时间和精力——特别是对很容易出错的多线程程序。同时我们也可以看到.net Framework强大的内置对象,这些将对我们的编程带来莫大的方便。
有 时候你会觉得上面介绍的方法好像不够用,对,我们解决了代码和资源的同步问题,解决了多线程自动化管理和定时触发的问题,但是如何控制多个线程相互之间的联系呢?例如我要到餐厅吃饭,在吃饭之前我先得等待厨师把饭菜做好,之后我开始吃饭,吃完我还得付款,付款方式可以是现金,也可以是信用卡,付款之后我才能离开。分析一下这个过程,我吃饭可以看作是主线程,厨师做饭又是一个线程,服务员用信用卡收款和收现金可以看作另外两个线程,大家可以很清楚地看到其中的关系——我吃饭必须等待厨师做饭,然后等待两个收款线程之中任意一个的完成,然后我吃饭这个线程可以执行离开这个步骤,于是我吃饭才算结束了。事实上,现实中有着比这更复杂的联系,我们怎样才能很好地控制它们而不产生冲突和重复呢?
这种情况下,我们需要用到互斥对象,即System.Threading命名空间中的Mutex类。大家一定坐过出租车吧,事实上我们可以把Mutex看作一个出租车,那么乘客就是线程了,乘客首先得等车,然后上车,最后下车,当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对象被释放,如果它等待的Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个Mutex对象的线程都只有等待。
下面这个例子使用了Mutex对象来同步四个线程,主线程等待四个线程的结束,而这四个线程的运行又是与两个Mutex对象相关联的。其中还用到AutoResetEvent类的对象,如同上面提到的ManualResetEvent对象一样,大家可以把它简单地理解为一个信号灯,使用AutoResetEvent.Set()方法可以设置它为有信号状态,而使用AutoResetEvent.Reset()方法把它设置为无信号状态。这里用它的有信号状态来表示一个线程的结束。
// Mutex.cs
usingSystem;
usingSystem.Threading;
public classMutexSample
{
static MutexgM1;
static MutexgM2;
const intITERS = 100;
staticAutoResetEvent Event1 = new AutoResetEvent(false);
staticAutoResetEvent Event2 = new AutoResetEvent(false);
staticAutoResetEvent Event3 = new AutoResetEvent(false);
staticAutoResetEvent Event4 = new AutoResetEvent(false);
publicstatic void Main(String[] args)
{
Console.WriteLine("MutexSample ...");
//创建一个Mutex对象,并且命名为MyMutex
gM1 = newMutex(true,"MyMutex");
//创建一个未命名的Mutex 对象.
gM2 = newMutex(true);
Console.WriteLine("- Main Owns gM1 and gM2");
AutoResetEvent[]evs = new AutoResetEvent[4];
evs[0] =Event1; file://为后面的线程t1,t2,t3,t4定义AutoResetEvent对象
evs[1] =Event2;
evs[2] = Event3;
evs[3] =Event4;
MutexSampletm = new MutexSample( );
Thread t1= new Thread(new ThreadStart(tm.t1Start));
Thread t2= new Thread(new ThreadStart(tm.t2Start));
Thread t3= new Thread(new ThreadStart(tm.t3Start));
Thread t4= new Thread(new ThreadStart(tm.t4Start));
t1.Start();// 使用Mutex.WaitAll()方法等待一个Mutex数组中的对象全部被释放
t2.Start();// 使用Mutex.WaitOne()方法等待gM1的释放
t3.Start();// 使用Mutex.WaitAny()方法等待一个Mutex数组中任意一个对象被释放
t4.Start();// 使用Mutex.WaitOne()方法等待gM2的释放
Thread.Sleep(2000);
Console.WriteLine("- Main releases gM1");
gM1.ReleaseMutex(); file://线程t2,t3结束条件满足
Thread.Sleep(1000);
Console.WriteLine("- Main releases gM2");
gM2.ReleaseMutex(); file://线程t1,t4结束条件满足
//等待所有四个线程结束
WaitHandle.WaitAll(evs);
Console.WriteLine("...Mutex Sample");
Console.ReadLine();
}
public voidt1Start( )
{
Console.WriteLine("t1Startstarted, Mutex.WaitAll(Mutex[])");
Mutex[]gMs = new Mutex[2];
gMs[0] =gM1;//创建一个Mutex数组作为Mutex.WaitAll()方法的参数
gMs[1] =gM2;
Mutex.WaitAll(gMs);//等待gM1和gM2都被释放
Thread.Sleep(2000);
Console.WriteLine("t1Startfinished, Mutex.WaitAll(Mutex[]) satisfied");
Event1.Set(); file://线程结束,将Event1设置为有信号状态
}
public voidt2Start( )
{
Console.WriteLine("t2Startstarted, gM1.WaitOne( )");
gM1.WaitOne();//等待gM1的释放
Console.WriteLine("t2Startfinished, gM1.WaitOne( ) satisfied");
Event2.Set();//线程结束,将Event2设置为有信号状态
}
public voidt3Start( )
{
Console.WriteLine("t3Startstarted, Mutex.WaitAny(Mutex[])");
Mutex[]gMs = new Mutex[2];
gMs[0] =gM1;//创建一个Mutex数组作为Mutex.WaitAny()方法的参数
gMs[1] =gM2;
Mutex.WaitAny(gMs);//等待数组中任意一个Mutex对象被释放
Console.WriteLine("t3Startfinished, Mutex.WaitAny(Mutex[])");
Event3.Set();//线程结束,将Event3设置为有信号状态
}
public voidt4Start( )
{
Console.WriteLine("t4Startstarted, gM2.WaitOne( )");
gM2.WaitOne();//等待gM2被释放
Console.WriteLine("t4Startfinished, gM2.WaitOne( )");
Event4.Set();//线程结束,将Event4设置为有信号状态
}
}
下面是该程序的执行结果:
从执行结果可以很清楚地看到,线程t2,t3的运行是以gM1的释放为条件的,而t4在gM2释放后开始执行,t1则在gM1和gM2都被释放了之后才执行。Main()函数最后,使用WaitHandle等待所有的AutoResetEvent对象的信号,这些对象的信号代表相应线程的结束。
六、backgroundWorker多线程组件
在VS2005中添加了BackgroundWorker组件,该组件在多线程编程方面使用起来非常方便。BackgroundWorker类中主要用到的有这列属性、方法和事件:
重要属性:
1、CancellationPending获取一个值,指示应用程序是否已请求取消后台操作。通过在DoWork事件中判断CancellationPending属性可以认定是否需要取消后台操作(也就是结束线程);
2、IsBusy获取一个值,指示 BackgroundWorker 是否正在运行异步操作。程序中使用IsBusy属性用来确定后台操作是否正在使用中;
3、WorkerReportsProgress获取或设置一个值,该值指示BackgroundWorker能否报告进度更新
4、WorkerSupportsCancellation获取或设置一个值,该值指示 BackgroundWorker 是否支持异步取消。设置WorkerSupportsCancellation为true使得程序可以调用CancelAsync方法提交终止挂起的后台操作的请求;
重要方法:
1、CancelAsync请求取消挂起的后台操作
2、RunWorkerAsync开始执行后台操作
3、ReportProgress引发ProgressChanged事件
重要事件:
1、DoWork调用 RunWorkerAsync 时发生
2、ProgressChanged调用 ReportProgress 时发生
3、RunWorkerCompleted当后台操作已完成、被取消或引发异常时发生
另外还有三个重要的参数是RunWorkerCompletedEventArgs以及DoWorkEventArgs、ProgressChangedEventArgs。
BackgroundWorker的各属性、方法、事件的调用机制和顺序:
从上图可见在整个生活周期内发生了3次重要的参数传递过程:
参数传递1:此次的参数传递是将RunWorkerAsync(Object)中的Object传递到DoWork事件的DoWorkEventArgs.Argument,由于在这里只有一个参数可以传递,所以在实际应用往封装一个类,将整个实例化的类作为RunWorkerAsync的Object传递到DoWorkEventArgs.Argument;
参数传递2:此次是将程序运行进度传递给ProgressChanged事件,实际使用中往往使用给方法和事件更新进度条或者日志信息;
参数传递3:在DoWork事件结束之前,将后台线程产生的结果数据赋给DoWorkEventArgs.Result一边在RunWorkerCompleted事件中调用RunWorkerCompletedEventArgs.Result属性取得后台线程产生的结果。
另外从上图可以看到DoWork事件是在后台线程中运行的,所以在该事件中不能够操作用户界面的内容,如果需要更新用户界面,可以使用ProgressChanged事件及RunWorkCompleted事件来实现。
明白了BagkgroundWorker的事件调用顺序和参数传递机制之后在使用该组件用于多线程编程的时候就可以轻松许多了。
下面看我写的一个demo
//我们假设获取的记录数固定,我们为此定义一个常量:
private static int MaxRecords = 100;
private void Form1_Load(object sender,EventArgs e)
{
}
/// <summary>
/// 开始
/// </summary>
private void button1_Click(object sender, EventArgs e)
{
if (this.backgroundWorker1.IsBusy)
{
MessageBox.Show("正在执行");
return;
}
this.textBox1.Clear();
//当Start按钮被点击后,RunWorkerAsync方法被掉调用,我们定义的常量(MaxRecords )当作参数被掺入。
//随后,将会触发其DoWork事件
this.backgroundWorker1.RunWorkerAsync(MaxRecords);
this.button1.Enabled = false;
this.button2.Enabled = true;
}
private voidbackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
try
{
//调用RetrieveData方法逐条获取数据
e.Result = this.RetrieveData(this.backgroundWorker1, e);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
throw;
}
}
private int RetrieveData(BackgroundWorkerworker, DoWorkEventArgs e)
{
int maxRecords = (int)e.Argument;
int percent = 0;
for (int i = 1; i <= maxRecords; i++)
{
if (worker.CancellationPending)
{
return i;
}
percent = (int)(((double)i /(double)maxRecords) * 100);
worker.ReportProgress(percent, new KeyValuePair<int, string>(i,Guid.NewGuid().ToString()));
System.Threading.Thread.Sleep(100);
}
return maxRecords;
}
//这些操作需要操作UI上的控件,只能在MainThread中进行。
//如果在RetrieveData方法进行的话,由于该方式是一个异步方法,是会抛出异常的
private void backgroundWorker1_ProgressChanged(object sender,ProgressChangedEventArgs e)
{
KeyValuePair<int, string> record = (KeyValuePair<int,string>)e.UserState;
this.label1.Text = string.Format("There are {0} records retrieved!",record.Key);
this.progressBar1.Value = e.ProgressPercentage;
this.textBox1.AppendText(record.Value+"\r\n");
}
private void button2_Click(object sender,EventArgs e)
{
this.backgroundWorker1.CancelAsync();//请求结束后台操作
}
//如果操作正常地结束,BackgroundWorker的RunWorkerCompleted会被触发
private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
{
try
{
this.label1.Text = string.Format("Total records: {0}", e.Result);
this.button1.Enabled = true;
this.button2.Enabled = false;
}
catch (TargetInvocationException ex)
{
MessageBox.Show(ex.InnerException.GetType().ToString());
}
}
7、注意事项
1、只要有一个前台线程在运行,应用程序的进程就在运行。如果多个前台线程在运行,而Main方法结束了,应用程序的进程就是激活的。直到所有前台线程完成其任务为止。在默认情况下,用Thread类创建的线程是前台线程;线程池中的线程总是后台线程。
2、只有引用对象才能用于锁。关于lock(this), lock(typeof(ClassName))以及lock("thisLock")的讨论。
MSDN上指出,使用lock时不注意可能导致以下问题:
1). 如果实例可以被公共访问,将出现 lock (this) 问题。
2). 如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
3). 由于进程中使用同一字符串的任何其他代码都将共享同一个锁,所以出现 lock(“myLock”) 问题。
下面来讨论这些问题:
首先看lock(this)问题,如果一个类是公有的时,应该避免在基方法或属性中使用lock(this)语句,因为如果有其他人使用你的组件,它并不了解你的组件内部是否使用了锁,如果使用了而使用者又在类外部对类实例尝试加锁,则可能导致一个死锁,下面是我从Google(原地址:http://www.toolazy.me.uk/template.php?content=lock(this)_causes_deadlocks.xml,原程序中由于将主线程睡眠,实际操作系统会自动切换可用线程,因而未能体现出死锁的效果)找到的修改后的例子:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace Test
{
class InternalClass
{
public void TryLockThis()
{
Thread t = new Thread(ThreadFunction);
t.Start();
}
private void ThreadFunction()
{
Thread.Sleep(3000); // 延迟,等待外部对对象实例加锁
Console.WriteLine("尝试通过lock(this)加锁下面的代码块...");
while (true)
{
lock (this)
{
Console.WriteLine("执行内部锁,1秒后继续...");
Thread.Sleep(1000);
Console.WriteLine("内部锁完成...");
}
}
}
}
class ClassMain
{
private InternalClass theClass = new InternalClass();
public ClassMain()
{
theClass.TryLockThis();
Console.WriteLine("在执行内部锁之前对对象进行加锁...");
lock (theClass) // 如果注释掉这句,ThreadFunction()中的lock将执行成功
{
Console.WriteLine("对象被锁定, 在这里我们获得了一个死锁...");
while (true) { }
}
}
[STAThread]
static void Main(string[] args)
{
ClassMain cm = new ClassMain();
Console.WriteLine("Press Enter toexit");
Console.ReadLine();
}
}
}
可以看到上述的程序会导致一个死锁,程序的执行结果是这样的:
在执行内部锁之前对对象进行加锁...
对象被锁定, 在这里我们获得了一个死锁...
尝试通过lock(this)加锁下面的代码块...
(可以这样使用:object ob = new object(); lock(ob) ....)
因此,应尽量避免甚至拒绝使用lock(this)这样的语句。
同样的,lock(typeof(ClassName))也有类似的问题,由于typeof语句返回的是一个类的类型实例,对于一个类来说只有一个,如果在类的方法或实例方法中使用了typeof(className)这样的语句,而在类的外部又尝试对类进行加锁,同样可能导致死锁。
关于lock("thisLock")的问题,是由.Net的字符串处理模式导致的。
[MSDN]
公共语言运行库通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。
例如,如果将同一字符串分配给几个变量,运行库就会从拘留池中检索对该字符串的相同引用,并将它分配给各个变量。
由于这个原因,假如你lock的字符串对象在拘留池中(通常是编译时显式声明的用引号引起来的字符串),那么,这个对象也有可能被分配到另外一个变量,如果这个变量也使用了lock,那么,两个lock语句表面上lock的不同对象,但实质上却是同一个对象,这将造成不必要的阻塞,甚至可能造成死锁。且不亦发现问题。
从下面的程序中可以看出一个问题:
string a = "String Example";
string b = "String Example";
string c = (new StringBuilder()).Append("String Example").ToString();
Console.WriteLine("a==b? {0}", object.ReferenceEquals(a, b));
Console.WriteLine("a==c? {0}", object.ReferenceEquals(a, c));
上面程序执行的结果是:
a==b? True
a==c? False
从上面可以看出,a和b指向的是同一个引用,而lock正是通过引用来区分并加锁临界代码段的。也就是说,如果在我们一程序的一个部分中使用了lock("thisLock")进行加锁,而在程序的另一个位置同样也使用lock("thisLock")进行加锁,则极有可能导致一个死锁,因而是很危险的。实际上,不只是lock("thisLock")这样的语句,在lock中使用string类型的引用都有可能导致死锁。
上述就是C#中应避免使用lock(this),lock(typeof(className)), lock("thisLock")的原因。
3、线程的基本操作,例如:暂停、继续、停止等?
我不建议使用Thread类提供的Suspend、Resume以及Abort这三个方法,前两个有问题,好像在VS05已经屏蔽这两个方法;对于Abort来说,除了资源没有得到及时释放外,有时候会出现异常。如何做呢,通过设置开关变量来完成。
4、如何向线程传递参数或者从中得到其返回值?
我不建议使用静态成员来完成,仅仅为了线程而破坏类的封装有些得不偿失。那如何做呢,通过创建单独的线程类来完成。
5、如何使线程所占用的CPU不要老是百分之百?
造成这个原因是由于线程中进行不间断的循环操作,从而使CPU完全被子线程占有。那么处理此类问题,其实很简单,在适当的位置调用Thread.Sleep(20)来释放所占有CPU资源,不要小看这20毫秒的睡眠,它的作用可是巨大的,可以使其他线程得到CPU资源,从而使你的CPU使用效率降下来。
6、如何在子线程中控制窗体控件?
为什么不能直接在子线程中操纵UI呢。原因在于子线程和UI线程属于不同的上下文,换句比较通俗的话说,就好比两个人在不同的房间里一样,那么要你直接操作另一个房间里的东西,恐怕不行罢,那么对于子线程来说也一样,不能直接操作UI线程中的对象。
那么如何在子线程中操纵UI线程中的对象呢,.Net提供了Invoke和BeginInvoke这两种方法。简单地说,就是子线程发消息让UI线程来完成相应的操作。
这两个方法有什么区别,Invoke需要等到所调函数的返回,而BeginInvoke则不需要。
用这两个方法需要注意的,有如下三点:
第一个是由于Invoke和BeginInvoke属于Control类型的成员方法,因此调用的时候,需要得到Control类型的对象才能触发,也就是说你要触发窗体做什么操作或者窗体上某个控件做什么操作,需要把窗体对象或者控件对象传递到线程中。
第二个,对于Invoke和BeginInvoke接受的参数属于一个delegate类型,我在以前的文章中使用的是MethodInvoker,这是.Net自带的一个delegate类型,而并不意味着在使用Invoke或者BeginInvoke的时候只能用它。
第三个,使用Invoke和BeginInvoke有个需要注意的,就是当子线程在Form_Load开启的时候,会遇到异常,这是因为触发Invoke的对象还没有完全初始化完毕。处理此类问题,在开启线程之前显式的调用“this.Show();”,来使窗体显示在线程开启之前。如果此时只是开启线程来初始化显示数据,那我建议你不要使用子线程,用Splash窗体的效果可能更好。这方面可以参看如下的例子。
http://www.syncfusion.com/FAQ/WindowsForms/FAQ_c95c.aspx#q621q
7、线程同步lock,Monitor,同步事件EventWaitHandler,互斥体Mutex的基本用法,在此基础上,我们对它们用法进行比较,并给出什么时候需要锁什么时候不需要的几点建议。最后,介绍几个FCL中线程安全的类,集合类的锁定方式等,做为对线程同步系列的完善和补充。
1).几种同步方法的区别 lock和Monitor是.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方面可能更为有效,同步速度较快,但不能跨进程同步。lock(Monitor.Enter和Monitor.Exit方法的封装),主要作用是锁定临界区,使临界区代码只能被获得锁的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死锁。互斥体Mutex和事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。 互斥体Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热闹。 EventWaitHandle 类允许线程通过发信号互相通信。通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。
2).什么时候需要锁定 首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程同时加一,后果就是损失了一个计数,但相当频繁的锁定又可能带来性能上的消耗,还有最可怕的情况死锁。那么什么情况下我们需要使用锁,什么情况下不需要呢? 1)只有共享资源才需要锁定 只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存中的值,而属于线程内部的变量不需要锁定。 2)多使用lock,少用Mutex 如果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.NET的Mutex,Semaphore,AutoResetEvent和ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清楚的了解到他们的不同和适用范围。 3)了解你的程序是怎么运行的 实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑锁定,当然对于ASP.NET中的Application对象中的数据,我们就要考虑加锁了。 4)把锁定交给数据库 数据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条记录时,我们才考虑加锁。 5)业务逻辑对事务和线程安全的要求 这条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲一些性能,和很多的开发时间来做这方面的工作。而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用锁定,比如有的时候计数器少一多一,对结果无伤大雅的情况下,我们就可以不用去管它。
3).InterLocked类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存中,则不同进程的线程就可以使用该机制。互锁操作是原子的,即整个操作是不能由相同变量上的另一个互锁操作所中断的单元。这在抢先多线程操作系统中是很重要的,在这样的操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改和存储该值之前被挂起。 我们来看一个InterLock.Increment()的例子,该方法以原子的形式递增指定变量并存储结果,示例如下: Increment()方法累加的示例 classInterLockedTest { public staticInt64 i = 0;public staticvoid Add() { for (int i = 0; i < 100000000; i++) {Interlocked.Increment(ref InterLockedTest.i); //InterLockedTest.i= InterLockedTest.i + 1; } } public static void Main(string[] args) { Thread t1= new Thread(newThreadStart(InterLockedTest.Add)); Thread t2 = new Thread(new ThreadStart(InterLockedTest.Add)); t1.Start(); t2.Start();t1.Join(); t2.Join(); Console.WriteLine(InterLockedTest.i.ToString());Console.Read(); } } 输出结果200000000,如果InterLockedTest.Add()方法中用注释掉的语句代替Interlocked.Increment()方法,结果将不可预知,每次执行结果不同。InterLockedTest.Add()方法保证了加1操作的原子性,功能上相当于自动给加操作使用了lock锁。同时我们也注意到InterLockedTest.Add()用时比直接用+号加1要耗时的多,所以说加锁资源损耗还是很明显的。 另外InterLockedTest类还有几个常用方法,具体用法可以参考MSDN上的介绍。
4).集合类的同步 .NET在一些集合类,比如Queue、ArrayList、HashTable和Stack,已经提供了一个供lock使用的对象SyncRoot。用Reflector查看了SyncRoot属性(Stack.SynchRoot略有不同)的源码如下:
SyncRoot属性源码
public virtual object SyncRoot
{
get
{ if (this._syncRoot == null)
{ //如果_syncRoot和null相等,将new object赋值给_syncRoot //Interlocked.CompareExchange方法保证多个线程在使用syncRoot时是线程安全的 Interlocked.CompareExchange(ref this._syncRoot,new object(), null);
}
return this._syncRoot;
}
}
这里要特别注意的是MSDN提到:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。应该使用下面的代码:
Queue使用lock示例
Queue q = new Queue();
lock (q.SyncRoot)
{
foreach (object item in q)
{ //do something }
}
还有一点需要说明的是,集合类提供了一个是和同步相关的方法Synchronized,该方法返回一个对应的集合类的wrapper类,该类是线程安全的,因为他的大部分方法都用lock关键字进行了同步处理。如HashTable的Synchronized返回一个新的线程安全的HashTable实例,代码如下:
Synchronized的使用和理解 //在多线程环境中只要我们用下面的方式实例化HashTable就可以了 Hashtable ht = Hashtable.Synchronized(newHashtable());
//以下代码是.NET Framework Class Library实现,增加对Synchronized的认识 [HostProtection(SecurityAction.LinkDemand,Synchronization=true)]
public static Hashtable Synchronized(Hashtable table)
{
if (table == null)
{ throw new ArgumentNullException("table"); }
return new SyncHashtable(table);
} //SyncHashtable的几个常用方法,我们可以看到内部实现都加了lock关键字保证线程安全public override void Add(object key, object value)
{
lock (this._table.SyncRoot)
{ this._table.Add(key,value); }
}
public override void Clear()
{
lock (this._table.SyncRoot)
{ this._table.Clear(); }
}
public override void Remove(object key)
{
lock (this._table.SyncRoot) { this._table.Remove(key);}
}
线程同步是一个非常复杂的话题,这里只是根据公司的一个项目把相关的知识整理出来,作为工作的一种总结。这些同步方法的使用场景是怎样的?究竟有哪些细微的差别?还有待于进一步的学习和实践。