第14章 多线程和异步程序设计 14.1 多线程程序设计 14.2 异步程序设计
14.1 多线程程序设计 14.1.1 多线程的概述 当一个程序开始运行时,它就是一个进程。 14.1 多线程程序设计 14.1.1 多线程的概述 当一个程序开始运行时,它就是一个进程。 进程所指包括执行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
从上图可以看出,几乎所有的进程都拥有两个以上的线程。从而可以看出,线程是提供应用程序性能的重要手段之一,尤其在多核CPU的机器上尤为明显。
一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并具有多线程创建额外的线程。 在C#应用程序中,第一个线程总是Main()方法,因为第一个线程是由.NET运行库开始执行的,Main()方法是.NET运行库选择的第一个方法。后续的线程由应用程序在内部启动,即应用程序可以创建和启动新的线程。
多线程:在同一时间执行多个任务的功能,称为多线程或自由线程。 多线程的优点:可以同时完成多个任务;可以使程序的响应速度更快;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;可以设置每个任务的优先级以优化程序性能。 多线程的缺点:对资源的共享访问可能造成冲突(对共享资源的访问进行同步或控制) ;程序的整体运行速度减慢等等。
14.1.2 线程命名空间 线程命名空间是System.Threading,它提供了多线程程序设计的类和接口等,用以执行诸如创建和启动线程、同步多个线程、挂起线程和中止线程等任务。
其中有关线程方面的类如下: Thread类:用于创建并控制线程、设置其优先级并获取其状态。 Monitor类:提供同步对对象的访问的机制。 Mutex类:一个同步基元,也可用于进程间同步。 ThreadAbortException类:在对Abort方法进行调用时引发的异常。无法继承此类。 ThreadInterruptedException类:中断处于等待状态的Thread时引发的异常。 ThreadStartException类:当基础操作系统线程已启动但该线程尚未准备好执行用户代码前,托管线程中出现错误,则会引发异常。 ThreadStateException类:当Thread处于对方法调用无效的ThreadState时引发的异常。
14.1.3 Thread类及其应用 1. Thread类 Thread类是最重要的线程类。 属性 说明 CurrentThread 获取当前正在运行的线程 IsAlive 获取一个值,该值指示当前线程的执行状态 IsBackground 获取或设置一个值,该值指示某个线程是否为后台线程 Name 获取或设置线程的名称 Priority 获取或设置一个值,该值指示线程的调度优先级 ThreadState 获取一个值,该值包含当前线程的状态
ThreadState属性的取值及其说明 成员名称 说 明 Aborted 线程已终止,但其状态尚未更改为 Stopped AbortRequested 已对线程调用了Abort方法,正在请求终止 Background 线程正作为后台线程执行 Running 线程正在运行 Stopped 线程已停止 StopRequested 正在请求线程停止,这仅用于内部 Suspended 线程已挂起 SuspendRequested 正在请求线程挂起 Unstarted 线程尚未启动 WaitSleepJoin 由于调用Thread.Sleep 或 Thread.Join等,线程已被阻止(等待或挂起)
方法 说明 Abort 在调用此方法的线程上引发ThreadAbortException,以开始终止此线程的过程。调用此方法通常会终止线程 Interrupt 中断处于等待、休眠或联接状态的线程 Join 阻塞调用线程,直到某个线程终止时为止。t.Join()可以理解为把线程t放到当前位置来执行,只有t结束以后才会执行t.Join()以后的代码 Sleep 将当前线程阻塞指定的毫秒数 Start 使线程得以按计划执行 ResetAbort 取消为当前线程请求的 Abort
导致状态更改的操作 操作 当前线程的ThreadState状态值 在公共语言运行库中创建线程 Unstarted 线程调用Start 线程开始运行 Running 线程调用 Sleep WaitSleepJoin 线程对其他对象调用Wait 线程对其他线程调用Join 另一个线程调用Interrupt 另一个线程调用Abort AbortRequested 线程响应 Abort 请求 Stopped 线程被终止
C#线程状态转换图
2. 创建和启动新线程 应用程序执行时,将创建新的应用程序域。当执行环境调用应用程序的入口点(Main方法)时,将创建应用程序的主线程。主线程以外的线程一般称为工作线程。
Thread类的主要构造函数如下: (1)public Thread(ThreadStart start) 其中,参数start为ThreadStart委托,它表示此线程开始执行时要调用的方法。 (2)public Thread(ParameterizedThreadStart start) 其中,参数start为ParameterizedThreadStart委托,表示此线程开始执行时要调用的方法。它表示初始化Thread类的新实例,并指定允许对象在线程启动时传递给线程的委托。
例如,有以下类: class MyClass //声明包含方法的类 { … { … public void method1() //不带参数的方法method1 { … } public void method2(object data) //带参数的方法method2
采用静态方式创建和启动工作线程的过程如下: MyClass obj = new MyClass(); //创建MyClass的实例 Thread workth1 = new Thread(obj.method1);//创建一个工作线程workth1 workth1.Start(); //启动工作线程workth1 … Thread workth2 = new Thread(obj.method1);//创建一个工作线程workth2 workth1.Start(10); //启动工作线程workth2,其中10为实参
采用委托方式创建和启动工作线程的过程如下: delegate void deletype1(); //声明委托类型deletype1 delegate void deletype2(object obj); //声明委托类型deletype2 … deletype1 mydele1; //定义委托变量mydele1 deletype2 mydele2; //定义委托变量mydele2 MyClass a = new MyClass(); //创建MyClass类的实例 mydele1 = new deletype1(a.method1); mydele2 = new deletype2(a.method2); Thread workth1 = new Thread(new ThreadStart(mydele1)); //创建一个工作线程workth1 workth1.Start(); //启动工作线程workth1 Thread workth2 = new Thread(new //创建一个工作线程workth2 ParameterizedThreadStart(mydele2)); workth2.Start(10); //启动工作线程workth2,其中10为实参
3. 暂停线程 一旦线程已启动,就可以调用其方法来更改它的状态。例如,通过调用Thread.Sleep()可以使线程暂停一段时间(以毫秒为单位)。其使用语法格式如下: Sleep(n); 其中n为挂起的毫秒数,即线程保持锁定状态的毫秒数。 如果使用参数Infinite调用Sleep,则会导致线程休眠,直至调用Interrupt的另一个线程将其唤醒为止。
4. 中断线程Interrupt() Interrupt()方法会将目标线程从其可能处于的任何等待状态中唤醒,并导致引发ThreadInterruptedException异常。 通过对被阻止的线程调用Interrupt()方法可以中断正在等待的线程,从而使该线程脱离造成阻止的调用。
5. 销毁线程Abort() Abort()方法用于永久地停止托管线程。调用Abort时,公共语言运行库在目标线程中引发ThreadAbortException,目标线程可捕捉此异常。 Abort()方法不直接导致线程中止,因为目标线程可捕捉ThreadAbortException并在finally块中执行任意数量的代码。 如果需要等待线程结束,可调用Join()方法。Thread.Join()是一种模块化调用,它在线程实际停止执行之前,或可选超时间隔结束之前不会返回。等待对Join()方法的调用的线程可由其他线程调用Interrupt()来中断。 注意:一旦线程被中止,它将无法重新启动。如果线程已经在中止,则不能通过Start()来启动线程。
【例14.1】 分析以下程序的执行结果。 using System; using System.Threading; //新增引用 namespace proj14_1 { public class A { public void fun() //定义类A的方法 { while (true) { Console.WriteLine("工作线程:正在执行A.fun方法..."); } } };
public class Program { public static void Main() { Console.WriteLine("主线程启动..."); A a = new A(); //创建A类的实例 Thread workth = new Thread(new ThreadStart(a.fun)); //创建一个线程,使之执行A类的fun()方法 Console.WriteLine("工作线程启动..."); workth.Start(); //启动工作线程workth while (!workth.IsAlive); //循环直到工作线程激活 Console.WriteLine("主线程睡眠1ms..."); Thread.Sleep(1); //让主线程睡眠1ms,以允许工作线程完成自已的工作 Console.WriteLine("终止工作线程"); workth.Abort(); Console.WriteLine("阻塞工作线程"); workth.Join();
try { Console.WriteLine("试图重新启动工作线程"); workth.Start(); } catch (ThreadStateException) //捕捉workth.Start()的异常 { Console.WriteLine("终止后的线程不能重启,”+ ”在重启时引发相关异常"); Console.WriteLine("主线程结束");
解:Main()方法的执行创建了主线程,其中创建了一个工作线程workth,通过Start()方法调用启动它,然后让主线程睡眠1ms以执行工作主线程,再通过调用Abort()和Join()方法终止工作线程。当试图再次启动已终止的工作线程时出现异常。Main()方法执行完毕,主线程已结束。
14.1.4 线程优先级和线程调度 每个线程都具有分配给它的线程优先级,通过Thread类的Priority属性设定,该属性是一个枚举值。 成员名 说明 Lowest 可以将Thread安排在具有任何其他优先级的线程之后 BelowNormal 可以将Thread安排在具有Normal优先级的线程之后,在具有Lowest优先级的线程之前 Normal 可以将Thread安排在具有AboveNormal优先级的线程之后,在具有BelowNormal优先级的线程之前。默认情况下,线程具有Normal优先级 AboveNormal 可以将Thread安排在具有Highest优先级的线程之后,在具有Normal优先级的线程之前 Highest 可以将Thread安排在具有任何其他优先级的线程之前
【例14.2】分析以下程序的执行结果。 using System; using System.Threading; namespace proj14_2 { class A //声明类A { bool looptag; public A() //构造函数 { looptag = true; } public bool plooptag //定义属性plooptag { set { looptag = value; } }
public void fun() //定义类A的方法 { long thcount = 0; //线程循环计数器 while (looptag) { thcount++; //累计循环次数 } Console.WriteLine("{0}优先级为:{1,12},循环次数为:{2}", Thread.CurrentThread.Name, Thread.CurrentThread.Priority.ToString(), thcount.ToString());
class Program { static void Main() { A a = new A(); Thread workth1 = new Thread(a.fun); Console.WriteLine("启动主线程..."); workth1.Name = "工作线程1"; Thread workth2 = new Thread(a.fun); workth2.Name = "工作线程2"; workth2.Priority = ThreadPriority.BelowNormal; Console.WriteLine("启动工作线程1..."); workth1.Start(); Console.WriteLine("启动工作线程2..."); workth2.Start(); Console.WriteLine("等待1秒以便执行工作线程...");
Thread.Sleep(1000); //主线程睡眠1秒以便执行工作线程 a.plooptag = false; //设为false使工作线程完成自动的工作 Console.WriteLine("等待1秒以便输出统计结果..."); Thread.Sleep(1000); //主线程睡眠1秒以便工作线程输出 Console.WriteLine("终止工作线程1"); workth1.Abort(); workth1.Join(); Console.WriteLine("终止工作线程2"); workth2.Abort(); workth2.Join(); Console.WriteLine("主线程结束"); }
解:上述程序的主线程中创建了两个工作线程workth1和workth2,在启动后等待它们执行1秒钟(这1秒钟都在执行while循环语句),然后中止各工作线程中的循环语句,再等待1秒钟输出(这1秒钟工作线程执行输出),最后终止工作线程。
14.1.5 线程互斥 多个线程在同时修改共享数据时可能发生错误,这样的共享数据称为临界区。 多个线程在同时修改共享数据时可能发生错误,这样的共享数据称为临界区。 线程互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
lock语句将一个语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。此语句的一般形式如下: Object thisLock = new Object(); lock(thisLock) { //访问共享资源的代码 } 其中lock的参数指定要锁定的对象,锁定该对象内的所有临界区,必须是引用类型。例如控制台应用程序中一般像上述方式使用,而在窗体一般将thisLock改为this,表示引用当前窗体。
【例】 在项目中有一个窗体Form2,其上有一个命令按钮button1和一个列表框listBox1,该窗体的程序如下: using System; using System.Windows.Forms; using System.Threading; namespace proj14 { public partial class Form2 : Form { int x; Random r = new Random(); //定义随机数对象 public Form2() { InitializeComponent(); }
private void button1_Click(object sender, EventArgs e) { int i; Thread workth1, workth2; for (i = 0; i < 100; i++) //循环100次 { x = 1; workth1 = new Thread(add); //创建工作线程1 workth2 = new Thread(sub); //创建工作线程2 workth1.Start(); //启动工作线程1 workth2.Start(); //启动工作线程2 Thread.Sleep(100); workth1.Abort(); //终止工作线程1 workth2.Abort(); //终止工作线程2 if (listBox1.FindString(x.ToString())==-1) listBox1.Items.Add(x); //当在listBox1中未找到时将x添加进去 listBox1.Items.Add("执行完毕"); }
public void add() //方法add() { int n; n = x; //取出x的值 n++; Thread.Sleep(r.Next(5,20)); //睡眠5~20ms x = n; //存回到x中 } public void sub() //方法sub() { int n; n--; Thread.Sleep(r.Next(5,20)); //睡眠5~20ms x = n; //存回到x中 } }
对这两个线程来说,x是共享变量。x的值应为1。
根据lock语句,可将add()和sub()方法的代码修改如下 public void add() //方法add { int n; lock (this) { n = x; //取出x的值 n++; Thread.Sleep(r.Next(5, 20)); //睡眠5~20ms x = n; //存回到x中 } public void sub() //方法sub { int n; n--;
修改后的执行结果
2. 用Mutex类实现线程互斥 可以使用Mutex对象提供对资源的独占访问。线程调用mutex的WaitOne方法请求所有权。该调用会一直阻塞到mutex可用,或直至达到可选的超时间隔。 如果没有任何线程拥有它,则Mutex的状态为已发信号的状态。 线程通过调用ReleaseMutex方法释放mutex。mutex具有线程关联,即mutex只能由拥有它的线程释放。如果线程释放不是它拥有的mutex,则会在该线程中引发ApplicationException异常。
3. 用Monitor类实现线程互斥 Monitor类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问代码块(通常称为临界区)的能力。 当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用Monitor来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。
Monitor类的主要静态方法如下: Enter():获取对象锁。此操作同样会标记临界区的开头。其他任何线程都不能进入临界区,除非它使用其他锁定对象执行临界区中的指令。 Wait():释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。 Pulse():向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。 Exit():释放对象上的锁。此操作还标记受锁定对象保护的临界区的结尾。
14.1.6 线程同步 线程同步是指多个相互关联的线程在某些确定点上协调工作,即需要互相等待或互相交换信息。 线程同步是指多个相互关联的线程在某些确定点上协调工作,即需要互相等待或互相交换信息。 假设这样一种情况,两个线程同时维护一个队列,如果一个线程对队列中添加元素,而另外一个线程从队列中取用元素,那么称添加元素的线程为生产者,称取用元素的线程为消费者。生产者与消费者问题看起来很简单,但是却是多线程应用中一个必须解决的问题,它涉及到线程之间的同步和通信问题。
【例14.5】 设计一个Windows应用程序项目Prog14-3,在窗体Form1上放置一个“开始”命令按钮button1和一个文本框textBox1,对应的程序如下: using System; using System.Windows.Forms; using System.Threading;
namespace proj14_3 { public partial class Form1 : Form { int x=0, sum=0; //类变量,在所有方法中有效 Random r = new Random(); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Thread workth1 = new Thread(put); //创建工作线程1 Thread workth2 = new Thread(get); //创建工作线程2 workth1.Start(); //启动工作线程1 workth2.Start(); //启动工作线程2 Thread.Sleep(600); workth1.Abort(); //终止工作线程1 workth2.Abort(); //终止工作线程2 textBox1.Text = "消费总数=" + sum.ToString(); }
分析单击“开始”命令按钮后,textBox1中显示的结果。 public void put() //生产者 { int k; for (k = 1; k <= 4; k++) { x = k; Thread.Sleep(r.Next(20, 50)); //睡眠2-50ms } public void get() //消费者 { int k; { sum += x; 分析单击“开始”命令按钮后,textBox1中显示的结果。
解:上述程序中,工作线程1和工作线程2分别执行方法put()和get(),分别称为生产者线程和消费者线程。生产者线程向x中放入1、2、3、4,消费者线程从中取出x的数据并求和sum,最后主线程输出sum,sum的值应为1+2+3+4=10。 但问题是两个线程并发执行,不能保证每放入一个x,等x取出后再放下一个x,这样sum的结果可能小于10。
如何解决这个问题呢?需要使这两个线程同步。实现线程同步方法较多,下面提供一种解决方法。 如何解决这个问题呢?需要使这两个线程同步。实现线程同步方法较多,下面提供一种解决方法。 使用Monitor类这样修改例14.5的程序,对应该项目中的Form7窗体,在窗体级增加以下字段: bool mark = false;
为此修改put()和get()方法的代码修改如下: public void put() //生产者 { int k; for (k = 1; k <= 4; k++) { Monitor.Enter(this); //加排它锁 if (mark) //若mark为true,不能放数据,本线程等待 Monitor.Wait(this); mark = !mark; //将mark由false改为true x = k; //放数据 Thread.Sleep(r.Next(20, 50)); //睡眠2-50ms Monitor.Pulse(this); //激活消费者线程 Monitor.Exit(this); //释放排它锁 } }
public void get() //消费者 { int k; for (k = 1; k <= 4; k++) { Monitor.Enter(this); //加排它锁 if (!mark) //若mark为false,不能取数据,本线程等待 Monitor.Wait(this); mark = !mark; //将mark由true改为false sum += x; //累加数 Thread.Sleep(r.Next(20, 50)); //睡眠2-50ms Monitor.Pulse(this); //激活生产者线程 Monitor.Exit(this); //释放排它锁 } }
其他地方不做改动,这样程序多次执行的结果都如图13.10所示,消费总数为10,结果正确,从中看到两线程实现同步操作。
14.1.7 volatile 关键字用于简单的线程同步 C#编译器提供了volatile关键字,该关键字告诉C#和JIT编译器不再在CPU寄存器中缓存字段,从而确保字段的所有读写操作都是对主存的读写。 当多个线程同时访问一个变量时,CLR为了效率,允许每个线程进行本地缓存,这就导致可能出现变量不一致性的情况。volatile关键字就是为了解决这个问题,用volatile修饰的变量不允许线程进行本地缓存,每个线程的读写都是直接操作在共享主存上,这就保证了变量始终具有一致性,但也牺牲了部分效率。
14.1.8 线程池 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。 如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个工作线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个工作线程,但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
一般使用ThreadPool类创建线程池。 ThreadPool类提供一个线程池,该线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器,该类的命名空间为 System.Threading。
ThreadPool类的常用方法如下: ① GetMaxThreads方法用于检索可以同时处于活动状态的线程池请求的数目。 所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。 ② GetMinThreads方法用于检索在发出新的请求时,切换到管理线程之前线程池按需创建的线程的最小数量。 ③ QueueUserWorkItem(WaitCallback,Object)和QueueUserWorkItem(WaitCallback)方法将要执行的方法排入队列以便执行,并指定包含该方法所用数据的对象。
【例14.6】现要计算f(x)=x2+ 的值,将计算x2值和 值分别作为两个线程放到线程池中。设计一个控制台应用程序项目完成该任务。 using System; using System.Threading; namespace proj14_5 { class Program { static double number1 = -1; //存放要计算的数值的字段 static double number2 = -1; public static void Main() { // 获取线程池的最大线程数和维护的最小空闲线程数 int maxThreadNum, portThreadNum; int minThreadNum; ThreadPool.GetMaxThreads(out maxThreadNum, out portThreadNum); ThreadPool.GetMinThreads(out minThreadNum, out portThreadNum); Console.WriteLine("最大线程数:{0}", maxThreadNum); Console.WriteLine("最小线程数:{0}", minThreadNum);
int x = 64; //函数变量值 Console.WriteLine("启动第一个任务:计算{0}的2次方", x); ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc1), x); Console.WriteLine("启动第二个任务:计算{0}的2次方根", x); ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc2), x); Thread.Sleep(1000); while (number1 == -1 || number2 == -1) ; //等待直到两个数值都完成计算 Console.WriteLine("f({0}) = {1}", x, number1+number2); //输出计算结果 } static void TaskProc1(object o) //计算x的2次方 { number1 = Math.Pow(Convert.ToDouble(o),2); } static void TaskProc2(object o) //计算x的2次方根 { number2 = Math.Pow(Convert.ToDouble(o),1.0/2.0); }
本项目的执行结果:
14.2 异步程序设计 14.2.1 异步的概念 同步方法调用在程序继续执行之前需要等待同步方法执行完毕返回结果,而异步方法则在被调用之后立即返回以便程序在被调用方法完成其任务的同时执行其他操作。
C#程序的异步操作由.NET Framework支持,.NET Framework提供了执行异步操作的三种模式: 基于任务的异步模式(Task-based Asynchronous Pattern,TAP)使用一种方法来表示异步操作的启动和完成。TAP是在.NET Framework 4中引入的。C# 中的async 和 await 关键词为TAP添加了语言支持。 异步编程模型(Asynchronous Programming Model,APM)模式(也称 IAsyncResult 模式),在此模式中异步操作需要Begin和End方法(比如用于异步写入操作的 BeginWrite 和 EndWrite)。 基于事件的异步模式(Event-based Asynchronous Pattern,EAP),这种模式需要 Async 后缀,也需要一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。
14.2.2 同步和异步的差别 创建一个Windows应用程序项目proj14-6,在其中添加一个Form1窗体,用于同步操作,设计界面如图14.13所示,包含一个多行文本框textBox1和一个命令按钮button1。
using System; using System.Windows.Forms; using System.Net; using System.Diagnostics; namespace proj14_5 { public partial class Form1 : Form { Stopwatch sw = new Stopwatch(); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { textBox1.Clear(); DoRun(); textBox1.Text += "-----执行完毕"; }
public void DoRun() { sw.Start(); CountCharacters("http://www.microsoft.com"); CountCharacters("http://www.whu.edu.cn"); CountTo(1, 6000000); CountTo(2, 7000000); CountTo(3, 8000000); CountTo(4, 9000000); }
private void CountCharacters(string uristr) { WebClient wc1 = new WebClient(); textBox1.Text += string.Format("访问网站: {0}\r\n开始时刻: {1}ms\r\n“, uristr,Math.Ceiling(sw.Elapsed.TotalMilliseconds).ToString()); string result = wc1.DownloadString(new Uri(uristr)); textBox1.Text += string.Format("结束时刻: {0}ms\r\n" , Math.Ceiling(sw.Elapsed.TotalMilliseconds).ToString()); textBox1.Text+="访问"+uristr+"的字符数: "+ result.Length.ToString()+"\r\n"; }
private void CountTo(int id, int value) { textBox1.Text += string.Format("计算{0}开始时刻: {1}ms\r\n“,id, Math.Ceiling(sw.Elapsed.TotalMilliseconds).ToString()); for (long i = 0; i < value; i++) ; textBox1.Text += string.Format("计算{0}结束时刻: {1}ms\r\n", id, Math.Ceiling(sw.Elapsed.TotalMilliseconds).ToString()); }
启动Form1窗体,单击“开始”命令按钮,它的一次执行结果如图14.14所示。 同步执行
现在改为异步方式。在proj14-6项目中添加一个Form2窗体,其设计界面与Form1相同。Form2窗体的代码如下: using System; using System.Windows.Forms; using System.Net; using System.Threading.Tasks; using System.Diagnostics; namespace proj14_5 { public partial class Form2 : Form { Stopwatch sw = new Stopwatch(); public Form2() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { button1.Enabled = false; textBox1.Clear(); DoRun(); button1.Enabled = true; }
public void DoRun() { sw.Start(); Task t1=CountCharacters("http://www.microsoft.com"); Task t2=CountCharacters("http://www.whu.edu.cn"); CountTo(1, 6000000); CountTo(2, 7000000); CountTo(3, 8000000); CountTo(4, 9000000); }
private async Task CountCharacters(string uristr) { WebClient wc1 = new WebClient(); textBox1.Text += string.Format("访问{0}的开始时刻: {1}ms\r\n", uristr,Math.Ceiling(sw.Elapsed.TotalMilliseconds).ToString()); string result = await wc1.DownloadStringTaskAsync(new Uri(uristr)); textBox1.Text += string.Format("访问{0}的结束时刻: {1}ms\r\n", textBox1.Text += "访问" + uristr +"的字符数: " + result.Length.ToString() + "\r\n"; }
private void CountTo(int id, int value) { textBox1.Text += string.Format("计算{0}的开始时刻: {1}ms\r\n“,id, Math.Ceiling(sw.Elapsed.TotalMilliseconds).ToString()); for (int i = 0; i < value; i++) ; textBox1.Text += string.Format("计算{0}的结束时刻: {1}ms\r\n", id, Math.Ceiling(sw.Elapsed.TotalMilliseconds).ToString()); }
启动Form2窗体,单击“开始”命令按钮,它的一次执行结果如下图所示。 异步执行 与Form1窗体相比,Form2窗体的执行时间减少了296ms,速度提高了39.2%。
14.2.3 TAP异步模式编程 1. async/await关键字 await 关键字用于异步方法内部,它挂起该异步方法的执行直到等待任务完成。包含有await 关键字的方法为异步方法,带有await 关键字的表达式称为await 表达式。
2. Task类和Task<TResult>类 Task类用于表示一个异步操作,Task<TResult>类是Task类的泛型版本,TResult类型为Task所调用方法的返回值。它们都位于System.Threading.Tasks命名空间。 主要区别在于Task构造函数接受的参数是Action委托(用于封装一个方法,该方法不具有参数并且不返回值),而Task<TResult>接受的是Func<TResult>委托(封装一个不具有参数但却返回 TResult 参数指定的类型值的方法)。 Task类的基本构造函数为:Task(Action action),其中action是Action 委托,用于封装一个方法。
Task类的属性 说明 Id 获取此 Task 实例的唯一ID IsCanceled 获取此Task实例是否由于被取消的原因而已完成执行 IsCompleted 获取此Task是否已完成 IsFaulted 获取Task是否由于未经处理异常的原因而完成 Status 获取此任务的状态
Task类的方法 说明 Delay(Int32) 创建将在时间延迟后完成的任务 Run(Action action) 将在线程池上运行的指定工作排队,并返回该工作的任务句柄 Run(Func<Task> function) 将在线程池上运行的指定工作排队,并返回 function 返回的任务的代理项 Run<TResult>(Func<Task<TResult>>) 将在线程池上运行的指定工作排队,并返回 function 返回的Task(TResult) 的代理项 Run<TResult>(Func<TResult>) 将在线程池上运行的指定工作排队,并返回该工作的 Task(TResult) 句柄 Start() 启动 Task,并将它安排到当前的任务调度器中执行 Wait() 等待 Task 完成执行过程 WaitAll(Task[]) 等待提供的所有 Task 对象完成执行过程
【例14.7】有如下控制台应用程序项目proj14-7的代码,分析执行结果。 using System; using System.Threading.Tasks; namespace proj14_7 { class Program { public static void Main() { Action myaction = disp; Task task1 = new Task(myaction); task1.Start(); Console.ReadKey(); } public static void disp() { Console.WriteLine("Task"); }
或者直接使用Lambda表达式: using System; using System.Threading.Tasks; namespace tmp { class Program { public static void Main() { Task task1 = new Task(()=>Console.WriteLine("Task")); task1.Start(); Console.ReadKey(); }
解:在Main方法中,通过实例化一个Task对象task1,然后用Start方法启动执行,在屏幕上显示“Task”。也可以直接使用Run方法,以下的等价的Main方法代码: Action myaction = disp; Task.Run(myaction); Console.ReadKey();
3. 异步方法设计 从前面示例看出,异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。从语法上讲,异步方法具有如下特征: ① 方法头部中包含async修饰符。 ② 包含一个或多个await表达式,表示可以异步完成的任务。 ③ 异步方法的参数可以任意个数任意类型,但不能为out或ref参数。
④ 异步方法的返回类型如下: void:如果调用方法仅仅想执行异步方法,而不需要与它做任何交互时,异步方法可以返回void类型,即不返回任何东西。所以,返回类型为void的异步方法中一般不包含return语句,即使有return语句,也不返回任何东西。 Task:如果调用方法不需要从异步方法返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。所以,返回类型为Task的异步方法中一般不包含return语句,即使有return语句,也不返回任何东西。 Task<T>:如果调用方法需要从异步方法返回某个T类型的值,异步方法的返回类型必须是Task<T>。调用方法通过读取异步方法返回的Task<T>类型对象的Result属性来获取这个T类型的值。所以,返回类型为Task<T>的异步方法中应有返回T类型值的return语句。
其中,task指定一个异步任务。这个任务通常是一个Task<T>类型的对象。默认情况下,这个任务在当前线程异步运行。 4. await表达式 await表达式的一般格式如下: await task 其中,task指定一个异步任务。这个任务通常是一个Task<T>类型的对象。默认情况下,这个任务在当前线程异步运行。
在.NET Framework 4.5版本中发布了大量的执行异步任务的异步方法。例如,前面Form2窗体中用到的WebClient类,就提供了异步下载的DownloadStringTaskAsync方法,它们的名称都以Async为后缀。
例如,以下是自己编写的异步方法(通常名称以Async为后缀): 除了使用.NET Framework 4.5版本中现成的异步方法外,用户可以编写自己的异步方法。最简单的方式是使用Task.Run方法来创建一个Task。 例如,以下是自己编写的异步方法(通常名称以Async为后缀): public async Task<string> methodAsync() { await Task.Delay(10000); //延迟10000ms return "Finished"; } 可以采用如下语句调用该异步方法: string result = await methodAsync();
5. 异步方法的控制流 一般地,一个异步方法包含3个不同的区域: 第一个await表达式之前的部分:从异步方法开始到第一个await表达式之间的所有代码。 await表达式:表示被异步执行的任务。 后续部分:在await表达式之后的其余代码。
例如,一个异步方法CharCountAsync的上述3个部分如下:
异步方法的控制流如图14.17所示。
6. 在调用方法中同步地等待异步任务 调用方法可以调用任意多个异步方法并接受它们返回的Task对象,然后代码会继续执行其他任务。但在某个点上可能会需要等待某个特殊的Task对象完成,然后再继续。 为此,Task类提供了Wait方法,可以在Task对象上调用该方法,用于等待该 Task任务的完成(WaitAll方法用于等待所有指定的 Task 任务的完成)。
【例14.9】设计一个控制台应用程序项目proj14-9,说明在调用方法中同步地等待异步任务的实现过程。 using System; using System.Threading.Tasks; using System.Net;
namespace proj14_9 { class MyClass { public void DoRun() { Task<int> t1 = CountCharacters("https://jw.cicc.com.cn"); t1.Wait(); Task<int> t2 = CountCharacters("http://www.chinahr.com"); t2.Wait(); Console.WriteLine("t1任务是否完成:{0}", t1.IsCompleted); Console.WriteLine("t2任务是否完成:{0}", t2.IsCompleted); Console.WriteLine("返回的总字符数:{0}", t1.Result+t2.Result); Console.ReadKey(); }
private async Task<int> CountCharacters(string myuri) { string result=await new WebClient().DownloadStringTaskAsync(new Uri(myuri)); return result.Length; } class Program { static void Main(string[] args) { MyClass s = new MyClass(); s.DoRun();
上述程序中,DoRun调用方法有两处调用异步方法CountCharacters,通过使用Wait方法,t1任务完成后,再执行t2任务,从而达到同步的目的。
━━本章完━━