C#语言 第五部分 线程(一)线程基础(1)

0 概述

Windows系列操作系统是建立在保护模式之上的32位/64位多任务操作系统,其特点是:时分抢先式多任务操作系统。我们来详细探讨一下其中的定义。

在操作系统中,进程线程是和我们运行程序紧密相关的两个概念,其中:

  • 进程是资源分配单元,用于执行一段程序前为其分配足够的资源;
  • 线程是程序执行单元,线程用于执行程序。

简单的叙述一下Windows操作系统是如何来启动一个应用程序(.exe文件)的。

  1. 操作系统分配一个进程,并向CPU下达一系列指令,包括创建该进程的虚拟内存映射表(关于虚拟内存,请参阅操作系统原理相关书籍,32位保护模式这一概念),分配虚拟内存,设定进程描述符等等,最终将进程封闭到一个独立的虚拟内存地址空间中,返回进程句柄;
  2. 操作系统为该进程创建一个线程,并启动它;
  3. 该线程从应用程序的Main方法开始执行。

也即是说,所有的Windows进程,都缺省拥有一个线程,这个线程称为主线程;主线程可以根据代码要求创建其它线程,称为辅助线程

举个形象的比喻,计算机是一个工厂,操作系统是这个工厂的领导者,进程是工厂的车间,线程是工厂的工人。工厂要能够正常运转,需要有领导者统一调配资源,把工作任务分配给每个车间,由派到该车间的工人进行工作。一个正在生产的车间必然得有一名工人,但该工人可以根据工作的需要,随时要求其他工人一起来共同工作。

在计算机中,CPU一般只有一个或几个,而同时运行的线程(一个或多个进程中)却有多个,其数量远远大于CPU的数量,那么,操作系统是如何在有限的CPU上调度这么多线程呢?

image
图1 线程调度示意图

如上图所示,Windows操作系统采用了“时分”的概念来调度多个线程,即每个线程占用CPU一段时间后,就会被强行将执行权交给下一个线程。线程轮流来使用CPU。使用CPU的线程,正常执行程序,时间到达时,操作系统执行线程切换操作:将CPU寄存器的内容全部转移到内存中,将另一个线程保存的寄存器状态从内存恢复到寄存器中,由于寄存器中保存了某个线程正在运行哪一行代码的指针,所以切换后另一个线程可以继续上一次运行状态继续运行。这种切换线程的方式称为:现场保存现场恢复

线程的时分调度,其实也不是将CPU时间平均分配给各个线程,因为不同线程执行的任务总有个轻重缓急之分,所以Windows由采用了一种叫做“抢先式”的方式来进一步调度线程。

在Windows操作系统中,每个进程都有一个优先级,进程中的线程也有一个优先级,优先级越高的进程,操作系统会分配给其线程更多的CPU时间,进程中哪个线程的优先级更高,操作系统也会分配给其更多的CPU事件。

总之一句话:让所有的线程都有机会运行,但优先级高的线程运行的时间更久一些。这就叫做时分抢先式多任务。

前面我们所做的练习,代码都是在一个线程中运行,现在我们知道这个线程叫做“主线程”。后面的课程,我们要学习如何建立辅助线程,在一个进程中调度多个线程。

1 建立并启动线程

和主线程一样,辅助线程也有一个入口方法。主线程的入口方法是Main方法,辅助线程的入口方法可以随意指定,可以是任意对象的符合ParameterizedThreadStart委托类型的任意方法。

ParameterizedThreadStart委托声明如下:

 

即任意返回类型为void,具备一个object类型参数的方法,都可以作为线程的入口方法。

Thread类(System.Threading.Thread类)是.net Framework提供的线程操作封装类,在实例化Thread类对象时指定线程入口方法,然后调用Thread类对象的Start方法启动线程即可。

 

当Thread类型的Start方法被执行后,ThreadWork方法中的代码就开始执行。

对于在主线程(例如Main方法中)里直接调用某个方法的情况,我们称为方法的同步调用,即直到被调用方法执行完毕返回,其后的代码才能够被执行;对于在主线程(例如Main方法中)里通过Thread类的Start方法调用某个方法,我们称为方法的异步调用,即被调用方法和主线程中其后代码是同时执行的。

还可以写的简单点,对于不需要进一步操作Thread对象的情况下(大部分时候都不需要的),也可以不声明Thread类型的变量,一步到位

 

下面我们看一个完整的例子:

建立一个窗体FormMain如下:

 image
图2 主界面

FormMain.cs文件

 

Program.cs文件

 

本节代码下载

代码很简单,有几点需要说明一下:

  • 第30-33行,在FormMain的构造器中,实例化的线程对象,此时只是获取到了线程对象的引用并指定了线程入口方法,线程并未启动;
  • 第72-75行,在startButon按钮的点击事件处理方法中,使用Thread类的Start方法将两个线程启动起来。线程对象thread1和thread2都指定当前类的ThreadWork方法为入口方法,所以相当于调用了ThreadWork方法两次,只不过是异步调用的。此时主线程和两个辅助线程同时运行。这里给线程对象的Start方法设置了参数,该参数会作为arg参数传入线程入口方法(40行);
  • 第77行,将启动线程的按钮设置为禁用,放置再次启动线程——一个线程对象只能启动一个线程;
  • 第40-56行,线程入口方法代码,该方法被两个线程同时执行了两次,各自将一个数字从1开始不断加1。
  • 第44行,一般而言,由于主线程需要处理窗口消息,所以在主线程中执行无限循环会导致窗口失去响应(主线程陷入死循环,无法运行处理窗口消息的代码,这样会导致界面失去响应),但在辅助线程中执行则可以执行这一类无限循环代码(辅助线程运行的同时,不影响主线程运行处理窗口消息的代码),所以使用异步方法来执行费时的工作,让主线程空闲下来,这是提高界面响应能力的主要方法;
  • 第51行,Thread.Sleep方法是让执行这一方法的线程休眠一段时间,在本例中,休眠的作用就是人为的降低当前线程占用CPU的时间。由于这种死循环相当占用CPU,有可能会导致其它线程得不到必要的CPU运行时间,Sleep可以初步解决这个问题;
  • 第48行,Control类(这里被Form类继承)的Invoke方法的作用是这样的:将一个方法的调用插入到窗体的调用队列中,该队列由主线程维护。即插入调用队列的方法会在稍候被主线程执行。由于窗体部分的代码并不是为多线程环境设计的(没有这个必要),所以当我们在一个线程中调用其它线程创建的窗体或其控件的属性和方法时,会引发线程不安全操作(即后面讲到的同步问题),抛出异常;解决方法就是通过窗体对象的Invoke方法将要调用的窗体对象的操作通过委托委托给主线程调用,即不存在任何问题。委托类型可以随意定义(本例定义了一个返回类型为void,具备一个TextBox类型和一个int类型参数的委托类型,第12行)。委托方法中,用方法的第二个整数参数给第一个文本框类型参数的Text属性赋值(第63-65行)。Invoke方法的第一个参数为委托方法,其后的参数为调用该委托方法的实参;
  • 第87-88行,Thread对象的Abord方法用于在一个线程中停止另一个线程的执行,此时该线程立即抛出一个ThreadAbortException异常,通过抛出异常跳出线程代码执行。通过try…catch(ThreadAbortException)…段包围线程方法内的代码,可以在线程被中止后得到一个通知(第42、53行);

执行上述例子,将会看到两个文本框在同时刷新数字,关闭窗口后即可停止线程执行。

image 图3 执行结果

可以看到,两个文本框内的数字并不同,这说明两个线程得到的CPU时间也不尽相同。

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值