C#沉淀-异步编程 一
什么是异步
任务以固定的顺序被执行叫做同步,任务不按固定顺序执行则叫做异步
关于进程与线程
启动程序时,系统会在内存中创建一个新进程
进程是构成运行程序的资源的集合
这些资源包括虚地址空间、文件句柄和许多其他程序运行所需的东西
在进程内部,系统创建了一个称为线程的内核对象,它代表了真正执行的程序
Main方法是程序的入口,在这里,程序会开始线程的执行
要点:
- 默认情况下,一个进程只包含一个线程从程序的开始一直执行到程序的结束
- 线程可以派生其他线程,因此在任意时刻,一个进程都可以包含不同状态的多个线程,来执行程序的不同任务
- 如果一个进程拥有多个线程,它们将共享进程的资源
- 系统为处理器执行所规划的单元是线程,不是进程
示例一,不使用异步:
using System;
using System.Net;
using System.Diagnostics;
namespace CodeForAsync
{
class MyDownloadString
{
//Stopwatch类可以用来计算资源耗时
Stopwatch sw = new Stopwatch();
public void DoRun()
{
const int LargeNumber = 6000000;
sw.Start();//启动计时
//调用两次下载资源的方法,测算时间消耗
//下载百度资源
int t1 = CountCharacters(1, "http://baidu.com");
//下载搜狗资源
int t2 = CountCharacters(2, "https://pinyin.sogou.com/");
//调用四次循环方法,测算时间消耗
CountToALargeNumber(1, LargeNumber);
CountToALargeNumber(2, LargeNumber);
CountToALargeNumber(3, LargeNumber);
CountToALargeNumber(4, LargeNumber);
}
//下载网站资源
private int CountCharacters(int id, string uristring)
{
//实例化一个WebClient对象,用于网站交互
WebClient wc1 = new WebClient();
Console.WriteLine("下载{0}开始运行 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);//sw.Elapsed.TotalMilliseconds表示一个毫秒级的时间跨度值
//将请求资源下载为string格式
string result = wc1.DownloadString(new Uri(uristring));
Console.WriteLine("\t下载{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
return result.Length;
}
//循环指定次数,不做任何操作
private void CountToALargeNumber(int id, int value)
{
for (long i = 0; i < value; i++);
Console.WriteLine("循环{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
}
}
class Program
{
static void Main(string[] args)
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
Console.ReadKey();
}
}
}
运行结果:
下载1开始运行 - 时间节点:2.1404 ms
下载1运行结束 - 时间节点:413.6084 ms
下载2开始运行 - 时间节点:413.7037 ms
下载2运行结束 - 时间节点:927.1802 ms
循环1运行结束 - 时间节点:949.9849 ms
循环2运行结束 - 时间节点:973.1916 ms
循环3运行结束 - 时间节点:995.1234 ms
循环4运行结束 - 时间节点:1017.0343 ms
从上例可以看出,执行的顺序是:下载1开始-下载1结束》下载2开始-下载2结束》循环1》循环2》循环3》循环4;它们的执行是有序的,如果上一步的任务未执行完毕,下一个任务是无法开始的
这里的循环并未消耗多少时间,而从网站下载资源的两个任务都消耗了比较多的资源,也就是说,在程序运行期间,所有的任务都会被这两个下载任务拖慢进度
示例二:使用异步
using System;
using System.Net;
using System.Diagnostics;
using System.Threading.Tasks;
namespace CodeForAsync
{
class MyDownloadString
{
Stopwatch sw = new Stopwatch();
public void DoRun()
{
const int LargeNumber = 6000000;
sw.Start();
//下载百度资源
Task<int> t1 = CountCharactersAsync(1, "http://baidu.com");
//下载搜狗资源
Task<int> t2 = CountCharactersAsync(2, "https://pinyin.sogou.com/");
//这里的返回结果将保存为一个Task<int>对象
CountToALargeNumber(1, LargeNumber);
CountToALargeNumber(2, LargeNumber);
CountToALargeNumber(3, LargeNumber);
CountToALargeNumber(4, LargeNumber);
}
//下载网站资源
private async Task<int> CountCharactersAsync(int id, string uristring)
{
//async为异步关键字
//Task<int>表示一个可以返回值的异步操作
//实例化一个WebClient对象,用于网站交互
WebClient wc = new WebClient();
Console.WriteLine("下载{0}开始运行 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
//将请求资源下载为string格式
//await表示异步等待返回结果
string result = await wc.DownloadStringTaskAsync(new Uri(uristring));
Console.WriteLine("\t下载{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
return result.Length;
}
//循环指定次数,不做任何操作
private void CountToALargeNumber(int id, int value)
{
for (long i = 0; i < value; i++);
Console.WriteLine("循环{0}运行结束 - 时间节点:{1} ms", id, sw.Elapsed.TotalMilliseconds);
}
}
class Program
{
static void Main(string[] args)
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
Console.ReadKey();
}
}
}
运行结果:(多运行几次,结果可能不一样)
下载1开始运行 - 时间节点:3.7227 ms
下载2开始运行 - 时间节点:250.3289 ms
下载1运行结束 - 时间节点:271.565 ms
循环1运行结束 - 时间节点:297.6998 ms
循环2运行结束 - 时间节点:321.2623 ms
循环3运行结束 - 时间节点:344.1153 ms
循环4运行结束 - 时间节点:371.043 ms
下载2运行结束 - 时间节点:637.35 ms
结果分析:与第一个示例比较可以看出,异步的结果很不一样;在先后开启两个下载后,并没有等待下载完成,就直接开始了循环任务,也就是说在下载任务运行的同时,仍然可以操作别的任务;示例一所有任务完成后的时间节点是1017.0343 ms,而这里所有任务完成后的时间节点是637.35 ms,节省了将近一半的时间
接下来将详细讲解关于异步的知识
C# 5.0引入了一个用来构建异步方法的新特性——anync/await
,还有一些异步特性没有包含在C#中,而是放在了.NET框架里
async/await 特性的结构
异步的方法会在处理完成之前就返回到调用方法
async/await的特性:
- 调用方法(calling method):该方法调用异步方法,然后在异步方法(可能是相同的线程,也可能在不同的线程)执行其任务的时候继续执行
- 异步(async):该方法异步执行其工作,然后立即返回到调用方法
- await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过一个都不包含的话编译器会发出警告
异步语法
调用方法
Task<int> value = SomeClass.FuncAsync(1, 2); //这被称作是调用方法
异步方法
static class SomeClass
{
//被async标识的方法为一个异步方法
public static async Task<int> FuncAsync(int x, int y)
{
//异步方法内部至少有一个await表达式
int sum = await Task.Run(() => x + y);
return sum;
}
}
Task.Run()表式开启一个异步任务,参数是一个委托,这里使用了Lambda表达式
异步方法解析
异步方法在完成其工作的之前即返回到调用方法,然后在调用方法继续执行的时候完成其他工作
在语法上,异步方法具有如下特点:
- 方法头中包含async方法修饰符
- 包含一个或多个await表达式,表示可以异步完成的任务
- 必须具备以下三种返回类型(
Task
与Task<T>
所返回的对象表示将在未来完成的工作,调用方法和异步方法可以继续执行void
Task
Task<T>
- 异步方法的参数可以为任意类型任意数量,但不能out和ref参数
- 按照约定,异步方法的名称应该以Async为后缀
- 除了方法以外,Lambda表达式和匿名方法也可以作为异步对象
//方法拥有async关键字
//这里的返回类型为Task<T>类型
async Task<int> FuncAsync(int x, int y)
{
//await 表达式
int sum = await Task.Run(() => x + y);
//返回语句
return sum;
}
异步方法在方法头中必须包含async关键字,而且必须出现在返回类型之前
async是个修饰符,只表示该方法将包含一个或多个awai表达式,也就是说,async本身并不能创建异步操作
async是一个上下文关键字,也就是说除了用途方法修饰符之外,async还可用作标识符
Task<T>
:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>
调用方法通过读取Task的Result属性来获取这个T类型的值
任何返回Task<T>
类型的异步方法其返回值必须是T类型或可以隐式转换为T的类型
示例:
Task<int> value = SomeClass.FuncAsync(1, 2); //这被称作是调用方法
int re = value.Result; //获取T类型的值
Task
:如果调用方法不需要从调用中返回某个值 ,便需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。这时,即使异步方法中出现了return语句,也不会返回任何东西
示例:
Task someTask = SomeClass.FuncAsync(1, 2); //这被称作是调用方法
someTask.Wait();
void
: 如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时【这称为“调用并忘记”】,异步方法可以返回void类型,这时,即使异步方法中有return语句,也不会返回任何东西
异步就去的控制流
异步方法的结构包含三个不同的区域
- 第一部分为await表达式之前的代码
- 第二部分为awiat表达式
- 第三部分为await表达式之后的代码
如下图示例中,蓝色框为第一部分,绿色框为第二部分,黄色框为第三部分;因为先执行await表达式,再执行赋值语句,所以string result=
属于第三部分
通过这个图例分析得出,因为await会执行一个异步任务,所以,它之前的代码也就是第一部分代码最好是简短而耗时短的任务,以便以更快的速度执行await表达式
上图阐明了一个异步方法的控制流,它从第一个await表达式之前的代码开始,正常执行(同步地)直到遇见第一个await。这一区域实际上是在第一个await表达式处结束,此时await任务还没有完成(大多数情况正如此)。当await任务完成时,访求继续同步执行。如果还有其他await,就重复上述过程。
当达到await表达式时,异步方法将控制返回到调用方法。如果方法的返回类型为Task<T>
或Task
类型,将创建一个Task
对象,表示需异步完成的任务和后续,然后将该Task
返回到调用方法
亲自测试上面的流程:
using System;
using System.Net;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Threading;
namespace CodeForAsync
{
class Program
{
static int id = 0;
//定义一个异步方法,返回Task<int>类型对象
static async Task<int> fun()
{
//执行第一部分代码即await表达式之前
Console.WriteLine("步骤{0}异步方法内部:第1部分", ++id);
//执行第二部分代码
//当遇到await表达的时候,会返回到调用方法
int x = await Task.Run(() =>
{
Thread.Sleep(1000 * 3);//消耗3秒钟
Console.WriteLine("步骤{0}异步方法内部:第2部分", ++id);
return 0;
});
//执行第三部分代码
Console.WriteLine("步骤{0}异步方法内部:第3部分", ++id);
return x;
}
static void Main(string[] args)
{
Console.WriteLine("步骤{0}异步方法外,准备开始调用异步方法", ++id);
Task<int> y = fun();
Console.WriteLine("步骤{0}异步方法外,调用异步方法之后", ++id);
Console.WriteLine("步骤{0}访问异步方法的返回值:{1}", ++id, y.Result);
Console.ReadKey();
}
}
}
输出:
步骤1异步方法外,准备开始调用异步方法
步骤2异步方法内部:第1部分
步骤3异步方法外,调用异步方法之后
步骤5异步方法内部:第2部分
步骤6异步方法内部:第3部分
步骤4访问异步方法的返回值:0
解析:
- 步骤1是在调用异步方法外发生的,这个没有什么疑问
- 步骤2是在调用了异步方法后,执行了异步方法内的await表达式之前的代码
- 步骤3是在异步方法遇到await表达式后,因为需要耗时3秒,所以直接返回到了调用方法,也就是到了异步方法之外,继续执行异步方法之外的代码,同时异步方法awiat表达式所指定的代码也在执行
- 步骤5与步骤6是消耗完3秒后输出的内容,表示异步的内容也执行完了
-
重点,为什么步骤4会在最后输出呢?首先步骤4是异步方法外的代码,所以当异步方法内部遇到await返回调用方法,依次被执行的是步骤3和步骤4,步骤3很快被输出没有问题,但步骤4使用了异步方法的返回类型中的值,而这个值必须在3秒消耗完后才有,如果在异步方法执行期间没有得到返回值,调用
Task<int>
去访问int类型的值,便会一直等待下去,所以步骤4是在异步完成后才输出的
另一个需要注意的地方是,异步方法的返回值并不是一个Task<int>
类型的,而是一个int类型的值,这里将其进行了隐式的转换;如果返回类型是Task
类型,return并不会返回任何值,只是退出了异步方法
await表达式
await表达式指定了一个异步执行的任务
await 后面的是一个空闲对象(称为任务),这个任务可能是一个Task
类型的对象,也可能不是。默认情况下,这个任务在当前 线程异步运行
await task
一个空闲对象即是一个awaitable类型的实例
awaitable类型指的是包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象。awaiter类型包含以下成员 :
- bool IsCompleted {get;}
- void OnCompleted (Action);
- void GetResult();
- T GetResult();
对于awaitalbe类型,无须息构建,只要使用Task即可,它也是awaitable类型的
Taks.Run()可以创建一个Task,它会在不同的线程上运行你的方法
Taks.Run()的签名如下:
Task.Run(Func<TReturn> func)
因此可以将一个泛型委托传进去
不过,Task.Run()具有很多重载,这里不一一详解,我们举一反三即可
示例,使用4个Task.Run()重载:
using System;
using System.Net;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Threading;
namespace CodeForAsync
{
static class MyClass
{
public static async Task DowWorkAsync()
{
//public static Task Run(Action action);
await Task.Run(() => Console.WriteLine("5"));
//public static Task<TResult> Run<TResult>(Func<TResult> function);
Console.WriteLine((await Task.Run(() => "6")).ToString());
//public static Task Run(Func<Task> function);
await Task.Run(() => Task.Run(()=>Console.WriteLine("7")));
//public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
int value = await Task.Run(() => Task.Run(() => 8));
Console.WriteLine(value.ToString());
}
}
class Program
{
static void Main(string[] args)
{
Task t = MyClass.DowWorkAsync();
t.Wait();
Console.WriteLine("---");
Console.ReadKey();
}
}
}
输出:
5
6
7
8
---
取消一个异步操作
System.Threading.Tasks命名空间中有两个类是为此目的而设计的:CancellationToken和CancellationTokenSource
- CancellationToken对象包含一个任务是否被取消的信息
- 拥有CancellationToken对象的任务需要定期检查其令牌(token)状态。如果CancellationToken对象的IsCancellationRequested属性为true,任务需停止其操作并返回
- CancellationToken是不可逆的,并且只能使用一次。也就是说,一旦IsCancellationRequested属性被设置为true,就不能更改了
- CancellationTokenSource对象创建可分配给不同任务的CancellationToken对象。任何持有CancellationTokenSource的对象都可以调用其Cancel方法,这会将CancellationToken的IsCancellationRequested属性设置为true
示例:
using System;
using System.Net;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Threading;
namespace CodeForAsync
{
class MyClass
{
public async Task RunAsync(CancellationToken ct)
{
if (ct.IsCancellationRequested)
return;
await Task.Run(()=>CycleMethod(ct),ct);
}
void CycleMethod(CancellationToken ct)
{
Console.WriteLine("Starting CycleMethod");
const int max = 10;
for (int i = 0; i < max; i++)
{
if (ct.IsCancellationRequested)
return;
Thread.Sleep(1000);
Console.WriteLine(" {0} of {1} iterations completed", i+1, max);
}
}
}
class Program
{
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
MyClass mc = new MyClass();
Task t = mc.RunAsync(token);
Thread.Sleep(3000);
cts.Cancel();//3秒后将token的IsCancellationRequested改为true
t.Wait();
Console.WriteLine("Was Cancelled: {0}",token.IsCancellationRequested);
Console.ReadKey();
}
}
}
输出:
Starting CycleMethod
1 of 10 iterations completed
2 of 10 iterations completed
3 of 10 iterations completed
Was Cancelled: True