- 概述与概念
1.1. 入门线程小例子
C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行。一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并具有多线程创建额外的线程。
class ThreadTest
{
static void Main()
{
Threadt = new Thread (WriteY);
t.Start();
while (true) Console.Write ("x");
}
static void WriteY()
{
while (true)Console.Write ("y");
}
}
主线程创建了一个新线程“t”,它运行了一个重复打印字母"y"的方法,同时主线程重复打印字母“x”。CLR分配每个线程到它自己的内存堆栈上,来保证局部变量的分离运行。在接下来的方法中我们定义了一个局部变量,然后在主线程和新创建的线程上同时地调用这个方法。
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
for (int cycles = 0; cycles < 5; cycles++)Console.Write ('?');
}
变量cycles的副本分别在各自的内存堆栈中创建,输出也一样,可预见,会有10个问号输出。当线程们引用了一些公用的目标实例的时候,他们会共享数据。下面是实例:
class ThreadTest
{
bool done;
static void Main()
{
ThreadTest tt = new ThreadTest();
new Thread (tt.Go).Start();
tt.Go();
}
void Go()
{
if (!done) { done = true; Console.WriteLine("Done"); }
}
}
因为在相同的ThreadTest实例中,两个线程都调用了Go(),它们共享了done字段,这个结果输出的是一个"Done",而不是两个。
静态字段提供了另一种在线程间共享数据的方式,下面是一个以done为静态字段的例子:
class ThreadTest
{
static bool done;
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
if (!done) { done = true; Console.WriteLine("Done"); }
}
}
上述两个例子足以说明, 另一个关键概念, 那就是线程安全(或反之,它的不足之处! ) 输出实际上是不确定的:它可能(虽然不大可能) ,“Done” ,可以被打印两次。然而,如果我们在Go方法里调换指令的顺序,"Done"被打印两次的机会会大幅地上升:
static void Go()
{
if (!done){ Console.WriteLine (“Done”); done = true; }
}
问题就是一个线程在判断if块的时候,正好另一个线程正在执行WriteLine语句,还没有将done设置为true。补救措施是当读写公共字段的时候,提供一个排他锁;C#提供了lock语句来达到这个目的。
class ThreadSafe
{
static bool done;
static object locker= new object();
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
lock (locker)
{
if (!done) { Console.WriteLine ("Done");done = true; }
}
}
}
当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以"Done"只被打印了1次。代码以如此方式在不确定的多线程环境中被叫做线程安全。
临时暂停,或阻止是多线程的协同工作,同步活动的本质特征。等待一个排它锁被释放是一个线程被阻止的原因,另一个原因是线程想要暂停或Sleep一段时间:
Thread.Sleep(TimeSpan.FromSeconds (30));
一个线程也可以使用它的Join方法来等待另一个线程结束:
static void Main()
{
Thread t = new Thread (WriteY);
t.Start();
t.Join();
for(int i=0;i<10;i++)
{
Console.Write(“X”);
}
}
static void WriteY()
{
for(int i=0;i<10;i++)
{
Console.Write(“Y”);
}
}
此时会等Y输出完毕才会输出X。
一个线程,一旦被阻止,它就不再消耗CPU的资源了。
1.2. 线程是如何工作的
线程被一个线程协调程序管理着(一个CLR委托给操作系统的函数)。
线程协调程序为所有活动的线程分配适当的执行时间;并且那些等待或阻止的线程(比如说在排它锁中、或在用户输入)都是不消耗CPU时间的。
在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。在多核的电脑中,多线程被实现成混合时间片和真实的并发,不同的线程在不同的CPU上运行。这几乎可以肯定仍然会出现一些时间切片, 由于操作系统的需要服务自己的线程,以及一些其他的应用程序。
线程由于外部因素被中断被称为被抢占,在大多数情况下,一个线程方面在被抢占的那一时那一刻就失去了对它的控制权。
1.3. 线程 vs. 进程
进程指一个应用程序所运行的操作系统单元。一个进程中可以包含多个线程。
线程于进程有某些相似的地方:都是以以时间片方式进行切换。
二者的关键区别在于进程彼此是完全隔绝的。线程与运行在相同程序其它线程共享(堆heap)内存。这就是线程为何如此有用:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。
1.4. 何时使用多线程
多线程程序一般被用来在后台执行耗时的任务。主线程保持运行,并且工作线程做它的后台工作。对于Windows Forms程序来说,如果主线程试图执行冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应。由于这个原因,应该在工作线程中运行一个耗时任务时添加一个工作线程,即使在主线程上有一个有好的提示“处理中…”,以防止工作无法继续。这就避免了程序出现由操作系统提示的“没有相应”,来诱使用户强制结束程序的进程而导致错误。模式对话框还允许实现“取消”功能,允许继续接收事件,而实际的任务已被工作线程完成。BackgroundWorker恰好可以辅助完成这一功能。
在没有用户界面的程序里,比如说Windows Service, 多线程在当一个任务有潜在的耗时,因为它在等待另台电脑的响应(比如一个应用服务器,数据库服务器,或者一个客户端)的实现特别有意义。用工作线程完成任务意味着主线程可以立即做其它的事情。
另一个多线程的用途是在方法中完成一个复杂的计算工作。这个方法会在多核的电脑上运行的更快,如果工作量被多个线程分开的话(使用Environment.ProcessorCount属性来侦测处理芯片的数量)。
一个C#程序称为多线程的可以通过2种方式:明确地创建和运行多线程,或者使用.NET framework的暗中使用了多线程的特性——比如BackgroundWorker类, 线程池,threadingtimer,远程服务器,或Web Services或ASP.NET程序。在后面的情况,人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即使有这样的事情;幸运的是,应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。
1.5. 何时不要使用多线程
多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程本身并不复杂,复杂是的线程的交互作用,这带来了无论是否交互是否是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的bugs。因此,要么多线程的交互设计简单一些,要么就根本不使用多线程。除非你有强烈的重写和调试欲望。
当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。在某些情况下,太多的I/O操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务块的多。稍后我们将实现生产者/耗费者 队列,它提供了上述功能。
1.6. 线程状态
可以通过ThreadState属性获取线程的执行状态。
Unstarted:线程未启动。
Running:运行状态。未被阻塞和挂起
WaitSleepJoin:线程处于等待状态,有三种情况使得线程处于该状态:(1)调用Sleep()(2)等待其他线程的join方法(3)线程同步过程中处于等待状态
Suspended:线程已经挂起,调用Suspend()方法可以进入该状态(已经过时,不建议使用)
SuspendRequested:已经调用了Suspend()方法,正在请求线程挂起(已经过时,不建议使用)
Aborted:线程已死,但是其状态尚未进入Stopped
AbortRequested:已经调用了Abort()方法,但是线程尚未进入Aborted状态。
Stopped:线程结束
StopRequested:请求线程结束,但尚未进入Stopped状态。
Background:表示线程是后台线程
- Thread应用
2.1. 线程对象的创建
2.1.1. 无传入参数的线程
线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托的定义:
public delegate void ThreadStart();
调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法创建TheadStart委托:
static void Main()
{
Threadt = new Thread (new ThreadStart (Go));
t.Start();
Go();
}
static void Go() { Console.WriteLine(“hello!”); }
在这个例子中,线程t执行Go()方法,大约与此同时主线程也调用了Go(),结果是两个几乎同时hello被打印出来:
也可是简写成如下代码:
static void Main()
{
Threadt = new Thread (Go);
t.Start();
Go();
}
static void Go() { Console.WriteLine(“hello!”); }
在这种情况,ThreadStart被编译器自动推断出来
2.1.2. 传入参数的线程
有三种方式实现传入参数的线程:
(1)使用ParameterizedThreadStart委托
.NET framework定义了另一个版本的委托叫做ParameterizedThreadStart,它可以接收一个单独的object类型参数:
public delegate void ParameterizedThreadStart(object obj);
程序示例如下:
static void Main()
{
Thread t = new Thread(new ParameterizedThreadStart (Go));
//可以简写成:
//Threadt = new Thread (Go);
//编译器自动推断出ParameterizedThreadStart委托
t.Start(true);
Go (false);
}
static void Go (object upperCase)
{
bool upper = (bool) upperCase;
Console.WriteLine (upper ?"HELLO!" : "hello!");
}
(二)使用匿名方法
一个替代方案是使用一个匿名方法调用一个普通的方法如下:
static void Main()
{
Threadt = new Thread (delegate() { WriteText ("Hello"); });
t.Start();
}
static void WriteText (string text) {Console.WriteLine (text); }
优点是目标方法可以接收任意数量的参数,并且没有装箱操作。不过这需要将一个外部变量放入到匿名方法中,向下面的一样:
static void Main()
{
string text ="Before";
Thread t = new Thread(delegate() { WriteText (text); });
text = "After";
t.Start();
}
static void WriteText (string text) { while(true){Console.WriteLine(text);} }
匿名方法打开了一种怪异的现象,当外部变量被后来的部分修改了值的时候,可能会透过外部变量进行无意的互动。
注意标红加粗的“可能”,举个例子:
借用上述的代码,如果在Main函数中这样写,
string text =“Before”;
Thread t = new Thread(delegate() { WriteText (text); });
text = “After”;
t.Start();
通常,输出结果为“After”。
如果代码如下:
text = “After”;
t.Start();
text = “Before”;
则输出有时全是“After”,有时全是“Before”。
猜测输出结果是以线程正式启动前最后的text值为准,虽然调用了t.Start()但线程t还没有正式启动的时候便执行了text = “Before”,则此时会一直输出“Before”。因此不建议使用这种方式启动线程。
(三)使用对象方法
另一种较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性可以告诉线程要做什么,如下列重写了原来的例子:
class ThreadTest {
bool upper;
static void Main() {
ThreadTest instance1 = new ThreadTest();
instance1.upper = true;
Thread t = new Thread (instance1.Go);
t.Start();
ThreadTest instance2 =new ThreadTest();
instance2.Go();
}
void Go() { Console.WriteLine (upper ?“HELLO!” : “hello!”); }
2.2. 常用方法
2.2.1. Start方法
void Start() 启动一个线程,该线程初始化中启用的方法无参数
void Start(object) 启动一个线程,并将一个参数传入入口函数,该入口函数有一个参数。
2.2.2. Sleep方法
这是一个静态方法,只能通过Thread.Sleep()调用。
Thread.Sleep放弃了占用CPU,请求不在被分配时间,线程进入WaitSleepJoin状态,直到给定的时间已过线程继续执行。
static void Main() {
Thread.Sleep(0); // 释放CPU时间片
Thread.Sleep(1000); // 休眠1000毫秒
Thread.Sleep(TimeSpan.FromHours (1)); // 休眠1小时
Thread.Sleep(Timeout.Infinite); // 休眠直到中断
}
2.2.3. Interrupt方法
在一个被阻止的线程(线程处于WaitSleepJoin状态)上调用Interrupt 方法,将强迫释放它,抛出ThreadInterruptedException异常,如下:
static void Main(string[] args)
{
Thread t = newThread(DoWork);
t.Start();
Thread.Sleep(500);
t.Interrupt();
Console.ReadKey();
}
static void DoWork()
{
int i = 0;
while (true)
{
i++;
try
{
Thread.Sleep(5000);
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
Console.WriteLine(i);
}
}
当线程调用Interrupt()方法,处于休眠状态的线程会进入catch块,执行完成catch块并不结束线程,线程会继续执行。
如果Interrupt被一个未阻止的线程调用,那么线程将继续执行
2.2.4. Abort方法
无论线程是否处于WaitSleepJoin状态,都将被强制停止。
被阻止的线程也通过Abort方法被强制释放,这与调用Interrupt相似。抛出 ThreadInterruptedException异常,异常将被抛出在catch里,在这期间线程的ThreadState为AbortRequested。被终止的线程将不再继续运行,除非调用Thread. ResetAbort()。如:
static void Main(string[] args)
{
Thread t = newThread(DoWork);
t.Start();
Thread.Sleep(500);
t.Abort();
Console.ReadKey();
}
static void DoWork()
{
int i = 0;
while (true)
{
i++;
try
{
Thread.Sleep(5000);
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
Thread.ResetAbort()//可以使线程不被终止,继续运行。
}
Console.WriteLine(i);
}
}
调用Abort()方法,进入catch块如果没有Thread. ResetAbort()则线程终止。如果有Thread. ResetAbort(),则线程继续执行while循环。
2.2.5. Join方法
(1)void Join()
通过Join方法阻止线程直到另一个线程结束:
static void Main() {
Thread t = newThread (delegate() { Console.ReadLine();});
t.Start();
t.Join(); // 等待直到线程完成
Console.WriteLine(“Thread t’s ReadLine complete!”);
}
主线程要等到t线程执行完毕后才输出“Thread t’s ReadLine complete!”
(2)bool Join(int)
当前线程等待调用Join的线程执行完,int参数为指定等待时间。
如果在指定时间内,线程执行完毕,则直接返回true,当前线程继续执行。
如果在指定时间内线程没有执行完成,则直接返回false,当前线程不再阻塞在Join方法上,继续执行。
2.3. 前台线程与后台线程
线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。C#也支持后台线程,当所有前台线程结束后,它们不维持程序的存活。
改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。
线程的IsBackground属性控制它的前后台状态,如下实例:
class PriorityTest {
static void Main (string[] args) {
Thread worker = new Thread(delegate() { while(true){Thread.Sleep(1000)}});
worker.Start();
Console.ReadKey();
}
}
主线程启动工作线程后一直等待用户输入。工作线程一直循环休眠。如果工作线程为前台线程,则用户触发回车,主线程退出但是程序保持运行。如果工作线程为后台线程,则用户触发回车,主线程退出,程序退出。
如果是Windows Form程序,如果有一个线程是前台线程。当关闭Windows窗口后,程序不会退出,因为有一个前台线程还在运行。如果线程为后台线程,则关闭Windows窗口后,后台线程自动退出,程序结束运行。
因此在编写多线程程序时,尽量使用后台线程,尤其对Windows Forms程序,对用户来说,关闭程序窗口,程序就应该结束。如果使用了前台线程,则关闭了窗口,它的进程仍然运行着。在Windows任务管理器它将从应用程序栏消失不见,但却可以在进程栏找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行。
对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。
2.4. 线程优先级
线程的Priority 属性确定了线程相对于其它同一进程的活动的线程拥有多少执行时间,以下是级别:
num ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal,Highest }
只有多个线程同时为活动时,优先级才有作用。
系统优先执行优先级较高的线程,但这只意味着优先级较高的线程占有更多的CPU时间,并不意味着一定要先执行完优先级较高的线程,才会执行优先级较低的线程。
设置一个线程的优先级为高一些,并不意味着它能执行实时的工作,因为它受限于程序的进程的级别。要执行实时的工作,必须提升在System.Diagnostics 命名空间下Process的级别,像下面这样:
Process.GetCurrentProcess().PriorityClass =ProcessPriorityClass.High;
如果一个实时的程序有一个用户界面,提升进程的级别是不太好的,因为当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。降低主线程的级别、提升进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑越来越慢,因为操作系统仍会拨出过多的CPU给整个进程。最理想的方案是使实时工作和用户界面在不同的进程(拥有不同的优先级)运行,通过Remoting或共享内存方式进行通信。
2.5. 异常处理
任何线程创建范围内try/catch/finally块,当线程开始执行便不再与其有任何关系。考虑下面的程序:
public static void Main()
{
try
{
new Thread(Go).Start();
}
catch (Exception ex)
{
// 不会在这得到异常
Console.WriteLine("Exception!");
}
}
static void Go() { throw null; }
当线程抛出异常,不会在Main方法的catch中捕获。补救方法是将try-catch放在Go方法中:
public static void Main()
{
new Thread (Go).Start();
}
static void Go()
{
try
{
...
throw null; // 这个异常在下面会被捕捉到
...
}
catch (Exception ex)
{
记录异常日志,并且或通知另一个线程
我们发生错误
...
}
}
从.NET 2.0开始,任何线程内的未处理的异常都将导致整个程序关闭,这意味着忽略异常不再是一个选项了。因此为了避免由未处理异常引起的程序崩溃,try/catch块需要出现在每个线程进入的方法内。
实际用法:
1、线程创建
2、线程执行的内容
3、线程结束