<三> 线程优先级
如果在应用程序中有多个程序在运行,但一些线程比另一些重要因而需要分配更多的CPU时间的情况下,可以在一个进行中为不同的线程指定不同的优先级。一般情况下,如果有优先级较高的线程在工作,就不会给优先级较低的线程分配任何时间片,其优点是可以保证给接收用户输入的线程指定较高的优先级。在大多数的时间内,这个线程什么也不做。而其他线程则执行它的任务。但是,如果用户输入了信息,这个线程就会立即获得比应用程序中其他线程更高的优先级,在短时间内处理用户输入数据。
线程的优先级定义为ThreadPriority枚举类型,取值如下表:
线程的优先级及其含义
|
名称 |
含义 |
|
Highest |
将线程安排在具有任何其他优先级的线程之前 |
|
AboveNormal |
将线程安排在具有Highest优先级的线程之后,在具有Normal优先级的线程之前 |
|
Normal |
将线程安排在具有AboveNormal优先级的线程之后,在具有BelowNormal优先级的线程之前。默认情况下,线程具有Normal优先级 |
|
BelowNormal |
将线程安排在具有Normal优先级的线程之后,在具有Lowest优先级的线程之前 |
|
Lowest |
将线程安排在具有任何其他优先级的线程之后 |
高优先级的线程可以完全阻止低优先级的线程执行,因此在改变线程的优先级时要特别小心以免造成某些线程得不到CPU时间。此外,每个进程都有一个基本优先级,这些值与进程的优先级是有关系的。给线程指定较高的优先级,可以确保它在该进程内比其它线程优先执行,但系统上可能还运行着其他进程,它们的线程有更高的优先级。
<四> 线程同步
使用线程的一个重要内容是同步访问多个线程访问的任何变量。所谓同步,是指在某一时刻只有一个线程可以访问变量。如果不能确保对变量的访问是同步的,就可能会产生错误或不可预料的结果。一般情况下,当一个线程写入一个变量,同时有其他线程读取或写入这个变量时,就应同步变量。
例如,两个线程thread1和thread2具有相同的优先级,而且同时在系统上运行。当在时间片中执行第一个线程时,它可能在public变量variable1中写入了某个值。然而,在下一个时间片中,另一个线程尝试读取或者在variable1中写入某个值。如果在第一个时间片中没有完成向variable1中的值写入过程,则会产生错误。当另一个线程读取这个变量时,它可以读取错误的值,这会产生错误。通过同步使得仅仅一个线程能使用variable1,就可以避免出现这种情况。
通过指定对象的加锁和解锁可以同步代码段的访问。在.NET的System.Threading命名空间中提供了Monitor类来实现加锁与解锁。这个类中的方法都是静态的,所以不需要实例化这个类。下表中一些静态方法提供了一种机制用来向同步对象的访问从而避免死锁和维护数据一致性。
Monitor类的主要方法
|
名称 |
含义 |
|
Enter |
在指定对象上获取排他锁 |
|
TryEnter |
试图获取指定对象的排他锁 |
|
Exit |
释放指定对象上的排他锁 |
|
Wait |
释放对象上的锁并阻塞当前线程,直到它重新获取该锁 |
|
Pulse |
通知等待队列中的线程锁定对象状态的更改 |
|
PulseAll |
通知所有等待线程对象状态的更改 |
Moinitor.Enter,Monitor.TryEnter和Monitor.Exit用来对指定对象的加锁和解锁。一旦获取(调用Monitor.Enter)指定对象(代码段)的锁,其他线程都不能获取该锁。举个例子来说,线程X获得了一个对象锁,这个对象锁可以释放(调用Monitor.Exit(object)or Monitro.Wait())。当这个对象锁被释放后,Monitor.Pulse方法和Monitor.PulseAll方法通知就线程X获取了锁,同时调用Monitor.Wait的线程X进入等待队列。当从当前锁定对象的线程(线程Y)收到了Pulse或PulseAll,等待队列的线程就进入就绪队列。线程X重新得到对象锁时,Monitor.Wait才返回。
以下是使用Monitor类的简单例子:
using System;
using System.Collections;
using System.Threading;
namespace 笔记
{
class MyThread
{
public void Method()
{
//获取锁
Monitor.Enter(this);
//处理需要同步的代码
//释放锁
Monitor.Enter(this);
}
}
}
上面的代码运行可能会产生问题。当代码运行到获取锁与释放锁之间时一旦发生异常,Monitor.Exit将不会返回。这段程序将挂起,其他的线程也将得不到锁。解决的方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后一定会释放锁。
C#lock关键辽提供了与Monitoy.Enter和Monitory.Exit同样的功能,这种方法用在代码段不能被其他独立的线程中断的情况。通过Monitor类的简单封装,lock为同步访问变量提供了一个非常简单的方式,用法如下:
lock(x)
{
//使用X的语句
}
lock语句把变量放在圆括号中,以包装对象,称为独占锁或排他锁。当执行带有lock关键字的复合语句时,独占锁会保留下来。当变量被包装在独占锁时,其他线程就不能访问该变量。如果在上面的代码中使用独占锁,在执行复合语句时,这个线程就会失去其时间片。如果下一个时间片的线程试图访问变量,变会被拒绝。Windows会让其他线程处理睡眠状态,直到解除了独占锁为止。
使用lock同步线程示例:
using System;
using System.Collections;
using System.Threading;
namespace 笔记
{
//银行账户类
class Account
{
int balance; //余额
Random r = new Random();
public Account(int initial)
{
balance = initial;
}
//取钱
int Withdraw(int amount)
{
if (balance < 0)
{
throw new Exception("余额为负!");
}
lock (this)
{
if (balance >= amount)
{
Console.WriteLine("原有余额:" + balance);
Console.WriteLine("支取金额:_" + amount);
balance = balance - amount;
Console.WriteLine("现有余额:" + balance);
return amount;
}
else
{
return 0; //拒绝交易
}
}
}
//测试交易
public void DoTransactions()
{
//支取随机金额100次
for (int i = 0; i < 100; i++)
{
Withdraw(r.Next(1, 100));
}
}
}
class TestApp
{
public static void MMain()
{
//建立10个线程同时进行交易
Thread[] threads = new Thread[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();
}
}
}
}
在这个事例中,10个线程同时进行交易,如果不加控制,很可能发生在支取金额时对balance字段的访问冲突。假设当前余额为100,有两个线程都要支持60,则各自检查余额时都以为可以支取,造成的后果则是总共支取120,从而导致余额为负值。读者可以试着将lock语句注释掉再运行,此时将产生余额为负的异常。
独占锁是控制变量访问的许多机制中最简单的。实际上,C#的lock语句是一个C#语法包装器,它封装了Monitor类的Enter和Exit两个方法的调用。如果需要进一步了解System.Threading命名空间的其他对象,请参阅MSDN和有关资料。
同步时要注意的问题
同步线程在多线程应用程序中非常重要。但是,这是一个需要详细讨论的内容,因为很容易出现微妙且难以察觉的问题特别是死锁。
线程同步非常重要,但只要在需要时使用这一条也是非常重要的。因为这会降低性能。原因有两个:第一,在对象上放置和解开锁会带来某些系统开销,但这些系统开销都非常小。第二个原因更为重要,线程同步使用得越多,等待释放对象的线程也就越多。如果一个线程在对象上放置了一个锁,需要访问该对象的其他线程就只能暂停执行,直到该锁被解开,才能继续执行。因此,在lock块内部编写的代码越少越好,以免出现线程同步错误。Lock语句在某种意义上就是临时禁用应用程序的多线程功能,也就临时删除了多线程的各种优势。
另一方面,使用过多的同步线程的危险性(性能和响应降低)并没有在需要时不使用同步线程那么高 (能以跟踪的运行时错误)。
死锁是一个错误,在两个线程都需要访问被互锁的资源时发生。假定一个线程运行下述代码,其中a 和b 是两个线程都可以访问的对象引用:
lock(a)
{
lock(b)
{
//do something
}
}
同时,另一个线程运行下述代码:
lock(b)
{
loca(a)
{
//do something
}
}
根据线程遇到不同的语句的时间,可能会出现下述情况:第一个线程在a上有一个锁,同时第二个线程在b上有一个锁。不久,线程A遇到lock(b)语句,立即进行睡眠状态,等待b上的锁被解开。之后,第二个线程遇到lock(a)语句,也立即进行睡眠状态,等待windows在a上的锁被解开唤醒它。但a上的锁永远不会解开,因数第一个线程拥有这个锁,目前正在睡眠状态,在b上的锁被解开前是不会醒的,而在第二个线程被叫醒之前,b上的锁不会解开,结果就是一个死锁。
在上面的代码中发生死锁是非常明显的,在编码中很容易避免,因为程序员肯定不会编写这样的代码,但记住不同的锁可以发生在不同的方法调用中。在这个示例中,第一个编程实际执行下述代码:
locak(a)
{
CallSomeMethod();
}
CallSomeMethod()可以调用其他方法,其中有一个locak(b)语句,此时是否会发生死锁就不那么明显了。事实上出现死锁的条件常常不明显,如果有这样的条件,也很难识别错误。一般情况下这需要一定的经验。但是在编写式线程应用程序时,如果需要同步,就必须考虑代码的所有部分,检查是否有可能发生死锁的条件。必须记住:不可能预见不同线程遇到不同语句的确切时间。
在应用程序中使用多线程要仔细规划,太多的线程会导致资源问题,线程不足又能会使需要处理多任务的应用程序执行缓慢,执行效果不好。虽然.NET Framework提供了处理线程的各种方法与机制,但并没有解决处理线程中所有困难的任务,如我们必须考虑线程的优先和同步问题。
浙公网安备 33010602011771号