步步为营 C# 技术漫谈 六、线程控制(Thread)
概论
多线程在构建大型系统的时候是需要重点关注的一个重要方面,特别是在效率(系统跑得多快?)和性能(系统工作正常?)之间做一个权衡的时候。恰当的使用多线程可以极大的提高系统性能。
什么是线程?
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
在Win32环境中常用的一些模型。
·单线程模型
在这种线程模型中,一个进程中只能有一个线程,剩下的进程必须等待当前的线程执行完。这种模型的缺点在于系统完成一个很小的任务都必须占用很长的时间。
·块线程模型(单线程多块模型STA)
这种模型里,一个程序里可能会包含多个执行的线程。在这里,每个线程被分为进程里一个单独的块。每个进程可以含有多个块,可以共享多个块中的数据。程序规定了每个块中线程的执行时间。所有的请求通过Windows消息队列进行串行化,这样保证了每个时刻只能访问一个块,因而只有一个单独的进程可以在某一个时刻得到执行。这种模型比单线程模型的好处在于,可以响应同一时刻的多个用户请求的任务而不只是单个用户请求。但它的性能还不是很好,因为它使用了串行化的线程模型,任务是一个接一个得到执行的。(此模型相当于在多线程中在一个线程执行的过程中,另外的线程都处于挂起的状态,直到此线程执行结束,后再开始另一个线程)
·多线程块模型(自由线程块模型)
多线程块模型(MTA)在每个进程里只有一个块而不是多个块。这单个块控制着多个线程而不是单个线程。这里不需要消息队列,因为所有的线程都是相同的块的一个部分,并且可以共享。这样的程序比单线程模型和STA的执行速度都要块,因为降低了系统的负载,因而可以优化来减少系统idle的时间。这些应用程序一般比较复杂,因为程序员必须提供线程同步以保证线程不会并发的请求相同的资源,因而导致竞争情况的发生。这里有必要提供一个锁机制。但是这样也许会导致系统死锁的发生。
多线程在.NET里如何工作?
在本质上和结构来说,.NET是一个多线程的环境。有两种主要的多线程方法是.NET所提倡的:使用ThreadStart来开始你自己的进程,直接的(使用ThreadPool.QueueUserWorkItem)或者间接的(比如Stream.BeginRead,或者调用BeginInvoke)使用ThreadPool类。一般来说,你可以"手动"为长时间运行的任务创建一个新的线程,另外对于短时间运行的任务尤其是经常需要开始的那些,进程池是一个非常好的选择。进程池可以同时运行多个任务,还可以使用框架类。对于资源紧缺需要进行同步的情况来说,它可以限制某一时刻只允许一个线程访问资源。这种情况可以视为给线程实现了锁机制。线程的基类是System.Threading。所有线程通过CLI来进行管理。
如何使用线程
using System.Threading;
所有与多线程机制应用相关的类都是放在System.Threading命名空间中的.
Thread类用于创建线程.
ThreadPool类用于管理线程池等等.
如果你想在你的应用程序中使用多线程,就必须包含这个Thread类.
Thread类有几个至关重要的方法:
Start():启动线程.
Sleep(int):静态方法,暂停当前线程指定的毫秒数.
Abort():通常使用该方法来终止一个线程.
Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复.
Resume():恢复被Suspend()方法挂起的线程的执行.
简单线程示例
class Program { static void Main(string[] args) { ThreadStart threadStart = new ThreadStart(SayHello); Thread thread = new Thread(threadStart); thread.Start(); Thread.Sleep(5000); } public static void SayHello() { Console.Write("Spring Yang say hello to you.\n hello,every one."); } }
运行结果:
传递单个参数
class Program { static void Main(string[] args) { string message = "Hello everyone."; ParameterizedThreadStart threadStart = new ParameterizedThreadStart(SayHello); Thread thread = new Thread(threadStart); thread.Start(message); Thread.Sleep(5000); } public static void SayHello(object message) { Console.Write("Spring Yang say hello to you.\n{0}.",message); } }
结果:
传递多个参数:
class Program { static void Main(string[] args) { ThreadTest threadTest = new ThreadTest("spring yang", "How are you?"); ThreadStart threadStart = new ThreadStart(threadTest.SayHello); Thread thread = new Thread(threadStart); thread.Start(); Thread.Sleep(5000); } } public class ThreadTest { private string _userName; private string _message; public string UserName { get { return _userName; } set { _userName = value; } } public string Message { get { return _message; } set { _message = value; } } public ThreadTest(string userName, string message) { this._userName = userName; this._message = message; } public void SayHello() { Console.Write("{0} say hello to you.\n{1}.", _userName, _message); } }
运行结果:
线程争夺资源示例:
class Program { static void Main(string[] args) { ThreadTest threadTest = new ThreadTest("spring yang", "How are you"); ThreadStart threadone = new ThreadStart(threadTest.SayHello); Thread firstThread = new Thread(threadone); firstThread.Start(); ThreadStart threadtwo = new ThreadStart(threadTest.DoWork); Thread secondThread = new Thread(threadtwo); secondThread.Start(); Thread.Sleep(50000); } } public class ThreadTest { private string _userName; private string _message; public string UserName { get { return _userName; } set { _userName = value; } } public string Message { get { return _message; } set { _message = value; } } public ThreadTest(string userName, string message) { this._userName = userName; this._message = message; } public void SayHello() { for(int i=0;i<1000;i++) { Console.Write("{0} times {1} say {2}.\n",i, _userName, _message); } } public void DoWork() { for (int i = 0; i < 1000; i++) { Console.Write("Do the {0} job.\n", i); } } }
结果:
必须要了解,执行.NET应用的线程实际上仍然是Windows线程。但是,当某个线程被CLR所知时,我们将它称为受托管的线程。具体来说,由受托管的代码创建出来的线程就是受托管的线程。如果一个线程由非托管的代码所创建,那么它就是非托管的线程。不过,一旦该线程执行了受托管的代码它就变成了受托管的线程。
一个受托管的线程和非托管的线程的区别在于,CLR将创建一个System.Threading.Thread类的实例来代表并操作前者。在内部实现中,CLR将一个包含了所有受托管线程的列表保存在一个叫做ThreadStore地方。
CLR确保每一个受托管的线程在任意时刻都在一个AppDomain中执行,但是这并不代表一个线程将永远处在一个AppDomain中,它可以随着时间的推移转到其他的AppDomain中。
从安全的角度来看,一个受托管的线程的主用户与底层的非托管线程中的Windows主用户是无关的。
前台线程与后台线程
启动了多个线程的程序在关闭的时候却出现了问题,如果程序退出的时候不关闭线程,那么线程就会一直的存在,但是大多启动的线程都是局部变量,不能一一的关闭,如果调用Thread.CurrentThread.Abort()方法关闭主线程的话,就会出现ThreadAbortException 异常,因此这样不行。
后来找到了这个办法: Thread.IsBackground 设置线程为后台线程。
msdn对前台线程和后台线程的解释:托管线程或者是后台线程,或者是前台线程。后台线程不会使托管执行环境处于活动状态,除此之外,后台线程与前台线程是一样的。一旦所有前台线程在托管进程(其中 .exe 文件是托管程序集)中被停止,系统将停止所有后台线程并关闭。通过设置 Thread.IsBackground 属性,可以将一个线程指定为后台线程或前台线程。例如,通过将 Thread.IsBackground 设置为 true,就可以将线程指定为后台线程。同样,通过将 IsBackground 设置为 false,就可以将线程指定为前台线程。从非托管代码进入托管执行环境的所有线程都被标记为后台线程。通过创建并启动新的 Thread 对象而生成的所有线程都是前台线程。如果要创建希望用来侦听某些活动(如套接字连接)的前台线程,则应将 Thread.IsBackground 设置为 true,以便进程可以终止。
所以解决办法就是在主线程初始化的时候,设置:Thread.CurrentThread.IsBackground = true;
这样,主线程就是后台线程,在关闭主程序的时候就会关闭主线程,从而关闭所有线程。但是这样的话,就会强制关闭所有正在执行的线程,所以在关闭的时候要对线程工作的结果保存。
Invoke,BeginInvoke干什么用的,内部是怎么实现的?
这两个方法主要是让给出的方法在控件创建的线程上执行
Invoke使用了Win32API的SendMessage,
UnsafeNativeMethods.SendMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
BeginInvoke使用了Win32API的PostMessage
UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
这两个方法向UI线程的消息队列中放入一个消息,当UI线程处理这个消息时,就会在自己的上下文中执行传入的方法,换句话说凡是使用BeginInvoke和Invoke调用的线程都是在UI主线程中执行的,所以如果这些方法里涉及一些静态变量,不用考虑加锁的问题.
BeginXXX和EndXXX的用法
这是.net的一个异步方法名称规范
.Net在设计的时候为异步编程设计了一个异步编程模型(APM),这个模型不仅是使用.NET的开发人员使用,.Net内部也频繁用到,比如所有的Stream就有BeginRead,EndRead,Socket,WebRequet,SqlCommand都运用到了这个模式,一般来讲,调用BegionXXX的时候,一般会启动一个异步过程去执行一个操作,EndEnvoke可以接收这个异步操作的返回,当然如果异步操作在EndEnvoke调用的时候还没有执行完成,EndInvoke会一直等待异步操作完成或者超时
.Net的异步编程模型(APM)一般包含BeginXXX,EndXXX,IAsyncResult这三个元素,BeginXXX方法都要返回一个IAsyncResult,而EndXXX都需要接收一个IAsyncResult作为参数,他们的函数签名模式如下
IAsyncResult BeginXXX(...);
<返回类型> EndXXX(IAsyncResult ar);
BeginXXX和EndXXX中的XXX,一般都对应一个同步的方法,比如FileStream的Read方法是一个同步方法,相应的BeginRead(),EndRead()就是他的异步版本,HttpRequest有GetResponse来同步接收一个响应,也提供了BeginGetResponse和EndGetResponse这个异步版本,而IAsynResult是二者联系的纽带,只有把BeginXXX所返回的IAsyncResult传给对应的EndXXX,EndXXX才知道需要去接收哪个BeginXXX发起的异步操作的返回值。
这个模式在实际使用时稍显繁琐,虽然原则上我们可以随时调用EndInvoke来获得返回值,并且可以同步多个线程,但是大多数情况下当我们不需要同步很多线程的时候使用回调是更好的选择,在这种情况下三个元素中的IAsynResult就显得多余,我们一不需要用其中的线程完结标志来判断线程是否成功完成(回调的时候线程应该已经完成了),二不需要他来传递数据,因为数据可以写在任何变量里,并且回调时应该已经填充,所以可以看到微软在新的.Net Framework中已经加强了对回调事件的支持,这总模型下,典型的回调程序应该这样写
a.DoWork+=new SomeEventHandler(Caculate);
a.CallBack+=new SomeEventHandler(callback);
a.Run();
(注:我上面讲的是普遍的用法,然而BeginXXX,EndXXX仅仅是一种模式,而对这个模式的实现完全取决于使用他的开发人员,具体实现的时候你可以使用另外一个线程来实现异步,也可能使用硬件的支持来实现异步,甚至可能根本和异步没有关系(尽管几乎没有人会这样做)-----比如直接在Beginxxx里直接输出一个"Helloworld",如果是这种极端的情况,那么上面说的一切都是废话,所以上面的探讨并不涉及内部实现,只是告诉大家微软的模式,和框架中对这个模式的经典实现)
有一句话总结的很好:多线程是实现异步的一种手段和工具
我们通常把多线程和异步等同起来,实际是一种误解,在实际实现的时候,异步有许多种实现方法,我们可以用进程来做异步,或者使用纤程,或者硬件的一些特性,比如在实现异步IO的时候,可以有下面两个方案:
1)可以通过初始化一个子线程,然后在子线程里进行IO,而让主线程顺利往下执行,当子线程执行完毕就回调
2)也可以根本不使用新线程,而使用硬件的支持(现在许多硬件都有自己的处理器),来实现完全的异步,这是我们只需要将IO请求告知硬件驱动程序,然后迅速返回,然后等着硬件IO就绪通知我们就可以了
实际上DotNet Framework里面就有这样的例子,当我们使用文件流的时候,如果制定文件流属性为同步,则使用BeginRead进行读取时,就是用一个子线程来调用同步的Read方法,而如果指定其为异步,则同样操作时就使用了需要硬件和操作系统支持的所谓IOCP的机制.
异步调用实例:
class Program { static void Main(string[] args) { ThreadTest threadTest = new ThreadTest("spring yang", "How are you"); ThreadStart threadone = new ThreadStart(threadTest.SayHello); Thread firstThread = new Thread(threadone); firstThread.IsBackground = true; firstThread.Name = "First Thread"; firstThread.Start(); Console.WriteLine(" First Thread Start."); ThreadStart threadtwo = new ThreadStart(threadTest.SayHello); Thread secondThread = new Thread(threadtwo); secondThread.IsBackground = true; secondThread.Name = "Second Thread"; secondThread.Start(); Console.WriteLine("Second Thread Start."); firstThread.Join(); secondThread.Join(); Thread.Sleep(50000); } } public class ThreadTest { private string _userName; private string _message; public string UserName { get { return _userName; } set { _userName = value; } } public string Message { get { return _message; } set { _message = value; } } public ThreadTest(string userName, string message) { this._userName = userName; this._message = message; } public void SayHello() { for(int i=0;i<100;i++) { Console.Write("{0} times {1} say {2},Thread Name:{3}.\n",i, _userName, _message,Thread.CurrentThread.Name); Thread.Sleep(1); } } }
运行结果:
多线程的好处:在于可以提高CPU的利用率;
不利方面:
-
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
-
多线程需要协调和管理,所以需要CPU时间跟踪线程;
-
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
-
线程太多会导致控制太复杂,最终可能造成很多Bug;
杀死线程
Thread类的Abort方法用于永久地杀死一个线程。但是请注意,在调用Abort方法前一定要判断线程是否还激活:
ThreadTest threadTest = new ThreadTest("spring yang", "How are you"); ThreadStart threadone = new ThreadStart(threadTest.SayHello); Thread firstThread = new Thread(threadone); firstThread.Start(); if (firstThread.IsAlive) { firstThread.Abort(); }
设置线程的优先权
我们可以使用Thread类的ThreadPriority属性设置线程的优先权。线程优先权的取值范围是Normal、AboveNormal、BelowNormal、Highest或者Lowest。请看下面的设置代码:
ThreadTest threadTest = new ThreadTest("spring yang", "How are you"); ThreadStart threadone = new ThreadStart(threadTest.SayHello); Thread firstThread = new Thread(threadone); firstThread.Priority = ThreadPriority.Highest; firstThread.Start();
延迟线程
Thread类的Suspend方法可以延迟一个线程(挂起线程)。线程被延迟到调用Resume方法为止。
ThreadTest threadTest = new ThreadTest("spring yang", "How are you"); ThreadStart threadone = new ThreadStart(threadTest.SayHello); Thread firstThread = new Thread(threadone); firstThread.Priority = ThreadPriority.Highest; firstThread.Start(); if (firstThread.ThreadState == ThreadState.Running) { firstThread.Suspend(); }
恢复被延迟的线程
调用Resume方法可以恢复一个被延迟的线程。如果线程没有被延迟,Resume方法就是无效的。
ThreadTest threadTest = new ThreadTest("spring yang", "How are you"); ThreadStart threadone = new ThreadStart(threadTest.SayHello); Thread firstThread = new Thread(threadone); firstThread.Priority = ThreadPriority.Highest; firstThread.Start(); if (firstThread.ThreadState == ThreadState.Suspended) { firstThread.Resume(); }
步步为营 C# 技术漫谈系列
步步为营 C# 技术漫谈 一、反射机制
步步为营 C# 技术漫谈 二、ASP.NET 页生命周期
步步为营 C# 技术漫谈 三、公共语言运行库(CLR)
步步为营 C# 技术漫谈 四、垃圾回收机制(GC)
步步为营 C# 技术漫谈 五、事件与委托机制
作者:spring yang
出处:http://www.cnblogs.com/springyangwc/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。