什么是异步
启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序的资源的集合。这些资源包括虚地址空间、文件句柄和许多其他程序运行所需的东西。
在进程内部,系统创建了一个称为线程的内核(kernel)对象,它代表了真正执行的程序。一旦进程建立,系统会在Main方法的第一行语句处就开始线程的执行。
有关线程的知识:
- 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束。
- 线程可以派生其他线程,因此在任何时刻,一个进程都可能包含不同状态的多个线程,来执行程序的不同部分。
- 如果一个进程有多个线程,它们将共享进程的资源。
- 系统为处理器执行所规划的单元是线程。
在异步程序中,程序代码不需要按照编写时的顺序严格执行。C#5.0引入了构建异步方法的async和await关键字。
使用异步和不使用异步的区别
例:不使用异步的示例。
using System; using System.Diagnostics; using System.Net;
public class MyDownloadString { Stopwatch sw = new Stopwatch();
public void DoRun() { const int LargeNumber = 6_000_000; sw.Start();
int t1 = CountCharacters(1, "http://www.microsoft.com"); int t2 = CountCharacters(2, "http://www.illustratedcsharp.com");
CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber); CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber);
Console.WriteLine("Chars in http://www.microsoft.com : {0}", t1); Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2); }
private int CountCharacters(int id, string uriString) { WebClient wc1 = new WebClient(); Console.WriteLine("Starting call {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds); string result = wc1.DownloadString(new Uri(uriString)); Console.WriteLine(" Call {0} completed: {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; }
private void CountToALargeNumber(int id, int value) { for (long i = 0; i < value; i++) ; Console.WriteLine(" End counting {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds); } }
class Program { static void Main(string[] args) { MyDownloadString ds = new MyDownloadString(); ds.DoRun(); Console.ReadKey(); } }
|
输出结果如下所示,计时以毫秒为单位。每次运行的结果可能不同:
下图总结了输出结果。Call1和Call2占用了大部分时间,而且都浪费在等待网站的响应上。
如果我们能初始化两个CountCharacter调用,无需等待结果,而是直接执行4个CountToALargeNumber调用,然后在两个CountCharacter方法调用结束时再获取结果,就可以显著地提高性能。这里可以使用C#的async/await特性。
- 当DoRun调用CountCharactersAsync时,CountCharactersAsync将立即返回,然后才真正开始下载字符。它向调用方法返回的是一个Task<int>类型的占位符对象,表示它计划进行的工作。这个占位符最终将“返回”一个int。
- 这使得DoRun不用等待实际工作完成就可以继续执行。下一条语句是再次调用CountCharactersAsync,同样会返回一个Task<int>对象。
- 接着,DoRun可以继续执行,调用4次CountToALargeNumber,同时CountCharactersAsync的两次调用继续它们的工作——基本上是等待。
- DoRun的最后两行从CountCharactersAsync调用返回的Tasks中获取结果。如果还没有结果,将阻塞并等待。
例:使用异步示例。
using System; using System.Diagnostics; using System.Net; using System.Threading.Tasks;
public class MyDownloadString { Stopwatch sw = new Stopwatch();
public void DoRun() { const int LargeNumber = 6_000_000; sw.Start();
Task<int> t1 = CountCharactersAsync(1, "http://www.microsoft.com"); Task<int> t2 = CountCharactersAsync(2, "http://www.illustratedcsharp.com");
CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber); CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber);
Console.WriteLine("Chars in http://www.microsoft.com : {0}", t1.Result); Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2.Result); }
private async Task<int> CountCharactersAsync(int id, string site) { WebClient wc = new WebClient(); Console.WriteLine("Starting call {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds); string result = await wc.DownloadStringTaskAsync(new Uri(site)); Console.WriteLine(" Call {0} completed: {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; }
private void CountToALargeNumber(int id, int value) { for (long i = 0; i < value; i++) ; Console.WriteLine(" End counting {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds); } }
|
输出结果:
下图总结了输出结果,展示了修改后的程序的时间轴。新版程序比旧版快了32%!这是因为CountToALargeNumber的4次调用是在CountCharactersAsync方法调用等待网站响应的时候进行的。
async/await特性的结构
如果一个程序调用某个方法,等待其执行所有处理后才继续执行,我们就称这一的方法是同步的,这是默认的。
相反,异步的方法在处理完成之前就返回到调用方法。
C#的async/await特性可以创建并使用异步方法。该特性由三个部分组成:
- 调用方法(calling method):该方法调用异步方法,然后在异步方法执行其任务的时候继续执行。
- 异步(async)方法:该方法异步执行其工作,然后立即返回到调用方法。
- await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告。
异步方法的语法
static class DoAsyncStuff { public static async Task<int> CalculateSumAsync(int i1, int i2) { int sum = await TaskEx.Run(() = > GetSum(i1, i2)); return sum; } }
class Program { static void Main() { Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6); } }
|
什么是异步方法
异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。
异步方法语法特点:
- 方法头中包含async方法修饰符。
- 包含一个或多个await表达式,表示可以异步完成的任务。
- 必须具有三种返回类型,void、Task、Task<T>。Task和Task<T>的返回对象表示将在未来完成的工作。
- 异步方法的参数可以为任何类型任意数量,但不能为out或ref参数。
- 异步方法的名称应该以Async为后缀命名。
- 除了方法以外,Lambda表达式和匿名方法也可以作为异步对象。
例:异步方法示例。
关键字 返回类型 ↓ ↓ async Task<int> CountCharactersAsync(int id, string site) { Console.WriteLine("Starting CountCharacters"); WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync(new Uri(site)); Console.WriteLine("CountCharacters Completed"); return result.Length; }
|
异步方法的组成:
- 异步方法在方法头中必须包含async关键字,而且必须出现在返回类型之前。
- 该修饰符只是标识该方法包含一个或多个await表达式。本身并不能创建任何异步操作。
- async关键字是一个上下文关键字,即除了作为方法修饰符之外,async还可用作标识符。
三种返回类型:
Task<T>:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>。调用方法将通过读取Task的Result属性来获取这个T类型的值。
Task<int> value = DoStuff.CalculateSumAsync(5, 6); Console.WriteLine("Value: {0}", value.Result);
|
Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。这时,即使异步方法中出现了return语句,也不会返回任何东西。
Task someTask = DoStuff.CalculateSumAsync(5, 6); someTask.Wait();
|
void:如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时(调用并忘记[fire and forget]),异步方法可以返回void类型。这时,即使异步方法中出现return语句,也不会返回任何东西。
注意:任何返回Task<T>类型的异步方法,其返回值必须为T类型或可以隐式转换为T的类型。
例:使用返回Task<int>对象的异步方法。
using System; using System.Threading.Tasks;
class Program { static void Main() { Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6); Console.WriteLine("Value: {0}", value.Result); } }
static class DoAsyncStuff { public static async Task<int> CalculateSumAsync(int i1, int i2) { int sum = await Task.Run(() => GetSum(i1, i2)); return sum; } private static int GetSum(int i1, int i2) { return i1 + i2; } }
|
例:使用返回Task对象的异步方法。
using System; using System.Threading.Tasks;
class Program { static void Main() { Task someTask = DoAsyncStuff.CalculateSumAsync(5, 6); someTask.wait(); Console.WriteLine("Async stuff is done"); } }
static class DoAsyncStuff { public static async Task CalculateSumAsync(int i1, int i2) { int value = await Task.Run(() => GetSum(i1, i2)); Console.WriteLine("Value: {0}", value); } private static int GetSum(int i1, int i2) { return i1 + i2; } }
|
例:使用“调用并忘记”的异步方法。
using System; using System.Threading; using System.Threading.Tasks;
class Program { static void Main() { DoAsyncStuff.CalculateSumAsync(5, 6); Thread.Sleep(200); Console.WriteLine("Program Exiting"); } }
static class DoAsyncStuff { public static async void CalculateSumAsync(int i1, int i2) { int value = await Task.Run(() => GetSum(i1, i2)); Console.WriteLine("Value: {0}", value); } private static int GetSum(int i1, int i2) { return i1 + i2; } }
|
异步方法的控制流
异步方法的结构包含三个不同的区域:
- await表达式之前的部分:从方法开头到第一个await表达式之间的所有代码。这部分应该只包含少量且无需长时间处理的代码。
- await表达式:表示将被异步执行的任务。
- 后续部分:在await表达式之后出现的方法中的其余代码。包括其执行环境,如所在线程信息、目前作用域内的变量值,以及当await表达式完成后要重新执行所需的其他信息。
注意:当达到await表达式时,异步方法将控制返回到调用方法。如果方法的返回类型为Task或Task<T>类型,将创建一个Task对象,表示需异步完成的任务和后续,然后将该Task返回到调用方法。
目前有两个控制流:异步方法内的和调用方法内的。异步方法内的代码完成以下工作:
- 异步执行await表达式的空闲任务。
- 当await表达式完成时,执行后续部分。
- 当后续部分遇到return语句或达到方法末尾时,根据返回类型void、Task、Task<T>设置对应的属性并退出控制流。
同时,调用方法中的代码将继续其进程,从异步方法获取Task对象。当需要其实际值时,就引用Task对象的Result属性。届时,如果异步方法设置了该属性,调用方法就能获得该值并继续。否则,将暂停并等待该属性被设置,然后再继续执行。
await表达式
await表达式指定了一个异步执行的任务。
语法:由await关键字和一个空闲对象(称为任务)组成。
这个任务可能是一个Task类型的对象,也可能不是。默认情况下,这个任务在当前线程异步运行。
一个空闲对象即是一个awaitable类型的实例。awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象。awaiter类型包含以下成员。
- bool isCompleted { get; }
- void OnCompleted(Action);
它还包含以下成员之一:
- void GetResult();
- T GetResult(); // T为任意类型
然而实际上,并不需要构建awaitable。应该使用Task类,它是awaitable类型。
Task.Run方法
尽管目前BCL中存在许多返回Task<T>类型对象的方法,但是也可以编写自己的方法。
最简单的方式是在方法中使用Task.Run方法来创建一个Task。Task.Run是在不同的线程上运行方法的。
例:Task.Run方法接受一个Func<TReturn>委托作为参数。
class MyClass { public int Get10() { return 10; }
public async Task DoWorkAsync() { Func<int> ten = new Func<int>(Get10); int a = await Task.Run(ten);
int b = await Task.Run(new Func<int>(Get10));
int c = await Task.Run(() => { return 10; });
Console.WriteLine("{0} {1} {2}", a, b, c); } }
class Program { static void Main() { Task t = (new MyClass()).DoWorkAsync(); t.Wait(); } }
|
上面的代码中,使用的Task.Run的签名以Func<TResult>为参数。该方法共8个重载如下所示:
可能用到的4个委托类型的签名:
例:使用Task.Run方法来运行4种不同的委托类型。
static class MyClass { public static async Task DoWorkAsync() { await Task.Run(() => Console.WriteLine(5.ToString()));
Console.WriteLine((await Task.Run(() => 6)).ToString());
await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString())));
int value = await Task.Run(() => Task.Run(() => 8)); Console.WriteLine(value.ToString()); } }
class Program { static void Main() { Task t = MyClass.DoWorkAsync(); t.Wait(); } }
|
任何可以使用表达式的地方,都可以使用await表达式,只要位于异步方法内。上面的代码中:
- 第一个和第三个实例将await表达式用作语句。
- 第二个实例将await表达式用作WriteLine方法的参数。
- 第四个实例将await表达式用作赋值语句的右端。
例:用可接受的Func委托的形式创建一个Lambda函数。
static class MyClass { private static int GetSum(int i1, int i2) { return i1 + i2; } public static async Task DoWorkAsync() { int value = await Task.Run(() => GetSum(5, 6)); Console.WriteLine(value.ToString()); } }
class Program { static void Main() { Task t = MyClass.DoWorkAsync(); t.Wait(); } }
|