打开你的
VS.net
,新建一个控制台应用程序(
Console
Application
)
,下面这些代码将让你体味到完全控制
一个线程的无穷乐趣!
ThreadTest.cs
using System;
using System.Threading;
namespace ThreadTest
{
public class Alpha
{
public void Beta()
{
while (true)
{
Console.WriteLine("Alpha.Beta is running in its own thread.");
}
}
};
public class Simple
{
public static int Main()
{
Console.WriteLine("Thread Start/Stop/Join Sample");
Alpha oAlpha = new Alpha();
file://
这里创建一个线程,使之执行
Alpha
类的
Beta()
方法
Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));
oThread.Start();
while (!oThread.IsAlive);
Thread.Sleep(1);
oThread.Abort();
oThread.Join();
Console.WriteLine();
Console.WriteLine("Alpha.Beta has finished");
try
{
Console.WriteLine("Try to restart the Alpha.Beta thread");
oThread.Start();
}
catch (ThreadStateException)
{
Console.Write("ThreadStateException trying to restart Alpha.Beta. ");
Console.WriteLine("Expected since aborted threads cannot be restarted.");
Console.ReadLine();
}
return 0;
}
}
}
这段程序包含两个类
Alpha
和
Simple
,在创建线程
oThread
时我们用指向
Alpha.Beta()
方法的初始化了
ThreadStart
代理(
delegate
)对象,当我们创建的线程
oThread
调用
oThread.Start()
方法启动时,实际上程序
运行的是
Alpha.Beta()
方法:
Alpha oAlpha = new Alpha();
Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));
oThread.Start();
然后在
Main()
函数的
while
循环中,我们使用静态方法
Thread.Sleep()
让主线程停了
1ms
,这段时间
CPU
转
向执行线程
oThread
。然后我们试图用
Thread.Abort()
方法终止线程
oThread
,注意后面的
oThread.Join()
,
Thread.Join()
方法使主线程等待,
直到
oThread
线程结束。
你可以给
Thread.Join()
方法指定一个
int
型的参数
作为等待的最长时间。之后,我们试图用
Thread.Start()
方法重新启动线程
oThread
,但是显然
Abort()
方法
带来的后果是不可恢复的终止线程,所以最后程序会抛出
ThreadStateException
异常。
程序最后得到的结果将如下图:
在这里我们要注意的是其它线程都是依附于
Main()
函数所在的线程的,
Main()
函数是
C#
程序的入口,起始
线程可以称之为主线程,如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无
条件终止。而所有的线程虽然在微观上是串行执行的,但是在宏观上你完全可以认为它们在并行执行。
读者一定注意到了
Thread.ThreadState
这个属性,
这个属性代表了线程运行时状态,
在不同的情况下有不同
的值,
于是我们有时候可以通过对该值的判断来设计程序流程。
ThreadState
在各种情况下的可能取值如下:
Aborted
:线程已停止
AbortRequested
:线程的
Thread.Abort()
方法已被调用,但是线程还未停止
Background
:线程在后台执行,与属性
Thread.IsBackground
有关
Running
:线程正在正常运行
Stopped
:线程已经被停止
StopRequested
:线程正在被要求停止
Suspended
:线程已经被挂起(此状态下,可以通过调用
Resume()
方法重新运行)
SuspendRequested
:线程正在要求被挂起,但是未来得及响应
Unstarted
:未调用
Thread.Start()
开始线程的运行
WaitSleepJoin
:线程因为调用了
Wait(),Sleep()
或
Join()
等方法处于封锁状态
上面提到了
Background
状态表示该线程在后台运行,
那么后台运行的线程有什么特别的地方呢?其实后台
线程跟前台线程只有一个区别,那就是
后台线程不妨碍程序的终止
。一旦一个进程所有的前台线程都终止
后,
CLR
(通用语言运行环境)将通过调用任意一个存活中的后台进程的
Abort()
方法来彻底终止进程。
当线程之间争夺
CPU
时间时,
CPU
按照是线程的优先级给予服务的。在
C#
应用程序中,用户可以设定
5
个不同的优先级,由高到低分别是
Highest
,
AboveNormal
,
Normal
,
BelowNormal
,
Lowest
,在创建线程
时如果不指定优先级,那么系统默认为
ThreadPriority.Normal
。给一个线程指定优先级
,我们可以使用如下代码:
//
设定优先级为最低
myThread.Priority=ThreadPriority.Lowest;
通过设定线程的优先级,我们可以安排一些相对重要的线程优先执行,例如对用户的响应等等。
现在我们对怎样创建和控制一个线程已经有了一个初步的了解,下面我们将深入研究线程实现中比较典型
的的问题,并且探讨其解决方法。
三
.
线程的同步和通讯——生产者和消费者
假设这样一种情况,两个线程同时维护一个队列,如果一个线程对队列中添加元素,而另外一个线程从队
列中取用元素,那么我们称添加元素的线程为生产者,称取用元素的线程为消费者。生产者与消费者问题
看起来很简单,但是却是多线程应用中一个必须解决的问题,它涉及到线程之间的同步和通讯问题。
前面说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。但是多
线程环境下,可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,
因此我们必须避免这种情况的发生。
C#
提供了一个关键字
lock
,它可以把一段代码定义为互斥段(
critical
section
)
,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在
C#
中,关键字
lock
定
义如下:
lock(expression) statement_block
expression
代表你希望跟踪的对象,
通常是对象引用。
一般地,
如果你想保护一个类的实例,
你可以使用
this
;
如果你希望保护一个静态变量(如互斥代码段在一个静态方法内部)
,一般使用类名就可以了。而
statement_block
就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
下面是一个使用
lock
关键字的典型例子,我将在注释里向大家说明
lock
关键字的用法和用途:
//lock.cs
using System;
using System.Threading;
internal class Account
{
int balance;
Random r = new Random();
internal Account(int initial)
{
balance = initial;
}
internal int Withdraw(int amount)
{
if (balance < 0)
{
file://
如果
balance
小于
0
则抛出异常
throw new Exception("Negative Balance");
}
//
下面的代码保证在当前线程修改
balance
的值完成之前
//
不会有其他线程也执行这段代码来修改
balance
的值
//
因此,
balance
的值是不可能小于
0
的
lock (this)
{
Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name);
file://
如果没有
lock
关键字的保护,那么可能在执行完
if
的条件判断之后
file://
另外一个线程却执行了
balance=balance-amount
修改了
balance
的值
file://
而这个修改对这个线程是不可见的,所以可能导致这时
if
的条件已经不成立了
file://
但是,这个线程却继续执行
balance=balance-amount
,所以导致
balance
可能小于
0
if (balance >= amount)
{
Thread.Sleep(5);
balance = balance - amount;
return amount;
}
else
{
return 0; // transaction rejected
}
}
}
internal void DoTransactions()
{
for (int i = 0; i < 100; i++)
Withdraw(r.Next(-50, 100));
}
}
internal class Test
{
static internal Thread[] threads = new Thread[10];
public static void Main()
{
Account acc = new Account (0);
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].Name=i.ToString();
for (int i = 0; i < 10; i++)
threads[i].Start();
Console.ReadLine();
}
}
而
多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用
lock
关键字了,这里
需要用到
System.Threading
中的一个类
Monitor
,我们可以称之为监视器,
Monitor
提供了使线程共享资源
的方案。
Monitor
类可以锁定一个对象,
一个线程只有得到这把锁才可以对该对象进行操作。
对象锁机制保证了
在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。
Monitor
必须和一个具体的对象相关
联
,但是由于它是一个静态的类,所以不能使用它来定义对象,而且
它的所有方法都是静态的,不能使用
对象来引用
。下面代码说明了使用
Monitor
锁定一个对象的情形:
......
Queue oQueue=new Queue();
......
Monitor.Enter(oQueue);
......//
现在
oQueue
对象只能被当前线程操纵了
Monitor.Exit(oQueue);//
释放锁
如上所示,
当一个线程调用
Monitor.Enter()
方法锁定一个对象时,
这个对象就归它所有了,
其它线程想要访
问这个对象,只有等待它使用
Monitor.Exit()
方法释放锁。为了保证线程最终都能释放锁,你可以把
Monitor.Exit()
方法写在
try-catch-finally
结构中的
finally
代码块里。
对于任何一个被
Monitor
锁定的对象,
内存中都保存着与它相关的一些信息,其一是现在持有锁的线程的引用,其二是一个预备队列,队列中保
存了已经准备好获取锁的线程,其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队
列的引用。当拥有对象锁的线程准备释放锁时,它使用
Monitor.Pulse()
方法通知等待队列中的第一个线程,
于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁