操作系统通过线程对程序的执行进行管理,当操作系统运行一个程序的时候,首先,操作系统将为这个准备运行的程序分配一个进程,以管理这个程序所需要的各种资源。在这些资源之中,会包含一个称为主线程的线程数据结构,用来管理这个程序的执行状态。
在Windows操作系统下,线程的的数据结构包含以下内容:
1、线程的核心对象:主要包含线程当前的寄存器状态,当操作系统调度这个线程开始运行的时候,寄存器的状态将被加载到CPU中,重新构建线程的执行环境,当线程被调度出来的时候,最后的寄存器状态被重新保存到这里,已备下一次执行的时候使用。
2、线程环境块(Thread Environment Block,TED):是一块用户模式下的内存,包含线程的异常处理链的头部。另外,线程的局部存储数据(Thread Local Storage Data)也存在这里。
3、用户模式的堆栈:用户程序的局部变量和参数传递所使用的堆栈,默认情况下,Windows将会被分配1M的空间用于用户模式堆栈。
4、内核模式堆栈:用于访问操作系统时使用的堆栈。
在抢先式多任务的环境下,在一个特定的时间,CPU将一个线程调度进CPU中执行,这个线程最多将会运行一个时间片的时间长度,当时间片到期之后,操作系统将这个线程调度出CPU,将另外一个线程调度进CPU,我们通常称这种操作为上下文切换。
在每一次的上下文切换时,Windows将执行下面的步骤:
- 将当前的CPU寄存器的值保存到当前运行的线程数据结构中,即其中的线程核心对象中。
- 选中下一个准备运行的线程,如果这个线程处于不同的进程中,那么,还必须首先切换虚拟地址空间。
- 加载准备运行线程的CPU寄存器状态到CPU中。
公共语言运行时CLR(Common Language Runtime)是.Net程序运行的环境,它负责资源管理,并保证应用和底层操作系统之间必要的分离。
在.Net环境下,CLR中的线程需要通过操作系统的线程完成实际的工作,目前情况下,.Net直接将CLR中的线程映射到操作系统的线程进行处理和调度,所以,我们每创建一个线程将会消耗1M以上的内存空间。但未来CLR中的线程并不一定与操作系统中的线程完全对应。通过创建CLR环境下的逻辑线程,我们可能创建更加节省资源的线程,使得大量的CLR线程可以工作在少量的操作系统线程之上。
一、线程的定义
在单CPU系统的一个单位时间(time slice)内,CPU只能运行单个线程,运行顺序取决于线程的优先级别。如果在单位时间内线程未能完成执行,系统就会把线程的状态信息保持到线程的本地存储器(TLS)中,以便下次执行时恢复执行。而多线程只是系统带来的一个假象,它在多个单位时间内进行多个线程的切换,因为切换频密而且单位时间非常短暂,所以多线程被视作同时运行。
适当使用多线程能提高系统的性能,比如:在系统请求大容量的数据时使用多线程,把数据输出工作交给异步线程,使主线程保持其稳定性去处理其他问题。但需要注意一点,因为CPU需要花费不少的时间在线程的切换上,所以过多地使用多线程反而会导致性能的下降。
1、System.Threading 命名空间中的常用类
在System.Threading命名空间内提供多个方法来构建多线程应用程序,其中ThreadPool与Thread是多线程开发中最常用到的,在.NET中专门设定了一个CLR线程池专门用于管理线程的运行,这个CLR线程池正是通过ThreadPool类来管理,而Thread是管理线程的最直接方式。
类 | 说明 |
AutoResetEvent | 通知正在等待的线程已发生事件 |
ManualResetEvent | 通知正在等待的线程已发生事件 |
Interlocked | 为多个线程共享的变量提供原子操作 |
Monitor | 提供同步对对象的访问的机制 |
Mutex | 一个同步基元,也可用于进程间同步 |
Thread | 创建并控制线程,设置其优先级并获取其状态 |
ThreadPool | 提供一个线程池,该线程池可用于发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器 |
WaitHandle | 封装等待对共享资源的独占访问的操作系统特定的对象 |
ReadWriterLock | 读写锁 |
Semaphore | 控制线程的访问数量 |
二、线程的优先级
为了方便线程的管理,线程有个优先级,优先级用于决定哪个线程优先执行,在Thread对象中就是Priority属性。
优先级由低到高分别是:
优先级 | 说明 |
Lowest | 可以将 Thread 安排在具有任何其他优先级的线程之后 |
BelowNormal | 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前 |
Normal | 默认值。可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前 |
AboveNormal | 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前 |
Highest | 可以将 Thread 安排在具有任何其他优先级的线程之前 |
先来看一个优先级的示例:
class Program { static void Main(string[] args) { //新建3个线程并设定各自的优先级 Thread t1 = new Thread(Run); t1.Priority = ThreadPriority.Lowest; Thread t2 = new Thread(Run); t2.Priority = ThreadPriority.Normal; Thread t3 = new Thread(Run); t3.Priority = ThreadPriority.Highest; //由低到高优先级的顺序依次调用 t1.Start(); t2.Start(); t3.Start(); Console.ReadKey(); } public static void Run() { Console.WriteLine("我的优先级是:" + Thread.CurrentThread.Priority); } }
来看输出:
留意到线程是按照优先级的顺序执行的。
三、常用属性
常用属性 | 说明 |
CurrentThread | 获取当前正在运行的线程 |
IsAlive | 获取一个值,该值指示当前线程的执行状态 |
IsBackground | 获取或设置一个值,该值指示某个线程是否为后台线程, 后台线程会随前台线程的关闭而退出 |
IsThreadPoolThread | 获取一个值,该值指示线程是否属于托管线程池 |
ManagedThreadId | 获取当前托管线程的唯一标识符 |
Name | 获取或设置线程的名称 |
Priority | 获取或设置一个值,该值指示线程的调度优先级 |
ThreadState | 获取一个值,该值包含当前线程的状态 |
ManagedThreadId是确认线程的唯一标识符,程序在大部分情况下都是通过Thread.ManagedThreadId来辨别线程的。不能够通过Name,因为Name只是一个简单的属性,可以随便改,不能保证无重复。
常用属性示例:
class Program { static void Main(string[] args) { //新建3个线程并设定各自的优先级 Thread t1 = new Thread(Run); t1.Priority = ThreadPriority.Normal; t1.Start(); Console.ReadKey(); } public static void Run() { Thread t1 = Thread.CurrentThread; //静态属性,获取当前执行这行代码的线程 Console.WriteLine("我的优先级是:" + t1.Priority); Console.WriteLine("我是否还在执行:" + t1.IsAlive); Console.WriteLine("是否是后台线程:" + t1.IsBackground); Console.WriteLine("是否是线程池线程:" + t1.IsThreadPoolThread); Console.WriteLine("线程唯一标识符:" + t1.ManagedThreadId); Console.WriteLine("我的名称是:" + t1.Name); Console.WriteLine("我的状态是:" + t1.ThreadState); } }
输出如下:
1、前台线程与后台线程的区别
我们看到上面有个属性叫后台线程,非后台线程就叫前台线程吧,Thread.Start()启动的线程默认为前台线程,启动程序时创建的主线程一定是前台线程。应用程序与必须等到所有的前台线程执行完毕才会卸载。而当IsBackground设置为true时,就是后台线程了,当主线程执行完毕后就直接卸载,不再理会后台线程是否执行完毕。
前台与后台线程的设置必须在线程启动之前进行设置,线程启动之后就不能设置了。
Thread创建的线程是前台线程,线程池中的是后台线程。
class Program { static void Main(string[] args) { Thread t1 = new Thread(Run); t1.IsBackground = true; //设为后台线程 t1.Start(); Console.WriteLine("不等你咯,后台线程!"); //注意这里不要Console.Readxxx();,让控制台执行完毕就自动关闭 } public static void Run() { Thread.Sleep(5000); Console.WriteLine("后台线程正在执行!"); } }
前台线程与后台线程的区别如下,上面的示例没法用图片来说明,简要说发生的情况。当t1设置为前台线程时,5秒后,控制台窗口才关闭。如果t1设置为后台线程,则窗口瞬间就关闭了。
2、ThreadState的状态
对于ThreadState的值有以下几种:
线程状态 | 说明 |
Aborted | 线程已停止 |
AbortRequested | 线程的Thread.Abort()方法已被调用,但是线程还未停止 |
Background | 线程在后台执行,与属性Thread.IsBackground有关 |
Running | 线程正在正常运行 |
Stopped | 线程已经被停止 |
StopRequested | 线程正在被要求停止 |
Suspended | 线程已经被挂起(此状态下,可以通过调用Resume()方法重新运行) |
SuspendRequested | 线程正在要求被挂起,但是未来得及响应 |
Unstarted | 未调用Thread.Start()开始线程的运行 |
WaitSleepJoin | 线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态 |
线程在以上几种状态的切换如下:
刚刚创建的线程处于已经准备好运行,但是还没有运行的状态,称为Ready(准备)状态。在操作系统的调度之下,这个线程可以进入(Runing)运行状态。运行状态的线程可能因为时间片用完的缘故被操作系统切换出CPU,称为Suspended(暂停运行)状态,也可能在时间片还没有用完的情况下,因为等待其他优先级更高的任务,而转换到Blocked(阻塞)状态。在阻塞状态下的线程,随时可以因为再次调度而重新进入运行状态。线程还可能通过Sleep方法进入Sleep(睡眠)状态,当睡眠时间到期之后,可以再次被调度运行。处于运行状态的线程还可能被主动终止执行,直接结束;也可能因为任务已经完成,被操作系统正常结束。
四、方法
方法 | 说明 |
Abort | 终止线程 |
GetDomain | 当前线程运行在的应用程序域 |
GetDomainID | 唯一的应用程序域标识符 |
Interrupt | 中断处于 WaitSleepJoin 线程状态的线程 |
Join | 阻塞调用线程,直到某个线程终止时为止 |
ResetAbort | 取消为当前线程请求的 Abort |
Sleep | 将当前线程阻塞指定的毫秒数 |
SpinWait | 导致线程等待由 iterations 参数定义的时间量 |
Start | 启动线程以按计划执行 |
1、Join串行执行
Join,串行执行,相当于ajax里面的async:false
class Program { static void Main(string[] args) { Thread t1 = new Thread(Run); t1.Name = "t1"; t1.Start(); t1.Join(); //等待t1执行完之后,主线程再执行,线程间的关系为串行,非并行 Console.WriteLine("主线程执行这了么?"); Console.ReadKey(); } public static void Run() { Console.WriteLine("线程" + Thread.CurrentThread.Name + "开始执行!"); Thread.Sleep(5000); Console.WriteLine("线程" + Thread.CurrentThread.Name + "执行完毕!"); } }
输出:
2、Interrupt 与 Abort
Interrupt和Abort:这两个关键字都是用来强制终止线程,不过两者还是有区别的。
1、Interrupt: 抛出的是 ThreadInterruptedException 异常。
Abort: 抛出的是 ThreadAbortException 异常。
2、Interrupt:如果终止工作线程,只能管到一次,工作线程的下一次sleep就管不到了,相当于一个contine操作。如果线程正在sleep状态,则通过Interrypt跳过一次此状态也能够达到唤醒效果。
Abort:这个就是相当于一个break操作,工作线程彻底停止掉。 当然,你也已在catch(ThreadAbortException ex){...} 中调用Thread.ResetAbort()取消终止。
class Program { static void Main(string[] args) { Thread t1 = new Thread(Run); t1.Start(); //当Interrup时,线程已进入for循环,中断第一次之后,第二次循环无法再停止 t1.Interrupt(); t1.Join(); Console.WriteLine("============================================================"); Thread t2 = new Thread(Run); t2.Start(); //停止1秒的目的是为了让线程t2开始,否则t2都没开始就直接中止了 Thread.Sleep(1000); //直接终止掉线程,线程被终止,自然无法输出什么! t2.Abort(); Console.ReadKey(); } static void Run() { for (int i = 1; i <= 5; i++) { try { //连续睡眠5次 Thread.Sleep(2000); Console.WriteLine("第" + i + "次Sleep!"); } catch (Exception e) { Console.WriteLine("第" + i + "次Sleep被中断!" + " " + e.Message); } } } }
输出:
3、Suspend 与 Resume (慎用)
Thread.Suspend()与 Thread.Resume()是在Framework1.0 就已经存在的老方法了,它们分别可以挂起、恢复线程。但在Framework2.0中就已经明确排斥这两个方法。这是因为一旦某个线程占用了已有的资源,再使用Suspend()使线程长期处于挂起状态,当在其他线程调用这些资源的时候就会引起死锁!所以在没有必要的情况下应该避免使用这两个方法。在MSDN中,这两个方法也已被标记为已过时。
五、ThreadStart委托
ThreadStart所生成并不受线程池管理。
通过ThreadStart委托启动线程:
class Program { static void Main(string[] args) { Console.WriteLine("主线程Id是:" + Thread.CurrentThread.ManagedThreadId); Message message = new Message(); Thread thread = new Thread(new ThreadStart(message.ShowMessage)); thread.Start(); Console.WriteLine("正在做某事......"); Console.WriteLine("主线程工作完成!"); Console.ReadKey(); } public class Message { public void ShowMessage() { string message = string.Format("异步线程Id是:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); for (int i = 0; i < 10; i++) { Thread.Sleep(300); Console.WriteLine("异步线程当前循环执行到" + i); } } } }
输出:
六、ParameterizedThreadStart委托
ParameterizeThreadStart委托于ThreadStart委托非常相似,但ParameterizedThreadStart委托是面向带参数方法的。注意ParameterizedThreadStart对应方法的参数为object。
class Person { public Person(string name, int age){ this.Name = name;this.Age = age; } public string Name { get; set; } public int Age { get; set; } } class Program { static void Main(string[] args) { //整数作为参数 for (int i = 0; i < 2; i++) { Thread t = new Thread(new ParameterizedThreadStart(Run)); t.Start(i); } Console.WriteLine("主线程执行完毕!"); //自定义类型作为参数 Person p1 = new Person("关羽", 22); Person p2 = new Person("张飞", 21); Thread t1 = new Thread(new ParameterizedThreadStart(RunP)); t1.Start(p1); Thread t2 = new Thread(new ParameterizedThreadStart(RunP)); t2.Start(p2); Console.ReadKey(); } public static void Run(object i) { Thread.Sleep(50); Console.WriteLine("线程传进来的参数是:" + i.ToString()); } public static void RunP(object o) { Thread.Sleep(50); Person p = o as Person; Console.WriteLine(p.Name + p.Age); } }
输出:
七、TimerCallback委托
TimerCallback委托专门用于定时器的操作,这个委托允许我们定义一个定时任务,在指定的间隔之后重复调用。实际的类型与ParameterizedThreadStart委托是一样的。
Timer类的构造函数定义如下:
public Timmer(TimerCallback callback,Object state,long dueTime,long period)
- Callback表示一个时间到达时执行的委托,这个委托代表的方法必须符合委托TimerCallback的定义。
- State表示当调用这个定时器委托时传递的参数。
- dutTime表示从创建定时器到第一次调用时延迟的时间,以毫秒为单位。
- Period表示定时器开始之后,每次调用之间的时间间隔,以毫秒为单位。
示例,使用TimerCallback每隔一秒钟输出一次时间:
class Program { static void Main(string[] args) { System.Threading.Timer clock = new System.Threading.Timer(ConsoleApplication1.Program.ShowTime, null, 0, 1000); Console.ReadKey(); } public static void ShowTime(object userData) { Console.WriteLine(DateTime.Now.ToString()); } }
输出如下: