async 和 await 在 C# 5.0 就已经引入了,用来处理异步编程,但之前用的相对较少,现在在 dotNet Core 时代,已经使用的非常普遍,很多的开源组件中提供了大量的后缀为 Async (异步)的方法。本文就简单讲讲 async 和 await。
异步是相对于同步来说的,同步是指多个方法顺序执行,后一个会等待前一个执行完成后,才开始执行;异步是指调用一个方法 A ,调用后会立即返回(不用等方法 A 执行完成),接着调用后面的方法 B,举个例子:
在 Task 出来之前,使用的比较多的就是多线程,最经典的问题就是在 Winform 程序中为了能让界面中显示进度之类的动态内容时,需要创建一个新的线程来做,这样主 UI 线程才不会被堵塞卡死,例如下面的代码:
private void btnTest_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
}
private void DoWork()
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
this.Invoke(new Action<string>(this.ChangeLabel),i.ToString());
}
}
private void ChangeLabel(string i)
{
label1.Text = i + "/100";
}
上面代码中的 DoWork 方法的 Thread.Sleep(100), 真实情况可能是一个耗时操作,那么这个线程会处于阻塞状态,直到结果返回,会影响性能和造成资源浪费。
在 C# 5 中引入了 Task,一个任务对象,用来实现异步编程,Task 是基于线程池,线程池避免了启动和终止线程的开销,也避免了创建太多的线程,防止系统将大量的时间耗费在线程的切换上。主线程结束后,所有的 Task 任务也将结束。下面是使用 Task 实现和上面相同的功能。
private void btnTest_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 0; i < 1000; i++)
{
this.Invoke(new Action(() => {
label1.Text = i.ToString();
}));
Thread.Sleep(10);
}
});
}
通常我们会使用 Task.Run 来开始执行一个任务,在 Run 方法中传入一个委托,可以是 Action 或者 Func、一旦 Run 方法调用,委托代码会立即执行。
当有多个 Task 任务的时候,可以使用 Task.WaitAll 或 Task.WaitAny 等待一个或多个任务的完成,才让主线程继续。下面例子使用 WatiAll 进行等待:
static void Main(string[] args)
{
Console.WriteLine("start");
Task task1= Task.Run(() =>
{
Thread.Sleep(2000);
Console.WriteLine("task1");
});
Task task2=Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("task2");
});
Task.WaitAll(task1,task2);
Console.WriteLine($"task1 task2已经执行完成");
Task task3=Task.Run(() =>
{
Thread.Sleep(5000);
Console.WriteLine("task3");
});
Console.WriteLine("end");
Console.ReadLine();
}
执行结果如下:
当把上面代码中的 WaitAll 换成 WaitAny ,可以看出当 task1 执行完成后等待就结束了,调用结果如下:
async 和 await 是 C# 的语法糖,用来简化异步编程模型,首先来看下 async 和 await 的代码结构。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("start");
Test1Async();
Console.WriteLine("end");
Console.ReadLine();
}
static async void Test1Async()
{
await Task.Delay(3000);
Console.WriteLine("Test1");
}
}
上面的代码中在 Task.Delay(3000); 前面添加了 await 关键字,会发现最后的执行结果为:
说明添加 await 关键字之后会进行等待,就让会等待,就变成和同步一样了吗?答案当然不是:
static async Task Main(string[] args)
{
Stopwatch watch =Stopwatch.StartNew();
Console.WriteLine("start");
Task<string> task1= Test1Async();
Task<string> task2= Test2Async();
Console.WriteLine("end");
Console.WriteLine($"{task1.Result},{task2.Result}");
watch.Stop();
Console.WriteLine($"运行时间:{watch.ElapsedMilliseconds/1000} 秒");
Console.ReadLine();
}
static async Task<string> Test1Async()
{
await Task.Delay(3000);
return "test1";
}
static async Task<string> Test2Async()
{
await Task.Delay(3000);
return "test2";
}
}
运行结果如下:
使用 async 标记的异步方法可以有四种类型的返回值:
不推荐使用,有下面几个原因:
没有返回值的异步方法,我们应该返回 Task:
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("start");
Task task1= Test1Async();
Console.WriteLine($"是否完成:{task1.IsCompleted}");
task1.Wait();
Console.WriteLine($"是否完成:{task1.IsCompleted}");
Console.WriteLine("end");
Console.ReadLine();
}
static async Task Test1Async()
{
await Task.Delay(3000);
Console.WriteLine("test1");
}
}
执行结果:
当异步方法需要返回一个值,给后面的步骤使用的的时候,就使用 Task,在调用的时候使用 Result 属性进行值的获取。
ValueTask是在 C#7.1 中推出的一种类型,使用 ValueTask 比 Task 更高效,该类型是一个 struct ,为值类型,在栈上分分配,不像 Task 是个 class ,频繁地分配和回收会对 GC 造成很大的压力。正因为 ValueTask是个 struct ,在功能上要弱于 Task。如果你的异步方法可以根据早前缓存的结果直接返回相应的值,应该使用 ValueTask。