一、 线程的概念
默认情况下,C# 程序具有一个线程。此线程执行程序中以Main方法开始和结束的代码。Main直接或间接执行的每一个命令都由默认线程(或主线程)执行,当Main返回时此线程也将终止。不过,可以创建辅助线程,以便与主线程一起并行执行代码。这些线程通常称为“辅助线程”。
辅助线程可以用于执行耗时较多的任务或时间要求紧迫的任务,而不必占用主线程。例如,辅助线程经常用在服务器应用程序中,以便无需等待前面的请求完成即可响应传入的请求。辅助线程还可用于在桌面应用程序中执行“后台”任务,以便使主线程(用于驱动用户界面元素)保持对用户操作的响应。
多线程处理解决了吞吐量和响应性的问题,但同时也带来了资源共享问题,如死锁和争用状态。多线程特别适用于需要不同资源(如文件句柄和网络连接)的任务。为单个资源分配多个线程可能会导致同步问题,线程会被频繁阻止以等待其他线程,从而与使用多线程的初衷背道而驰。
常见的策略是使用辅助线程执行不需要大量占用其他线程所使用的资源的、耗时较多的任务或时间要求紧迫的任务。实际上,程序中的某些资源必须由多个线程访问。考虑到这些情况,System.Threading 命名空间提供了用于同步线程的类。
二、 线程的优先级
Windows之所以被称为一种抢占式多线程操作系统,是因为线程可以任何时间停止,并调度另一个线程。
Windows为每个线程分配了从0(最低)~31(最高)的一个优先级,系统决定将哪个线程分配给一个CPU时,它首先会检查优先级为31的线程,并一种轮流的方式调度它们,如果一个优先级为31的线程是可调用的,就把它分配给一个CPU,在这个线程的时间片结束时,系统检查是否有另一个优先级为31的线程可以运行,如果是,就允许将那个线程分配给一个CPU。只要存在可以存在调度的31的线程,那么系统就永远不会将优先级0至30的任何线程分配给CPU,这种情况称为“饥饿”。当较高优先级线程点用了太多CPU时间,至使较低优先级的线程无法运行时,就会发生这种情况,因此在用户给一个线程设定优先级时,是需要慎重考虑的,而且开发人员在为线程分配线程优先级时,很难做到完全合理,你无法确定这个线程的优先级应该设为10吗?另一个线程为25?因此为了解决这个问题,微软公开了一个优先级的抽象层。
Windows支持7个相对线程优先级:Idle、Lowest、BelowNormal、Normal、Above Normal、Highest、Time-Critical,为什么是相对线程优先级,这是因为这些优先级是相对进程优先级来区分的,因为不同进程优先级之间相同的线程优先级不是在相同环境进行比较的,对于进程优先级,这里不做说明,但我们可以《CLR VIA C#》中的一个图表来清楚的知道它们之间的关系
三、线程的创建
我们可以通过System.Threading命名空间下的Thread类,来使用线程.
对于Thread的构造,我们一般使用的是以下这两种:
Thread(ParameterizedThreadStart start)
Thread(ThreadStart start)
ParameterizedThreadStart 带有参数的委托类型
ThreadStart 无参的委托类型
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thd1 = new Thread(HasOneParaMethod);
Thread thd2 = new Thread(NonParaMethod);
thd1.Start("hello world");//启动线程,并传递参数
thd2.Start();//启动线程¨
Console.WriteLine("this is main thread");
Console.Read();
}
static void HasOneParaMethod(object obj)
{
Thread.Sleep(1000);//模拟工作1秒
Console.WriteLine("This is method with one para and arg = {0}", obj);
}
static void NonParaMethod()
{
Thread.Sleep(2000);//模拟工作2秒
Console.WriteLine("This is NonePara Method");
}
}
}
输出结果:
四、前台线程与后台线程
CLR将每个线程要么视为前台线程,要么视为后台线程。一个进程中的所有台线程停止运行时,CLR将强制终止仍在运行的后台线程,这些后台线程被直接终止,并且不会抛出异常。CLR创建一个线程时,默认为前台线程,但我们可以更改Thread类中的BOOL属性IsBackground 来设置该线程是属于前台或者是后台线程。
代码示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
//创建一个新的线程,且默认为前台¬线程
Thread thd = new Thread(Worker);
//使之成为后台线
thd.IsBackground = true;
//启动线程
thd.Start();
//如果线程为前台线程,则应用程序会在¨秒后输出Running in Worker后停止
//如果线程为后台线程,应|用程序在输出Running in Main后立即终止
Console.WriteLine("Running in Main");
//注意,如果添加以下这行代码,则表明主线程需要等待一个IO输入之后才会结束
//因此如果用户不进行IO输入时,则2秒后,后台线程输出Running Main,这是因为前台
//线程还未停止
//Console.Read();
}
static void Worker()
{
Thread.Sleep(2000);//模拟工作秒2
Console.WriteLine("Running in Worker");
}
}
}
了解前台线程和后台线程,那么对前台线程与后台线程的适用场景,我们要有一个大概的认识,前台线程应该用于执行确实想完成的任务,比如将数据从内存缓冲区flush到磁盘,另外,应该为非关键的任务使用后台线程。
五、线程的控制
1、 线程休眠
Thread.Sleep、AutoEventHanle.WaitOne、MaunalEventHanle.WaitOne 等这些方法都可导致线程休眠,即阻塞,在这节我仅讨论Thread.Sleep,另外两个方法打算放到后续的线程同步里面来讨论。
当在线程内部调用Sleep方法时,会导致此线程将会阻塞一段时间,多长时间有Sleep的参数决定,当参数为Timeout.Infinite时,将会无限阻塞(直至外部显示调用类似Interrupt方法调用),当调用完Sleep后,线程的状态将为WaitSleepJoin状态
另外要注意的,Sleep方法为静态方法,说明他不能由某个线程实例发起调用,这个原因我在网上找到有这样的一段理解:“sleep()函数只能由需sleep的线程自己调用,不允许其他线程调用,正如when to sleep是个人私事不能由他人决定”
我的理解是,一般如果是主叫线程(例如Main方法处的主线程)能控制某个子线程的行为,则为实例方法,比如Interrupt、Abort、Join等,对于由线程本身的行为,不受主叫线程控制的,则为静态方法。为什么呢,想这种情况,一个线程内部调用了Sleep(-1)无限休眠,因此只能在外部调用该线程的某个方法来进行中断,而线程实例则标识我要中断的那个线程。
对于线程休眠的一段代码如下:using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thd = new Thread(Worker);
thd.Start();
Thread.Sleep(10);//保证子线程先执行
//将会在5秒后,输出wake up
Console.WriteLine("running in main");
Console.ReadLine();
}
static void Worker()
{
Console.WriteLine("running in worker,but the thread will be sleep 5s at next step");
Thread.Sleep(5000);
Console.WriteLine("wake up");
}
}
}
输出结果:
2、线程的挂起与恢复
Thread.Suspend 挂起
Thread.Resume 恢复
这两个方法在.net Framework 1.0的时候就支持的方法,他们分别可以挂起线程和恢复挂起的线程。但在.net Framework 2.0以后的版本中这两个方法都过时了,MSDN的解释是这样:
不要使用Suspend 和Resume方法来同步线程的活动。您无法知道挂起线程时它正在执行什么代码。如果您在安全权限评估期间挂起持有锁的线程,则 AppDomain中的其他线程可能被阻止。如果您在线程正在执行类构造函数时挂起它,则AppDomain中尝试使用该类的其他线程将被阻止。这样很容易发生死锁。
正常使用场景,如下代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thd = new Thread(Worker);
thd.Name = "Thread1";
thd.Start();
Console.WriteLine("Main Thread is running");
//进行一个IO输入后,就会恢复Thread1线程
Console.ReadLine();
thd.Resume();
}
static void Worker()
{
Console.WriteLine("Thread:{0},has been suspended");
Thread.CurrentThread.Suspend();//挂起线程,当进行一个IO输入后,该线程被恢复
Console.WriteLine("Thread:{0},has been resumed");
}
}
}
输出结果:
当我们进行一个Console.ReadLine后,Thread1线程就会继续,输出如下:
以上都是与预期相关并且正常使用的情况下,使用这两种方法时,需要开发人员很清楚这两个方法不会产生一些异常的情况,并且十分小心使用
以下代码体现了,如果对于过程控制不严谨可能带来不可预知的异常
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thd = new Thread(Worker);
thd.Name = "Thread1";
thd.Start();
Thread.Sleep(1000); //保证子线程先于SomeWork执行
Console.WriteLine("Main Thread is running");
//执行某个过程,预期不会产生异常,但实际却可能产生异常
//产生异常后,则后续的thd.Resume则无法执行
//因此会造成thd线程会一直处于挂起状态,thd所使用的资源将得不到释放
SomeWork();
thd.Resume(); //恢复挂起的线程
}
/// <summary>
///模拟主线程中执行的某个过程,并且产生了异常
/// </summary>
static void SomeWork()
{
int a = 0;
float f = 3 / a;
}
static void Worker()
{
Console.WriteLine("Thread:{0},has been suspended");
Thread.CurrentThread.Suspend();
Console.WriteLine("Thread:{0},has been resumed");
}
}
}
开发人员原本是期望在挂起某个线程后,能在主叫线程中正常恢复挂起的子线程,但实际过程中在挂起之前,可能程序会提前进行中止。
并且我们注意到,在使用Suspend与Resume方法时,编译器会给出我们两个警告:
当然以上这个例子用来说明Suspend与Resume方法过时,还不够充分,因为就算在我们在恢复线程之前主叫线程发生了异常,导致后续恢复子线程无法进行,但由于异常已经导致了应用程序中止,我们是很容易知道这个错误。
那么我们通过以下代码更能清楚的印证MSDN给出的解释:using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thd = new Thread(Worker);
thd.Name = "Thread1";
thd.Start();
Console.WriteLine("Main Thread is running");
//此时Thread1线程还处于5秒的休眠之中,并未挂起
//因此在此尝试恢复线程时,会产生一个异常
thd.Resume();
}
static void Worker()
{
Console.WriteLine("Thread:{0},has been suspended");
Thread.Sleep(5000);
Thread.CurrentThread.Suspend();
Console.WriteLine("Thread:{0},has been resumed");
}
}
}
当主叫线程执行的速度比子线程快时,在子线程还未挂起的时候,主叫线程尝试去恢复线程时,此时就会出现异常了,因此对于Suspend与Resume方法,我们开发过程中要避免使用
3、线程阻塞
Thread.Join
MSDN对此方法的解释是,阻塞调用线程,直至调用该方法的线程调用中止。
当一个线程的后续执行是需要等待另外两个线程结束后才能开始时,就可调用这个方法
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main(string[] args)
{
//初始主线程即调用线程的名称
Thread.CurrentThread.Name = "Main";
Person p = new Person();
//Thread1线程用来初始姓名
Thread thd1 = new Thread(SetPersonName);
thd1.Name = "Thread1";
//Thread2线程用来初始年龄
Thread thd2 = new Thread(SetPersonAge);
thd2.Name = "Thread2";
thd1.Start(p);
thd2.Start(p);
Console.WriteLine("Now Time : {0}", DateTime.Now);
thd1.Join();//阻塞Main线程,直至Thread1线程结束
Console.WriteLine("Person's Name is seted and now time :{0}", DateTime.Now);
thd2.Join(); //阻塞Main线程,直至Thread2线程结束
Console.WriteLine("Person's Age is seted and now time :{0}", DateTime.Now);
//当Thread1和Thread2线程都结束时,Person的Name和Age都确保初始完成
Console.WriteLine("Pesron Name:{0},Person Age:{1}", p.Name, p.Age);
Console.ReadLine();
}
static void SetPersonName(object obj)
{
Thread.Sleep(1000);
Person p = obj as Person;
p.Name = "July";
}
static void SetPersonAge(object obj)
{
Thread.Sleep(2000);
Person p = obj as Person;
p.Age = 25;
}
}
}
输出结果:
我用两个线程来初始化一个Person对象,并且需要在它们都初始完成之后,主线程再访问Person对象,因此,我们可以通过调用Join()方法,阻塞主线程,直至子线程执行结束
4、线程中止与线程终止
Thread.Abort和Thread.Interrupt
之所以把这两个方法放在一起讲,主要是他们都可以进行线程中断,但他们又有些区别。
Abort是指销毁一个线程,它会在被销毁的线程中引发ThreadabortException异常,我们可以把线程内的一些代码放在try块内,并把相应的处理代码放入相应的catch块内,当线程执行try块内代码时,被调用Abort方法时,即会跳入catch块内执行,执行完catch块代码后会立即终止(如有finally块,则执行完finally块代码)
Interrupt是指唤醒(也可称为中断)一个正在休眠的线程,通俗一点讲,一个线程如果此时正处于休眠(WaitSleepJoin)状态,如果想提前唤醒该线程(即让此线程执行休眠后的代码),此时可通过Interrupt方法,它能唤醒由Sleep导致的线程阻塞,Interrupt方法会在线程内部引发ThreadInterrupt异常,我们可以把导致线程阻塞的代码放入try块,而把相应的中断处理代码放入catch块内,当一个线程处于WaitSleepJoin状态时,在外部调用此线程的Interrupt方法时,会立即跳入线程内的catch块内执行,执行完catch,finally块内容后,会继续执后续的代码,注意,Interrupt的作用,仅仅是起唤醒线程的作用
我们来看一个Abort方法的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread abortThread = new Thread(Worker);
abortThread.Name = "AbortThread";
abortThread.Start();
Thread.Sleep(100);// 保证AbortThread线程执行
//此时abortThread线程状态为WaitSleepJoin状态
Console.WriteLine("{0}'s Status = {1}", abortThread.Name, abortThread.ThreadState);
abortThread.Abort();//此时abortThread线程将永久阻塞,调用Abort方法中止线程
abortThread.Join();
Console.WriteLine("{0}'s Status = {1} in Main", abortThread.Name, abortThread.ThreadState);
Console.ReadLine();
}
static void Worker()
{
try
{
Thread.Sleep(Timeout.Infinite);// 线程将永远处理Sleep阻塞状态
}
catch (Exception e)
{
Console.WriteLine("{0} happen in {1}", e.GetType(), Thread.CurrentThread.Name);
Console.WriteLine("{0}'s Status = {1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
finally
{
Console.WriteLine("{0}'s Status = {1} in finally block", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
//注意以下代码
//如果是Abort方法中断,则以下代码不会执行
//如果是Interrupt方法中断,则以下代码还会执行
Console.WriteLine("{0}'s Status = {1} and continue to work", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
}
}
输出结果:
大家注意一下,进行Abort方法中断后,被调用线程会立即执行catch和finally块并销毁,并不会执行后续的代码
再来看一个Interrupt方法的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread interruptThread = new Thread(Worker);
interruptThread.Name = "interruptThread";
interruptThread.Start();
Thread.Sleep(100);/ 保证interruptThread线程执行
//此时interruptThread线程状态为WaitSleepJoin状态
Console.WriteLine("{0}'s Status = {1}", interruptThread.Name, interruptThread.ThreadState);
interruptThread.Interrupt();//此时interruptThread线程将永久阻塞,调用Interrupt方法唤醒线程
interruptThread.Join();
Console.WriteLine("{0}'s Status = {1} in Main", interruptThread.Name, interruptThread.ThreadState);
Console.ReadLine();
}
static void Worker()
{
try
{
Thread.Sleep(Timeout.Infinite);// 线程将永远处理Sleep阻塞状态
}
catch (Exception e)
{
Console.WriteLine("{0} happen in {1}", e.GetType(), Thread.CurrentThread.Name);
Console.WriteLine("{0}'s Status = {1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
finally
{
Console.WriteLine("{0}'s Status = {1} in finally block", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
//注意以下代码
//如果是Abort方法中断,则以下代码不会执行
//如果是Interrupt方法中断,则以下代码还会执行
Console.WriteLine("{0}'s Status = {1} and continue to work", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
}
}
输出结果:
注意红色标识的地方,在调用Interrupt方法后,线程从休眠状态中断回来,进入catch、finally块后,注意一下此时它的状态,仍然是Running状态,线程并不销毁,并且线程将会继续执行,会输出红色标识的语句。
通过以Abort和Interrupt代码,相信大家现在对它们之间的区别有了一个清晰的了解了。
在使用这两个方法的过程中,还有一些要注意的地方,如下:
1)、对于调用了一个线程的Abort方法后,可在线程内部通过ResetAbort方法取消销毁,在取消后,在该线程执行完后,它的状态是Stop,而不是Abort,这一点大家一定要注意,如下代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread abortThread = new Thread(Worker);
abortThread.Name = "AbortThread";
abortThread.Start();
Thread.Sleep(100);// 保证AbortThread线程执行
//此时abortThread线程状态为WaitSleepJoin状态
Console.WriteLine("{0}'s Status = {1}", abortThread.Name, abortThread.ThreadState);
abortThread.Abort();//此时abortThread线程将永久阻塞,调用Abort方法中止线程
abortThread.Join();
Console.WriteLine("{0}'s Status = {1} in Main", abortThread.Name, abortThread.ThreadState);
Console.ReadLine();
}
static void Worker()
{
try
{
Thread.Sleep(Timeout.Infinite);// 线程将永远处理Sleep阻塞状态
}
catch (Exception e)
{
Console.WriteLine("{0} happen in {1}", e.GetType(), Thread.CurrentThread.Name);
Console.WriteLine("{0}'s Status = {1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
Thread.ResetAbort();//取消销毁
}
finally
{
Console.WriteLine("{0}'s Status = {1} in finally block", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
//注意以下代码
//线程调用了ResetAbort方法,以下代码将会执行
Console.WriteLine("{0}'s Status = {1} and continue to work", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
}
}
输出结果:
2)、对某一线程调用interrupt,如他正处于WaitSleepJoin状态,则进入相应的中断处理程式执行,若此时他不处于WaitSleepJoin状态,则他后来进入此状态时,将被即时中断。若在中断前调用几次interrupt,只有第一次调用有效,如下面代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread interruptThread = new Thread(Worker);
interruptThread.Name = "InterruptThread";
Console.WriteLine("{0}'s Status = {1}", interruptThread.Name, interruptThread.ThreadState);
//此时interruptThread状态为UnStart状态,即还没有进入WaitSleepJoin状态
//若他后来进入WaitSleepJoin状态,则进入相应的处理,即被立即中断
//若在中断之前调用几次Interrupt,只对第一次调用有效
interruptThread.Interrupt();
interruptThread.Interrupt();
interruptThread.Start();
Console.ReadLine();
}
static void Worker()
{
try
{
Thread.Sleep(Timeout.Infinite);
}
//只中断第一次
catch (Exception e)
{
Console.WriteLine("1th ThreadInterruptException happen");
}
try
{
Thread.Sleep(Timeout.Infinite);
}
catch (Exception ex)
{
Console.WriteLine("2th ThreadInterruptException happen");
}
}
}
}
输出结果:
注意输出结果,只输出第一次中断后的处理代码,后续线程将继续处理WaitSleepJoin状态