瑞星卡卡安全论坛技术交流区系统软件 【推荐】《C#高级编程(第3版)》

1   1  /  1  页   跳转

【推荐】《C#高级编程(第3版)》

【推荐】《C#高级编程(第3版)》

C#中的线程(一)

本文摘至清华大学出版社出版的Wrox红皮书《C#高级编程(第3版)》,转载必须标明出处


本文介绍C#和.NET基类为开发多线程应用程序所提供的支持。我们将简要介绍Thread类以及各种线程支持,再用两个示例来说明线程的规则。然后论述线程同步时会出现的问题。由于这个主题非常复杂,所以本节的重点是理解一些基本规则,而不是开发真实的应用程序。本文的主要内容如下:
●    如何开始一个线程
●    提供线程的优先级
●    通过同步控制对对象的访问
学习完本文,就可以在自己的代码中处理线程了。下面首先了解线程的基础知识。
15.1  线程
线程是程序中的执行序列。使用C#编写任何程序时,都有一个入口:Main()方法。程序从Main()方法的第一条语句开始执行,直到这个方法返回为止。
这个程序结构非常适合于有一个可识别的任务序列的程序,但程序常常需要同时完成多个任务。例如启动Internet Explorer,并为某些页面需要越来越多的时间加载而烦恼。最终(可能就在2秒钟后),用户会单击Back按钮,或者键入其他的URL查看其他的页面。为此,Internet Explorer必须至少做3件事:
●    从Internet返回时,把页面的数据和附带的文件收集到垃圾箱中
●    显示页面
●    查看用户希望IE执行其他任务的输入内容(例如查看按钮的单击)
这种情况也会发生在下述场合下:程序在执行某个任务,同时显示一个对话框,用户可以在这个对话框中随时取消这个任务。
下面更详细地讨论Internet Explorer示例。为了简化问题,我们忽略存储来自Internet的数据的任务,并假定Internet Explorer只有两个任务:
●    显示页面
●    查看用户的输入
假定这个Web页面需要较长的时间才能显示,其中有一些处理器密集型的JavaScript,或者包含需要持续更新的选取框元素。处理这种情况的一个方式是编写一个方法,它在显示页面的过程中,还执行其他工作。过一会儿,假定是20分之一秒,该方法将检查是否有用户输入。如果有,就处理该用户的输入(这会取消显示任务)。否则,该方法就在下一个20分之一秒内显示页面。
这个方法是有效的,但要执行一个非常复杂的方法。更糟糕的是,它将完全忽略Windows基于事件的结构。如果在系统中有任何用户输入,就会通知应用程序产生了一个事件。下面修改这个方法,让Windows使用事件:
●    编写一个响应用户输入的事件处理程序。该响应应包括设置一些标志,表示显示页面的过程停止。
●    编写一个方法处理显示任务,这个方法用于系统在没有做其他事情时显示页面。
这种解决方案比较好,因为它利用了Windows事件结构。下面看看这个方法要完成的工作:从一开始就必须仔细考虑时间。在这个方法运行时,计算机不能响应任何用户输入。即这个方法必须知道自己被调用的时间,在工作过程中一直监视着时间,一旦过去指定的时间(留给用户响应的时间略小于10分之一秒),就必须返回。而且,在这个方法返回前,还需要存储当前的状态,这样,在下一次调用时,才知道应从哪里开始。这样的方法是肯定可以编写出来的,过去使用Windows 3.1时,就必须这样处理。NT 3.1和后来的Windows 95引入了多线程处理,以更方便的方式解决该问题。
15.2  多线程应用程序
上面的示例说明了应用程序需要处理多个任务的情形,所以最明显的解决方案是给应用程序提供多个执行线程。线程表示计算机执行的指令序列。应用程序不应只有一个这样的序列,实际上,应用程序可以有任意多个线程。每次创建一个新执行线程时,都需要指定从哪个方法开始执行。应用程序中的第一个线程总是Main()方法,因为第一个线程是由.NET运行库开始执行的,Main()方法是.NET运行库选择的第一个方法。后续的线程由应用程序在内部启动,即应用程序可以选择启动哪个线程。
多线程的工作方式
我们仅讨论了同时执行的线程。实际上,一个处理器在某一刻只能处理一个任务。如果有一个多处理器系统,理论上它可以同时执行多个指令——一个处理器执行一个指令,但大多数人使用的是单处理器计算机,这种情况是不可能同时发生的。而实际上,Windows操作系统表面上可以同时处理多个任务,这个过程称为抢先式多任务处理(pre-emptive multitasking)。
所谓抢先式多任务处理,是指Windows在某个进程中选择一个线程,该线程运行一小段时间。Microsoft没有说明这段时间有多长,因为为了获得最好的性能,Windows有一个内部操作系统参数来控制这个时间值。但在运行Windows应用程序时,用户不需要知道它。从我们的角度来看,这个时间非常短,肯定不会超过几毫秒。这段很短的时间称为线程的时间片(time slice)。过了这个时间片后,Windows就收回控制权,选择下一个被分配了时间片的线程。这些时间片非常短,我们可以认为许多事件是同时发生的。
即使应用程序只有一个线程,抢先式多任务处理的进程也在进行,因为系统上运行了许多其他过程,每个过程都需要一定的时间片来完成其线程。当屏幕上有许多窗口时,每个窗口都代表不同的过程,可以单击它们中的任一个,让它显示响应。这种响应不是即时的,在相关进程中下一个负责处理该窗口的用户输入的线程得到一个时间片时,这种响应才会发生。如果系统非常忙,就需等待,但这种等待的时间非常短暂,用户不会察觉到。
15.3  线程的处理
线程是使用Thread类来处理的,该类在System.Threading命名空间中。一个Thread实例表示一个线程,即执行序列。通过简单实例化一个Thread对象,就可以创建另一个线程。
启动线程
要使下面的代码段更具体,假定编写一个图形图像编辑器,用户请求修改图像的颜色深度。因为对于一个大的图像,这个操作需要一定的时间才能完成。此时要创建一个单独的线程来处理这个过程,所以在颜色的深度变化时,用户可以不中断用户界面。首先实例化一个Thread    对象:
// entryPoint has been declared previously as a delegate
// of type ThreadStart
Thread depthChangeThread = new Thread(entryPoint);
这段代码指定变量名depthChangeThread。
注意:
在一个应用程序中创建另一个线程,执行一些任务,通常称为工作线程(worker thread)。
上面的代码说明,Thread构造函数需要一个参数,用于指定线程的入口——即线程开始执行的方法。因为我们传送的是方法的详细信息,所以需要使用委托。实际上,委托已经在System. Threading类中定义好了。它称为ThreadStart,其签名如下所示:
public delegate void ThreadStart();
传送给构造函数的参数必须是这种类型的委托。
但完成后,新线程实际上并没有执行任务,它只是在等待执行。我们调用Thread.Start()方法来启动线程。
假定有一个方法ChangeColorDepth():
void ChangeColorDepth()
{
  // processing to change color depth of image
}
执行下述代码:
Thread depthChangeThread = new Thread();
depthChangeThread.Name = "Depth Change Thread";
ThreadStart entryPoint = new ThreadStart(ChangeColorDepth);
depthChangeThread.Start();
完成后,两个线程就会同时运行。

图  15-1
在这段代码中,还使用Thread.Name属性给线程赋予一个友好的名称,如图15-1所示,这不是必要的,但非常有效。
注意,线程的入口(在本例中是ChangeColorDepth())不带任何参数,所以必须用其他方式给方法传递它需要的信息。最显而易见的方式就是使用该方法所属的类的成员字段。而且,该方法没有返回值(如果有返回值,则应返回到什么地方?只要这个方法返回,运行它的线程就会终止,所以不能接收任何返回值。我们几乎不能把它返回给调用该线程的线程,因为该线程大概在忙着做其他事)。
启动了一个线程后,还可以挂起、恢复或中止它。挂起一个线程就是让它进入睡眠状态,此时,线程仅是停止运行某段时间,不占用任何处理器时间,以后还可以恢复,从被挂起的那个状态重新运行。如果线程被中止,就是停止运行。Windows会永久地删除该线程的所有数据,所以该线程不能重新启动。
继续上面的图像编辑器例子,假定由于某些原因,用户界面线程显示一个对话框,允许用户选择临时挂起会话进程(用户通常不会这么做,但这仅是一个示例,在更真实的示例中,用户可能是暂停声音文件或视频文件的播放)。在主线程中编写如下响应:
depthChangeThread.Suspend();
如果用户以后要求恢复该线程,可以使用下面的方法:
depthChangeThread.Resume();
最后,如果用户(更真实)决定不进行这样的会话,单击取消按钮,可以使用下面的方法:
depthChangeThread.Abort();
注意Suspend()和 Abort()方法不必立即起作用。对于Suspend()方法,.NET允许要挂起的线程再执行几个指令,目的是为了到达.NET认为线程可以安全挂起的状态。这么做,从技术上讲,是为了确保垃圾收集器执行正确的操作,具体内容见MSDN文档说明。在中止线程时,Abort()方法会在受影响的线程中产生一个ThreadAbortException,ThreadAbortException是一个特殊的异常类,以前我们没有遇到过。以这种方式中止线程,如果线程当前执行try块中的代码,则在线程真正中止前,将执行相应的finally块。这就可以保证清理资源,并有机会确保线程正在处理的数据(例如,在线程中止后仍保留的类实例的字段)处于有效的状态。

注意:
在开发.NET以前,不推荐使用这种方式中止线程,但极端情况除外,因为受影响的线程会立即中止,它正在处理的数据将处于无效状态,线程所使用的资源仍被占用。.NET使用的异常机制可以使线程的中止更加安全。
这种异常机制可以使线程的中止比较安全,但中止线程要用一定的时间,因为从理论上讲,finally块中的代码执行多长时间是没有限制的。因此,在中止线程后需要等待一段时间,线程被真正中止后,才能继续执行其他操作。如果后续的处理依赖于另一个已经中止的线程,可以调用Join()方法,等待线程中止:
depthChangeThread.Abort();
depthChangeThread.Join();
Join() 的其他重载方法可以指定等待的时间期限。如果过了等待的时间期限,程序会继续执行。如果没有指定时间期限,线程就要等待需要等待的时间。
上面的代码段还显示了在一个线程上执行操作的另一个线程(至少在Join()中,是等待另一个线程)。但是,如果主线程要在它自己的线程上执行某些操作,该怎么办?此时需要一个线程对象的引用来表示它自己的线程。使用Thread类的静态属性CurrentThread,就可以获得这样一个引用:
Thread myOwnThread = Thread.CurrentThread;
线程实际上是一个不太好处理的类,因为即使在没有实例化其他线程以前,也总是会有一个线程:目前正在执行的线程。因此处理这个类与其他类有两个区别:
●    可以实例化一个线程对象,它表示一个正在运行的线程,其实例成员应用于正在运行的线程上。
●    可以调用任意个静态方法。这些方法一般会应用到实际调用它们的线程上。
可以调用的一个静态方法是Sleep(),它使正在运行的线程进入睡眠状态,过一段时间之后该线程会继续运行。
最后编辑2005-11-10 13:23:02
分享到:
gototop
 

C#中的线程(二)

15.4  ThreadPlayaround示例
下面用一个简单的示例ThreadPlayaround来说明如何使用线程。这个示例的目的是介绍如何处理线程,而不是说明实际编程问题。
示例ThreadPlayaround的核心是方法DisplayNumbers(),它累加一个数字,并显示每次累加的结果。DisplayNumbers()还会显示它运行的线程名称和文化背景:
      static void DisplayNumbers()
      {
        Thread thisThread = Thread.CurrentThread;
        string name = thisThread.Name;
        Console.WriteLine("Starting thread: " + name);
        Console.WriteLine(name + ": Current Culture = " +
                          thisThread.CurrentCulture);
        for (int i=1 ; i<= 8*interval ; i++)
        {
            if (i%interval == 0)
              Console.WriteLine(name + ": count has reached " + i);
        }
      }
累加的数字取决于interval字段,它的值是用户输入的。如果用户输入100,就累加到800,显示数字100, 200, 300, 400, 500, 600, 700和800,如果用户输入1000,就累加到8000,显示数字1000, 2000, 3000, 4000, 5000, 6000, 7000和 8000,依次类推。这似乎是一个没有意义的方法,但它的目的是让处理器停止一段时间,以便查看处理器是如何处理这个任务的。
ThreadPlayaround示例启动了第二个工作线程,运行DisplayNumbers(),但启动这个工作线程后,主线程就开始执行同一个方法,此时我们应看到有两个累加过程同时发生。
ThreadPlayaround示例的Main()方法及其包含的类如下所示:
  class EntryPoint
  {
      static int interval;

      static void Main()
      {
        Console.Write("Interval to display results at?> ");
        interval = int.Parse(Console.ReadLine());

        Thread thisThread = Thread.CurrentThread;
        thisThread.Name = "Main Thread";

        ThreadStart workerStart = new ThreadStart(StartMethod);
        Thread workerThread = new Thread(workerStart);
        workerThread.Name = "Worker";
        workerThread.Start();

        DisplayNumbers();
        Console.WriteLine("Main Thread Finished");

        Console.ReadLine();
      }
  }
该代码段从类的声明开始,interval是这个类的一个静态字段。在Main()方法中,首先要求用户输入interval的值。然后获取表示主线程的线程对象引用,这样,就可以给线程指定名称,并可以在结果中看到具体的执行情况。
接着,创建工作线程,设置它的名称,启动它,给它传送一个委托,指定它必须从方法WorkerStart开始执行,最后调用DisplayNumbers()方法,开始累加。工作线程的入口是:
      static void StartMethod()
      {
        DisplayNumbers();
        Console.WriteLine("Worker Thread Finished");
      }
注意所有这些方法都是类EntryPoint的静态方法。两个累加过程是完全独立的,因为DisplayNumbers()方法中用于累加数字的变量i 是一个局部变量。局部变量只能在定义它们的方法中使用,也只有在执行该方法的线程中是可见的。如果另一个线程开始执行这个方法,该线程就会获得该局部变量的副本。运行这段代码,给interval选择一个相对小的值100,得到如下结果:
ThreadPlayaround
Interval to display results at?> 100
Starting thread: Main Thread
Main Thread: Current Culture = en-US
Main Thread: count has reached 100
Main Thread: count has reached 200
Main Thread: count has reached 300
Main Thread: count has reached 400
Main Thread: count has reached 500
Main Thread: count has reached 600
Main Thread: count has reached 700
Main Thread: count has reached 800
Main Thread Finished
Starting thread: Worker
Worker: Current Culture = en-US
Worker: count has reached 100
Worker: count has reached 200
Worker: count has reached 300
Worker: count has reached 400
Worker: count has reached 500
Worker: count has reached 600
Worker: count has reached 700
Worker: count has reached 800
Worker Thread Finished
对于并行的线程而言,两个线程的执行都非常成功。主线程启动后,累加到800之后完成执行,然后启动工作线程,执行累加过程。
此处的问题是启动线程是一个主进程,在实例化一个新线程后,主线程会遇到下面的代码:
        workerThread.Start();
它调用Thread.Start(),告诉Windows新线程已经准备启动,然后即时返回。在累加到800时,Windows就启动新线程,这意味着给该线程分配各种资源,执行各种安全检查。到新线程启动时,主线程已经完成了任务。
解决这个问题的方式是选择一个比较大的 interval,这样,两个线程在DisplayNumbers()方法中花费的时间会比较长,这次给interval输入1000000,得到如下所示的结果:
ThreadPlayaround
Interval to display results at?> 1000000
Starting thread: Main Thread
Main Thread: Current Culture = en-US
Main Thread: count has reached 1000000
Starting thread: Worker
Worker: Current Culture = en-US
Main Thread: count has reached 2000000
Worker: count has reached 1000000
Main Thread: count has reached 3000000
Worker: count has reached 2000000
Main Thread: count has reached 4000000
Worker: count has reached 3000000
Main Thread: count has reached 5000000
Main Thread: count has reached 6000000
Worker: count has reached 4000000
Main Thread: count has reached 7000000
Worker: count has reached 5000000
Main Thread: count has reached 8000000
Main Thread Finished
Worker: count has reached 6000000
Worker: count has reached 7000000
Worker: count has reached 8000000
Worker Thread Finished
现在就可以看出,这两个线程实际上是并行工作的。主线程启动,累加到100万,当主线程计算下一个100万时,工作线程启动,从那时起,两个线程以相同的速度累加,直到完成任务为止。
除非运行一个多处理器计算机,否则在CPU密集的任务中使用两个线程不能节省多少时间,理解这一点是很重要的。在单处理器计算机上,让两个线程都累加到800万所花的时间与让一个线程累加到1600万是相同的,甚至使用两个线程所用的时间会略长,因为要处理另一个线程,操作系统必须用一定的时间切换线程,但这种区别可以忽略不计。使用多个线程的优点有两个。首先,可以作出响应,因为一个线程在处理用户输入时,另一个线程在后台完成其他工作;第二,如果一个或多个线程所处理的工作不占用CPU时间(例如,等待从Internet中获取数据),就可以节省时间,因为其他线程可以在未激活的线程处于等待状态时执行它们的任务。
gototop
 
1   1  /  1  页   跳转
页面顶部
Powered by Discuz!NT