什么是异步
启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序的资源的集合。这些资源包括虚地址空间、文件句柄和许多其他程序运行所需的东西。
在进程内部,系统创建了一个称为线程的内核(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);
}
/// <summary>
/// 下载某网站的内容,返回该网站包含的字符数
/// </summary>
/// <param name="id"></param>
/// <param name="uriString"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 执行一个消耗一定时间的任务
/// </summary>
/// <param name="id"></param>
/// <param name="value"></param>
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。 - 这使得DoRun不用等待实际工作完成就可以继续执行。下一条语句是再次调用CountCharactersAsync,同样会返回一个Task
对象 。 - 接着,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) // Task<int>表示正在执行的工作,最终将返回int
{
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)); // await表达式
return sum;
}
}
class Program
{
// 调用方法
static void Main()
{
Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6);
}
}
什么是异步方法
异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。
异步方法语法特点:
- 方法头中包含async方法修饰符。
- 包含一个或多个await表达式,表示可以异步完成的任务。
- 必须具有三种返回类型,void、Task、Task
。Task和Task 的返回对象表示将在未来完成的工作。 - 异步方法的参数可以为任何类型任意数量,但不能为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)); // await表达式
Console.WriteLine("CountCharacters Completed");
return result.Length; // 返回语句
}
异步方法的组成:
- 异步方法在方法头中必须包含async关键字,而且必须出现在返回类型之前。
- 该修饰符只是标识该方法包含一个或多个await表达式。本身并不能创建任何异步操作。
- async关键字是一个上下文关键字,即除了作为方法修饰符之外,async还可用作标识符。
三种返回类型:
Task
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
例:使用返回Task
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); // 由于使用了Thread.Sleep方法来暂停当前线程,所以异步方法完成时它还没完成
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
目前有两个控制流:异步方法内的和调用方法内的。异步方法内的代码完成以下工作:
- 异步执行await表达式的空闲任务。
- 当await表达式完成时,执行后续部分。
- 当后续部分遇到return语句或达到方法末尾时,根据返回类型void、Task、Task
设置对应的属性并退出控制流 。
同时,调用方法中的代码将继续其进程,从异步方法获取Task对象。当需要其实际值时,就引用Task对象的Result属性。届时,如果异步方法设置了该属性,调用方法就能获得该值并继续。否则,将暂停并等待该属性被设置,然后再继续执行。
await表达式
await表达式指定了一个异步执行的任务。
语法:由await关键字和一个空闲对象(称为任务)组成。
这个任务可能是一个Task类型的对象,也可能不是。默认情况下,这个任务在当前线程异步运行。
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
最简单的方式是在方法中使用Task.Run方法来创建一个Task。Task.Run是在不同的线程上运行方法的。
例:Task.Run方法接受一个Func
class MyClass
{
public int Get10() // 与Func<int>兼容
{
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; }); // Lambda表达式隐式转换为Func<int>委托
Console.WriteLine("{0} {1} {2}", a, b, c);
}
}
class Program
{
static void Main()
{
Task t = (new MyClass()).DoWorkAsync();
t.Wait(); // 10 10 10
}
}
上面的代码中,使用的Task.Run的签名以Func
可能用到的4个委托类型的签名:
例:使用Task.Run方法来运行4种不同的委托类型。
static class MyClass
{
public static async Task DoWorkAsync()
{
await Task.Run(() => Console.WriteLine(5.ToString())); // Action
Console.WriteLine((await Task.Run(() => 6)).ToString()); // TResult Func()
await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString()))); // Task Func()
int value = await Task.Run(() => Task.Run(() => 8)); // Task<TResult> Func()
Console.WriteLine(value.ToString());
}
}
class Program
{
static void Main()
{
Task t = MyClass.DoWorkAsync();
t.Wait(); // 5 6 7 8
}
}
任何可以使用表达式的地方,都可以使用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)); // TResult Func()
Console.WriteLine(value.ToString());
}
}
class Program
{
static void Main()
{
Task t = MyClass.DoWorkAsync();
t.Wait(); // 11
}
}