C#编程总结(二)多线程基础
无论您是为具有单个处理器的计算机还是为具有多个处理器的计算机进行开发,您都希望应用程序为用户提供最好的响应性能,即使应用程序当前正在完成其他工作。要使应用程序能够快速响应用户操作,同时在用户事件之间或者甚至在用户事件期间利用处理器,最强大的方式之一是使用多线程技术。
多线程:线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程。如果某个线程进行一次长延迟操作, 处理器就切换到另一个线程执行。这样,多个线程的并行(并发)执行隐藏了长延迟,提高了处理器资源利用率,从而提高了整体性能。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率
一、进程与线程
进程,是操作系统进行资源调度和分配的基本单位。是由进程控制块、程序段、数据段三部分组成。一个进程可以包含若干线程(Thread),线程可以帮助应用程序同时做几件事(比 如一个线程向磁盘写入文件,另一个则接收用户的按键操作并及时做出反应,互相不干扰),在程序被运行后中,系统首先要做的就是为该程序进程建立一个默认线程,然后程序可 以根据需要自行添加或删除相关的线程。它是可并发执行的程序。在一个数据集合上的运行过程,是系统进行资源分配和调度的一个独立单位,也是称活动、路径或任务,它有两方面性质:活动性、并发性。进程可以划分为运行、阻塞、就绪三种状态,并随一定条件而相互转化:就绪--运行,运行--阻塞,阻塞--就绪。
线程(thread),线程是CPU调度和执行的最小单位。有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。
主线程,进程创建时,默认创建一个线程,这个线程就是主线程。主线程是产生其他子线程的线程,同时,主线程必须是最后一个结束执行的线程,它完成各种关闭其他子线程的操作。尽管主线程是程序开始时自动创建的,它也可以通过Thead类对象来控制,通过调用CurrentThread方法获得当前线程的引用
多线程的优势:进程有独立的地址空间,同一进程内的线程共享进程的地址空间。启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
二、多线程优点
1、提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。。
多线程尽管优势明显,但是线程并发冲突、同步以及管理跟踪,可能给系统带来很多不确定性,这些必须引起足够重视。
废话不多说开始我们的多线程之旅。
三、多线程的应用场合:
简单总结了一下,一般有两种情况:
1)多个线程,完成同类任务,提高并发性能
2)一个任务有多个独立的步骤,多个线程并发执行各子任务,提高任务处理效率
四、案例--搬运工
在我们现实生活中,经常看到这样的场景。有一堆货物,有几个搬运工负责将货物搬运到指定地点。但是搬运工能力不同,有人一次能搬多箱,有人走路比较慢,搬运一趟的时间间隔比较长。搬运工,各自搬运,无先后,互不干扰。我们如何在程序中实现这种场景呢?
案例分析:
这个就是最简单的多线程的实际案例。每个人相当于一个线程,并发执行。当货物搬运完毕后,每个线程自动停止。这里暂时不考虑死锁情况。
案例代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace MutiThreadSample.Transport { /// <summary> /// 搬运工 /// </summary> public class Mover { /// <summary> /// 总数 /// </summary> public static int GoodsTotal { get; set; } /// <summary> /// 间隔时间 /// </summary> public static int IntervalTime { get; set; } /// <summary> /// 名称 /// </summary> public string Name { get; set; } /// <summary> /// 单位时间搬运量 /// </summary> public int LaborAmount { get; set; } /// <summary> /// 搬运 /// </summary> public void Move() { while (GoodsTotal > 0) { GoodsTotal -= LaborAmount; Console.WriteLine("搬运者:{0} 于 {1} 搬运货物 {2}",this.Name,DateTime.Now.Millisecond,this.LaborAmount); Thread.Sleep(IntervalTime); Console.WriteLine("搬运者:{0} Continue",this.Name); } } /// <summary> /// 搬运 /// </summary> /// <param name="interval">时间间隔</param> public void Move(object interval) { int tempInterval = 0; if (!int.TryParse(interval.ToString(), out tempInterval)) { tempInterval = IntervalTime; } while (GoodsTotal > 0) { GoodsTotal -= LaborAmount; Console.WriteLine("搬运者:{0} 于 {1} 搬运货物 {2}", this.Name, DateTime.Now.Millisecond, this.LaborAmount); Thread.Sleep(tempInterval); } } } }
测试:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace MutiThreadSample.Transport { /// <summary> /// 测试搬运 /// </summary> public class TestMove { /// <summary> /// 搬运 /// </summary> public static void Move() { //测试搬运工 Mover.GoodsTotal = 200; Mover.IntervalTime = 10; Mover m1 = new Mover() { Name = "Tom", LaborAmount = 5 }; Mover m2 = new Mover() { Name = "Jim", LaborAmount = 10 }; Mover m3 = new Mover() { Name = "Lucy", LaborAmount = 20 }; List<Mover> movers = new List<Mover>(); movers.Add(m1); //movers.Add(m2); //movers.Add(m3); if (movers != null && movers.Count > 0) { foreach (Mover m in movers) { Thread thread = new Thread(new ThreadStart(m.Move)); thread.Start(); } } //Main Thread continue // validate Thread.Sleep() //int i =0; //int j = 0; //while (i < 10) //{ // while(j<10000000) // { // j++; // } // Console.WriteLine("CurrentThread:{0}", Thread.CurrentThread.Name); // i++; //} } /// <summary> /// 搬运 /// </summary> public static void MoveWithParamThread() { //测试搬运工 Mover.GoodsTotal = 1000; Mover.IntervalTime = 100; Mover m1 = new Mover() { Name = "Tom", LaborAmount = 5 }; Mover m2 = new Mover() { Name = "Jim", LaborAmount = 10 }; Mover m3 = new Mover() { Name = "Lucy", LaborAmount = 20 }; List<Mover> movers = new List<Mover>(); movers.Add(m1); movers.Add(m2); movers.Add(m3); if (movers != null && movers.Count > 0) { foreach (Mover m in movers) { Thread thread = new Thread(new ParameterizedThreadStart(m.Move)); thread.Start(10); } } } } }
通过案例我们也接触了Thread,下面我们将详细介绍Thread的功能。
五、Thread
创建并控制线程,设置其优先级并获取其状态。
常用方法:
Start()
导致操作系统将当前实例的状态更改为 ThreadState.Running。
一旦线程处于 ThreadState.Running 状态,操作系统就可以安排其执行。 线程从方法的第一行(由提供给线程构造函数的 ThreadStart 或 ParameterizedThreadStart 委托表示)开始执行。线程一旦终止,它就无法通过再次调用 Start 来重新启动。
Thread.Sleep()
调用 Thread.Sleep 方法会导致当前线程立即阻止,阻止时间的长度等于传递给 Thread.Sleep 的毫秒数,这样,就会将其时间片中剩余的部分让与另一个线程。 一个线程不能针对另一个线程调用 Thread.Sleep。
Interrupt()
中断处于 WaitSleepJoin 线程状态的线程。
Suspend和Resume(已过时)
挂起和继续
在 .NET Framework 2.0 版中,Thread.Suspend 和 Thread.Resume 方法已标记为过时,并将从未来版本中移除。
Abort()
方法用于永久地停止托管线程。一旦线程被中止,它将无法重新启动。
Join()
阻塞调用线程,直到某个线程终止时为止。
ThreadPriority(优先级)
指定 Thread 的调度优先级。
ThreadPriority 定义一组线程优先级的所有可能值。线程优先级指定一个线程相对于另一个线程的相对优先级。
每个线程都有一个分配的优先级。在运行库内创建的线程最初被分配 Normal 优先级,而在运行库外创建的线程在进入运行库时将保留其先前的优先级。可以通过访问线程的 Priority 属性来获取和设置其优先级。
根据线程的优先级调度线程的执行。用于确定线程执行顺序的调度算法随操作系统的不同而不同。操作系统也可以在用户界面的焦点在前台和后台之间移动时动态地调整线程的优先级。
一个线程的优先级不影响该线程的状态;该线程的状态在操作系统可以调度该线程之前必须为 Running。
六、创建线程方式
通过搬运工案例我们能够了解线程的工作原理,也明白了线程的创建方式。
其实在C#中创建线程有几种方式,这里给大家举几个常用例子,如下:
using System; using System.Threading; namespace MutiThreadSample { /// <summary> /// 创建线程的方式 /// </summary> class CreateThread { /// <summary> /// 不带参数的委托 /// </summary> public void CreateThreadWithThreadStart() { Thread thread = new Thread(new ThreadStart(ThreadCallBack)); thread.Start(); } /// <summary> /// 带参数的委托 /// </summary> public void CreateThreadWithParamThreadStart() { Thread thread = new Thread(new ParameterizedThreadStart(ThreadCallBackWithParam)); Object param = null; thread.Start(param); } /// <summary> /// 匿名函数 /// </summary> public void CreateThreadWithAnonymousFunction() { Thread thread = new Thread(delegate() { Console.WriteLine("进入子线程1"); for (int i = 1; i < 4; ++i) { Thread.Sleep(50); Console.WriteLine("\t+++++++子线程1+++++++++"); } Console.WriteLine("退出子线程1"); }); thread.Start(); } /// <summary> /// 直接赋值委托 /// </summary> public void CreateThreadWithCallBack() { Thread _hThread = new Thread(ThreadCallBack); _hThread.Start(); } /// <summary> /// 无参数的方法调用 /// </summary> public void ThreadCallBack() { // Do Something } /// <summary> /// 带参数的方法 /// </summary> /// <param name="obj"></param> public void ThreadCallBackWithParam(object obj) { // Do Something } } }
时钟线程
使用 TimerCallback 委托指定希望 Timer 执行的方法。 计时器委托在构造计时器时指定,并且不能更改。 此方法不在创建计时器的线程上执行,而是在系统提供的 ThreadPool 线程上执行。创建计时器时,可以指定在第一次执行方法之前等待的时间量(截止时间)以及此后的执行期间等待的时间量(时间周期)。 可以使用 Change 方法更改这些值或禁用计时器。
using System; using System.Threading; class TimerExample { static void Main() { // Create an event to signal the timeout count threshold in the // timer callback. AutoResetEvent autoEvent = new AutoResetEvent(false); StatusChecker statusChecker = new StatusChecker(10); // Create an inferred delegate that invokes methods for the timer. TimerCallback tcb = statusChecker.CheckStatus; // Create a timer that signals the delegate to invoke // CheckStatus after one second, and every 1/4 second // thereafter. Console.WriteLine("{0} Creating timer.\n", DateTime.Now.ToString("h:mm:ss.fff")); Timer stateTimer = new Timer(tcb, autoEvent, 1000, 250); // When autoEvent signals, change the period to every // 1/2 second. autoEvent.WaitOne(5000, false); stateTimer.Change(0, 500); Console.WriteLine("\nChanging period.\n"); // When autoEvent signals the second time, dispose of // the timer. autoEvent.WaitOne(5000, false); stateTimer.Dispose(); Console.WriteLine("\nDestroying timer."); } } class StatusChecker { private int invokeCount; private int maxCount; public StatusChecker(int count) { invokeCount = 0; maxCount = count; } // This method is called by the timer delegate. public void CheckStatus(Object stateInfo) { AutoResetEvent autoEvent = (AutoResetEvent)stateInfo; Console.WriteLine("{0} Checking status {1,2}.", DateTime.Now.ToString("h:mm:ss.fff"), (++invokeCount).ToString()); if(invokeCount == maxCount) { // Reset the counter and signal Main. invokeCount = 0; autoEvent.Set(); } } }
七、前台线程和后台线程
.Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
一个线程是前台线程还是后台线程可由它的IsBackground属性来决定。这个属性是可读又可写的。它的默认值为false,即意味着一个线程默认为前台线程。
我们可以将它的IsBackground属性设置为true,从而使之成为一个后台线程。下面的例子是一个控制台程序,程序一开始便启动了10个线程,每个线程运行5秒钟时间。由于线程的IsBackground属性默认为false,即它们都是前台线程,所以尽管程序的主线程很快就运行结束了,但程序要到所有已启动的线程都运行完毕才会结束。示例代码如下例子中的Test()所示
using System; using System.Threading; namespace MutiThreadSample.ThreadType { class ThreadTypeTest { /// <summary> /// 测试前台线程 /// </summary> public static void Test() { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new ThreadStart(ThreadFunc)); thread.Start(); } } /// <summary> /// 测试后台线程 /// </summary> public static void TestBackgroundThread() { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new ThreadStart(ThreadFunc)); thread.IsBackground = true; thread.Start(); } } public static void ThreadFunc() { Thread.Sleep(0); DateTime start = DateTime.Now; while ((DateTime.Now - start).Seconds < 20);//可以停顿的时间长一点,效果更加明显 } } }
接下来我们对上面的代码进行略微修改,将每个线程的IsBackground属性都设置为true,则每个线程都是后台线程了。那么只要程序的主线程结束了,整个程序也就结束了。示例代码如代码中的TestBackgroundThread()。
这个例子直接创建一个控制台程序即可检验。
前台和后台线程的使用原则
既然前台线程和后台线程有这种差别,那么我们怎么知道该如何设置一个线程的IsBackground属性呢?下面是一些基本的原则:对于一些在后台运行的线程,当程序结束时这些线程没有必要继续运行了,那么这些线程就应该设置为后台线程。比如一个程序启动了一个进行大量运算的线程,可是只要程序一旦结束,那个线程就失去了继续存在的意义,那么那个线程就该是作为后台线程的。而对于一些服务于用户界面的线程往往是要设置为前台线程的,因为即使程序的主线程结束了,其他的用户界面的线程很可能要继续存在来显示相关的信息,所以不能立即终止它们。这里我只是给出了一些原则,具体到实际的运用往往需要编程者的进一步仔细斟酌。
八、总结
这一章主要介绍多线程技术的基本知识。涉及多线程的具体应用,包括预防死锁、线程同步、线程池等,在今后的文章会涉及到。