线程优先级的概念在很多技术框架下都有应用,.Net框架也不例外,.NET框架为程序员提供了方便的接口以供使用。系统会为每一个线程分配一个优先级别。.NET线程优先级,是指定一个线程的相对于其他线程的相对优先级,它规定了线程的执行顺序,对于在CLR中创建的线程,其优先级别默认为Normal,而在CLR之外创建的线程进入CLR时,将会保留其先前的优先级,可以通过访问线程的Priority属性来获取或设置线程的优先级别。
System.Threading命名空间中的ThreadPriority枚举定义了一组线程优先级的所有可能值,我这里按级别由高到低排列出来,具体的说明就不在这里解释。
Highest , AboveNormal , Normal , BelowNormal , Lowest
无论线程是否运行于CLR中,线程执行时间片是有操作系统进行分配的,线程是严格按照其优先级来调度执行的,调度算法来确定线程的执行顺序,不同的操作系统可能又不同的调度算法。具有最高优先级别的线程经过调度后总是首先运行,只要具有较高优先级别的线程可以运行,较低级别的所有线程就必须等待。如果具有相同优先级的多个线程都可以运行,那么调度程序将遍历处于该优先级的所有线程,并为每个线程提供一个固定的执行时间片,因此同一优先级的线程很多时候都是交替执行的。如果具有较高优先级的线程可以运行,较低优先级的线程将被抢先,并允许具有较高优先级的线程再次执行,如果在给定的优先级上不再有可运行的线程,则调度程序将转移到下一个低级别的优先级,并在该优先级山调度线程。
下面我们看一段示例代码:在这里我们定义四个线程,分别为ThreadA,ThreadB,ThreadC,ThreadD,ThreadA和ThreadB优先级默认为Normal优先级,将ThreadC和ThreadD的优先级设置为BelowNormal,这里我们不再单独创建被委托的方法类,直接使用匿名方法。
using System; using System.Threading; class Test { public static void Main() { ThreadStart threadStartA=new ThreadStart(delegate(){ //线程threadA for(int i=0;i<1000000;i++) { if (i%10000==0) Console.Write("A"); } }); Thread threadA=new Thread(threadStartA); ThreadStart threadStartB=new ThreadStart(delegate(){ //线程threadB for(int i=0;i<1000000;i++) { if (i%10000==0) Console.Write("B"); } }); Thread threadB=new Thread(threadStartB); ThreadStart threadStartC=new ThreadStart(delegate(){ //线程threadC for(int i=0;i<1000000;i++) { if (i%10000==0) Console.Write("C"); } }); Thread threadC=new Thread(threadStartC); threadC.Priority=ThreadPriority.BelowNormal; //将threadC的优先级设置为BelowNormal ThreadStart threadStartD=new ThreadStart(delegate(){ //线程threadD for(int i=0;i<1000000;i++) { if (i%10000==0) Console.Write("D"); } }); Thread threadD=new Thread(threadStartD); threadD.Priority=ThreadPriority.BelowNormal; //将threadD的优先级设置为BelowNormal //启动线程 threadA.Start(); threadB.Start(); threadC.Start(); threadD.Start(); } }
下面我们看一下运行结果:
通过运行结果我们可以得知:1.由于ThreadA与ThreadB的优先级别高于ThreadC和ThreadD,且ThreadA和ThreadB处于同一优先级,因此ThreadA和ThreadB交替执行,直到ThreadA和ThreadB执行完毕,ThreadC和ThreadD开始交替执行。
下面我们了解一下Thread.Join()方法,微软MSDN的解释为,阻塞调用线程,直到某一线程终止时为止。某一线程指的是调用Join()方法的线程。下面我们看一段示例程序:
using System; using System.Threading; class Test { public static void Main() { ThreadStart threadStartA=new ThreadStart(delegate(){ //线程threadA for(int i=0;i<1000000;i++) { if (i%10000==0) Console.Write("A"); } }); Thread threadA=new Thread(threadStartA); ThreadStart threadStartB=new ThreadStart(delegate(){ //线程threadB for(int i=0;i<500000;i++) { if (i%10000==0) Console.Write("B1"); } threadA.Join(); //阻塞线程threadB,插入threadA进行执行 for(int i=0;i<500000;i++) { if (i%10000==0) Console.Write("B2"); } }); Thread threadB=new Thread(threadStartB); //启动线程 threadA.Start(); threadB.Start(); } }
运行结果:
从运行结果可以看出:一开始,ThreadA和ThreadB交替执行,当ThreadB执行到ThreadA.Join()方法时,ThreadB被阻塞,ThreadA插入进来单独执行,当ThreadA执行完毕以后,ThreadB继续执行。
除了ThreadA和ThreadB外,程序中还有一个主线程(Main Thread)。现在我们在主线程中添加一些输出代码,看看主线程和工作线程A、B是如何并发运行的。
using System; using System.Threading; class Test { public static void Main() { ThreadStart threadStartA=new ThreadStart(delegate(){ //线程threadA for(int i=0;i<1000000;i++) { if (i%10000==0) Console.Write("A"); } }); Thread threadA=new Thread(threadStartA); ThreadStart threadStartB=new ThreadStart(delegate(){ //线程threadB for(int i=0;i<1000000;i++) { if (i%10000==0) Console.Write("B"); } }); Thread threadB=new Thread(threadStartB); //启动线程 threadA.Start(); threadB.Start(); for(int i=0;i<1000000;i++) //主线程 { if (i%10000==0) Console.Write("M"); } } }
运行结果:
从从这里可以看出,ThreadA和ThreadB以及主线程是交替执行的。
线程同步
所谓同步,是指在某一时刻只有一个线程可以访问变量。如果不能确保对变量的访问是同步的,就可能会产生错误或不可预料的结果。一般情况下,当一个线程写入一个变量,同时有其他线程读取或写入这个变量时,就应同步变量。 例如,两个线程thread1和thread2具有相同的优先级,而且同时在系统上运行。当在时间片中执行第一个线程时,它可能在public变量variable1中写入了某个值。然而,在下一个时间片中,另一个线程尝试读取或者在variable1中写入某个值。如果在第一个时间片中没有完成向variable1中的值写入过程,则会产生错误。当另一个线程读取这个变量时,它可以读取错误的值,这会产生错误。通过同步使得仅仅一个线程能使用variable1,就可以避免出现这种情况。通过指定对象的加锁和解锁可以同步代码段的访问。在.NET的System.Threading命名空间中提供了Monitor类来实现加锁与解锁。这个类中的方法都是静态的,所以不需要实例化这个类。下表中一些静态方法提供了一种机制用来向同步对象的访问从而避免死锁和维护数据一致性。
Monitor类的主要方法:
Monitor.Enter():给指定的对象枷锁,确保访问同步。
Monitor.TryEnter():尝试给指定对象枷锁。
Monitor.Exit():释放指定对象上的 枷锁。
Monitor.Wait():释放指定对象上的枷锁,并阻塞当前线程,直至其从新获得该锁。
Monitor.Pulse():通知等待队列中的线程锁定对象状态的更改
Monitor.PulseAll():通知所有等待线程对象状态的更改.
下面我们看一段代码:
using System;
using System.Collections;
using System.Threading;
class MyThread
{
public void Method()
{
//获取锁
Monitor.Enter(this);
//处理需要同步的代码
//释放锁
Monitor.Exit(this);
}
}
上面的代码运行可能会产生问题。当代码运行到获取锁与释放锁之间时一旦发生异常,Monitor.Exit将不会返回。这段程序将挂起,其他的线程也将得不到锁。解决的方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后一定会释放锁。
C#中的Lock关键字提供了Monitor.Enter()和Monitor.Exit()同样的功能,这种方法用在代码段不能被其他独立的线程中断的情况,通过对Monitor类简单的封装,Lock为变量的同步访问提供了很简单的方法,用法如下:
Lock(obj)
{
//访问obj的语句
}
Lock语句把变量放在圆括号中,以包装对象,称为独占锁会排他锁,当变量被包装在独占锁时,其他线程就不能访问该变量。
下面我们看一个例子:
using System; using System.Collections; using System.Linq; using System.Text; using System.Threading; class Account //银行账户类 { private int TotalMoney; //余额 Random r = new Random(); public Account(int storeMoney) { TotalMoney = storeMoney; } public int GetMoney(int amount) //取钱 { if (TotalMoney < 0) { throw new Exception("余额为负!"); } lock (this) { if (TotalMoney >= amount) { Console.WriteLine("原有余额:" + TotalMoney); Console.WriteLine("支取金额:" + amount); TotalMoney = TotalMoney - amount; Console.WriteLine("现有余额:" + TotalMoney); return amount; } else { return 0; //拒绝交易 } } } public void DoTransactions() //测试交易 { //支取随机金额100次 for (int i = 0; i < 100; i++) { GetMoney(r.Next(1, 100)); } } } class TestApp { public static void Main() { Thread[] threads = new Thread[10]; //建立10个线程同时进行交易 Account acc = new Account(1000); for (int i = 0; i < 10; i++) { Thread t = new Thread(new ThreadStart(acc.DoTransactions)); threads[i] = t; } for (int i = 0; i < 10; i++) { threads[i].Start(); } Console.ReadLine(); } }
在这个事例中,10个线程同时进行交易,如果不加控制,很可能发生在支取金额时对TotalMoney字段的访问冲突。假设当前余额为100,有两个线程都要支持60,则各自检查余额时都以为可以支取,造成的后果则是总共支取120,从而导致余额为负值。下面我们看看运行结果:
如果我们现在将Lock关键字去掉,再看看结果
出现了余额为负的的异常。
独占锁是控制变量访问的许多机制中最简单的。实际上,C#的lock语句是一个C#语法包装器,它封装了Monitor类的Enter和Exit两个方法的调用。