




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、.,第9章 C#多线程技术,9.1线程概述 使用C#编写任何程序时,都有一个入口:Main()方法。程序从Main()方法的第一条语句开始执行,直到这个方法返回为止。这样的程序结构非常适合于有一个可识别的任务序列的程序,但程序常常需要同时完成多个任务。例如在使用文字处理软件的时候,用户在输入文字的同时,软件能同步进行拼写检查而不需要用户的等待;再如在一个应用程序的打印功能中,如果程序只能执行一个任务序列,用户可能需要等待所有的打印任务完成后才能继续操作,这时就需要能让程序同时处理多个任务的能力。 在C#应用程序中,第一个线程总是Main()方法,因为第一个线程是由.NET运行库开始执行的,Ma
2、in()方法是.NET运行库选择的第一个方法。后续的线程由应用程序在内部启动,即应用程序可以创建和启动新的线程。,.,9.2 .NET对多线程的支持,在.NET程序设计中,线程是使用Thread类来处理的,该类在System.Threading命名空间中。一个Thread实例管理一个线程,即执行序列。通过简单实例化一个Thread对象,就可以创建一个线程,然后通过Thread对象提供的方法对线程进行管理。 9.2.1 线程的建立与启动 假定我们需要编写一个文件压缩软件,用户点击压缩按钮后开始压缩指定的文件。因为整个压缩过程需要一定的时间才能完成,而用户此时还可能需要移动或缩放程序的窗口,甚至暂
3、停或中止当前文件的压缩。此时一般需要创建一个单独的线程来处理这个压缩过程使得在压缩过程中可以不中断用户界面的响应。因此,我们需要实例化一个Thread对象来创建这个线程: /假设DoCompress是前面已经声明了的一个ThreadStart委托 Thread compressThread = New Thread(entryPoint); 这段代码指定线程对象的实例名为compressThread。在一个应用程序中创建另一个线程,执行一些任务,通常称为工作线程(worker thread),这里compressThread就是一个工作线程,而Main()方法所在的线程常被称为主线程。,.,9
4、.2.1 线程的建立与启动,从代码可以看出,Thread构造函数需要一个参数,用于指定线程的入口即线程开始执行的方法,因为我们传送的是方法的详细信息,所以需要使用委托。实际上,该委托已经在System.Threading命名空间中定义好了。它称为ThreadStart,其声明如下所示: public delegate void ThreadStart(); 传送给构造函数的参数必须是这种类型的委托。上面的例子中是entryPoint,我们来看如何定义这个委托: / 实际线程执行的方法 static void DoCompress() / 压缩代码 ThreadStart entryPoint
5、= new ThreadStart(DoCompress); 线程对象建立完成后,新线程实际上并没有执行任务,它只是在等待执行。我们需要显式地调用Thread对象的Start()方法来启动线程: compressThread.Start(); 此外还可以使用Thread对象的Name属性给线程赋予一个友好的名称。,.,9.2.2 线程的挂起、恢复与终止,启动了一个线程后,线程将运行到所在的方法结束为止,在此期间还可以挂起、恢复或中止它。挂起一个线程就是让它进入睡眠状态,此时,线程仅是停止运行某段时间,不占用任何处理器时间,以后还可以恢复,从被挂起的那个状态重新运行。如果线程被中止,就是停止运行
6、,Windows会永久地删除该线程的所有数据,所以该线程不能重新启动。 继续上面的文件压缩例子,假定由于某些原因,用户界面线程显示一个对话框,允许用户选择临时暂停压缩过程。在主线程中编写如下响应: compressThread.Suspend(); 如果用户以后要求恢复该线程,可以使用下面的方法: CompressThread.Resume() 最后,如果用户决定不需要继续压缩的话,单击取消按钮,可以使用下面的方法: CompressThread.Abort(),.,9.4 线程的优先级,如果在应用程序中有多个线程在运行,但一些线程比另一些线程重要因而需要分配更多的CPU时间该怎么办?在这种情
7、况下,可以在一个进程中为不同的线程指定不同的优先级。一般情况下,如果有优先级较高的线程在工作,就不会给优先级较低的线程分配任何时间片,其优点是可以保证给接收用户输入的线程指定较高的优先级。在大多数的时间内,这个线程什么也不做,而其他线程则执行它们的任务。但是,如果用户输入了信息,这个线程就立即获得比应用程序中其他线程更高的优先级,在短时间内处理用户输入控件。 线程的优先级定义为ThreadPriority枚举类型,取值如表9.1所示:,表9.1 线程的优先级及其含义,.,9.4 线程的优先级,高优先级的线程可以完全阻止低优先级的线程执行,因此在改变线程的优先级时要特别小心以免造成某些线程得不到
8、CPU时间。此外,每个进程都有个基本优先级,这些值与进程的优先级是有关系的。给线程指定较高的优先级,可以确保它在该进程内比其他线程优先执行,但系统上可能还运行着其他进程,它们的线程有更高的优先级。如Windows给自己的操作系统线程指定高优先级。 在【例9.1】中,对Main()方法做如下修改,就可以看出修改线程的优先级的效果: / 建立新线程对象 ThreadStart workerStart = new ThreadStart(DisplayNumbers); Thread workerThread = new Thread(workerStart); workerThread.Name
9、= Worker Thread; workerThread.Priority = AboveNormal;,.,9.4 线程的优先级,其中通过代码设置工作线程的优先级比主线程高,运行结果如下所示: 请输入一个数字:1000000 线程: Main Thread 已开始运行. Main Thread: 当前计数为 1000000 Main Thread: 当前计数为 2000000 Main Thread: 当前计数为 3000000 Main Thread: 当前计数为 4000000 Main Thread: 当前计数为 5000000 Main Thread: 当前计数为 6000000
10、线程: Worker Thread 已开始运行. Worker Thread: 当前计数为 1000000 Worker Thread: 当前计数为 2000000 Worker Thread: 当前计数为 3000000,.,9.4 线程的优先级,Worker Thread: 当前计数为 4000000 Worker Thread: 当前计数为 5000000 Worker Thread: 当前计数为 6000000 Worker Thread: 当前计数为 7000000 Worker Thread: 当前计数为 8000000 线程 Worker Thread 完成. Main Thre
11、ad: 当前计数为 7000000 Main Thread: 当前计数为 8000000 线程 Main Thread 完成. 这说明,当工作线程的优先级为AboveNormal时,一旦工作线程被启动,主线程就不再运行,直到工作线程结束后主线程才重新计算。让我们继续试验操作系统如何对线程分配CPU时间:,.,9.4 线程的优先级,在DisplayNumbers()方法的循环体中加上一句代码,: if(i%interval = 0) Console.WriteLine(thisThread.Name + : 当前计数为 + i); Thread.Sleep(10);/ 让当前工作线程暂停10毫秒
12、 现在来看运行结果: 请输入一个数字:1000000 线程: Main Thread 已开始运行. Main Thread: 当前计数为 1000000 线程: Worker Thread 已开始运行. Worker Thread: 当前计数为 1000000 Main Thread: 当前计数为 2000000 Main Thread: 当前计数为 3000000 Worker Thread: 当前计数为 2000000 Main Thread: 当前计数为 4000000 Worker Thread: 当前计数为 3000000 Worker Thread: 当前计数为 4000000,.
13、,9.4 线程的优先级,Main Thread: 当前计数为 5000000 Worker Thread: 当前计数为 5000000 Worker Thread: 当前计数为 6000000 Main Thread: 当前计数为 6000000 Worker Thread: 当前计数为 7000000 Worker Thread: 当前计数为 8000000 线程 Worker Thread 完成. Main Thread: 当前计数为 7000000 Main Thread: 当前计数为 8000000 线程 Main Thread 完成. 此时的结果与前面有很大的不同,虽然工作线程仍然早
14、于主线程完成,但是在工作线程的计算过程中,主线程也获到了CPU时间。这是因为在DisplayNumbers()方法中使用的Thread静态方法Sleep()放弃了CPU时间,即使当前线程具有较高的优先级,操作系统也会把时间片分配给其他优先级低的线程。如果我们把Sleep()的参数加到100毫秒,运行结果又会有很大的不同,甚至可能两个线程是几乎并行完成的。,.,9.5 线程同步,使用线程的一个重要方面是同步访问多个线程访问的任何变量。所谓同步,是指在某一时刻只有一个线程可以访问变量。如果不能确保对变量的访问是同步的,就可能会产生错误或不可预料的结果。一般情况下,当一个线程写入一个变量,同时有其他
15、线程读取或写入这个变量时,就应同步变量。本节将简要介绍同步的一些主要内容。 9.5.1 同步的含义 同步问题的产生,主要是由于在高级语言的源代码中,大多数情况下看起来是一条语句,但在最后编译好的汇编语言机器码中则会被翻译为许多条语句,从而在操作系统调度时被划分到不同的时间片中。,.,9.5.1 同步的含义,看看下面这个语句,假设message是一个string对象,已经保存了一个字符串: message += Hello world!; 这条语句在C#语法上是一条语句,但在执行代码时,实际上它涉及到许多操作。需要重新分配内存以存储更长的新字符串,需要设置变量message使之指向新的内存,需要
16、复制实际文本等。显然,这里选择了一种复杂字符串,但即使在基本数字类型上执行算术操作,后台进行的操作也比从C#代码中看到的要多。而且,许多操作不能直接在存储于内存空间中的变量上进行,它们的值必须单独复制到处理器的特定位置上,即寄存器。只要一个C#语句翻译为多个本机代码命令,线程的时间片就有可能在执行该语句的进程中终止,如果是这样,同一个进程中的另一个线程就会获得一个时间片,如果涉及到这条语句的变量访问(在上面的示例中,是message)不是同步的,那么另一个线程可能读写同一个变量。,.,9.5.2 在C#中处理同步,通过对指定对象的加锁和解锁可以同步代码段的访问。在.NET的System.Thr
17、eading命名空间中提供了Monitor类来实现加锁与解锁。这个类中的方法都是静态的,所以不需要实例化这个类。表9.2中一些静态的方法提供了一种机制用来同步对象的访问从而避免死锁和维护数据的一致性。,表9.2 Monitor类的主要方法,.,9.5.2 在C#中处理同步,以下是使用Monitor类的简单例子: public void some_method() / 获取锁 Monitor.Enter(this); / 处理需要同步的代码. / 释放锁 Monitor.Exit(this); 上面的代码运行可能会产生问题。当代码运行到获取锁与释放锁之间时一旦发生异常,Monitor.Exit将
18、不会返回。这段程序将挂起,其他的线程也将得不到锁。解决方法是:将代码放入tryfinally内,在finally调用Monitor.Exit,这样的话最后一定会释放锁。,.,9.5.2 在C#中处理同步,C# lock关键字提供了与Monitoy.Enter和Monitoy.Exit同样的功能,这种方法用在你的代码段不能被其他独立的线程中断的情况。通过对Monitor类的简易封装,lock为同步访问变量提供了一个非常简单的方式,其用法如下: lock(x) / 使用x的语句 lock语句把变量放在圆括号中,以包装对象,称为独占锁或排它锁。当执行带有lock关键字的复合语句时,独占锁会保留下来。
19、当变量被包装在独占锁中时,其他线程就不能访问该变量。如果在上面的代码中使用独占锁,在执行复合语句时,这个线程就会失去其时间片。如果下一个获得时间片的线程试图访问变量,就会被拒绝。Windows会让其他线程处于睡眠状态,直到解除了独占锁为止。,.,9.5.2 在C#中处理同步,【例9.2】使用lock同步线程。 本示例建立了10个线程 using System; using System.Threading; /银行帐户类 class Account int balance;/余额 Random r = new Random(); public Account(int initial) bala
20、nce = initial; / 取钱 int Withdraw(int amount) if (balance 0) throw new Exception(余额为负!); ,.,【例9.2】,lock (this) if (balance = amount) Console.WriteLine(原有余额: + balance); Console.WriteLine(支取金额: - + amount); balance = balance - amount; Console.WriteLine(现有余额: + balance); return amount; else return 0;/
21、拒绝交易 ,.,【例9.2】,/ 测试交易 public void DoTransactions() / 支取随机的金额100次 for (int i = 0; i 100; i+) Withdraw(r.Next(1, 100); class TestApp public static void Main() /建立10个线程同时进行交易 Thread threads = new Thread10; Account acc = new Account (1000); for (int i = 0; i 10; i+),.,【例9.2】, Thread t = new Thread(new T
22、hreadStart(acc.DoTransactions); threadsi = t; for (int i = 0; i 10; i+) threadsi.Start(); 在这个示例中,10个线程同时进行交易,如果不加控制,很可能发生在支取金 额时对balance字段的访问冲突。假设当前余额为100,有两个线程都要支取 60,则各自检查余额时都认为可以支取,造成的后果则是总共被支取120,从 而导致余额为负值。读者可以试着将lock语句注释掉再运行,此时将产生余额为 负的异常。,.,9.5.3 同步时要注意的问题,同步线程在多线程应用程序中非常重要。但是,这是一个需要详细讨论的内容,因
23、为很容易出现微妙且难以察觉的问题,特别是死锁。 线程同步非常重要,但只在需要时使用也是非常重要的。因为这会降低性能。原因有两个: 首先,在对象上放置和解开锁会带来某些系统开销,但这些系统开销都非常小。第二个原因更为重要,线程同步使用得越多,等待释放对象的线程就越多。如果一个线程在对象上放置了一个锁,需要访问该对象的其他线程就只能暂停执行,直到该锁被解开,才能继续执行。因此,在lock块内部编写的代码越少越好,以免出现线程同步错误。lock语句在某种意义上就是临时禁用应用程序的多线程功能,也就临时删除了多线程的各种优势。 另一方面,使用过多的同步线程的危险性(性能和响应降低)并没有在需要时不使用同步线程那么高(难以跟踪的运行时错误)。,.,9.5.3 同步时要注意的问题,死锁是一个错误,在两个线程都需要访问被互锁的资源时发生。假定一个线程运行下述代码,其中a和b是两个线程都可以访问的对象引用: lock(a) ( lock(b) / do something 同时,另一个线程运行下述代码: lock(b) ( lock(a) / do
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 商铺租赁合同终止协议
- 食堂劳务派遣用工合同范例二零二五年
- 俱乐部教练合同样本
- oem贴牌合同样本
- 初中开学第一课疫情防控主题班会教案
- 乙供工程合同样本
- 雨棚钢结构施工方案
- 2025年冷芯盒树脂合作协议书
- 小学生外出活动方案
- 盐类的水解第一课时教案
- 《急性阑尾炎幻灯》课件
- 员工黄赌毒法制培训
- 广东省广州市番禺区2023-2024学年八年级上学期期末英语试题(答案)
- 《编制说明-变电站监控系统防止电气误操作技术规范》
- 高中化学基础知识超级判断300题
- 邮政储蓄银行的2024年度借款合同范本
- 汽车吊起重吊装方案
- 从0到1开播指导抖音本地生活商家直播培训
- 产房助产士进修汇报
- 大型综合楼新建工程技术方案、施工方案投标文件(投标方案)
- GB/T 16439-2024交流伺服系统通用技术规范
评论
0/150
提交评论