异步,多线程和并行的区别
首先, Asynchronized (异步)和 Synchronised (同步)是相对应的。
异步就好像去邮局发信一样,你希望把信发到A家,你把信交给邮局工作人员就可以回家了,不用等着A收到再回家,这个就是异步.
同步是,你给A打电话,如果打通了, 你一言我一语的就开始交流,这就是同步。
多线程是你同时给A,B,C…打电话。 多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码。多线程可以实现线程间的切换执行。
异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等这件事完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。 实现异步可以采用多线程技术或交给另外的进程来处理。
异步和多线程两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。
- 异步操作的优缺点
因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。
当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些出入,而且难以调试。 - 多线程的优缺点
多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。
但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。
使用场景:
当需要执行I/O操作时,使用异步操作比使用线程 + 同步I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。
而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。
异步
.NET 提供了执行异步操作的三种模式:
基于任务的异步模式 (TAP) ,该模式使用单一方法表示异步操作的开始和完成。 TAP 是在 .NET Framework 4 中引入的。 这是在 .NET 中进行异步编程的推荐方法。 C# 中的 async 和 await 关键词为 TAP 添加了语言支持。
基于事件的异步模式 (EAP),是提供异步行为的基于事件的旧模型。 这种模式需要后缀为 Async 的方法,以及一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。 EAP 是在 .NET Framework 2.0 中引入的。 建议新开发中不再使用这种模式。
异步编程模型 (APM) 模式(也称为 IAsyncResult 模式),这是使用 IAsyncResult 接口提供异步行为的旧模型。 在这种模式下,同步操作需要 Begin 和 End 方法(例如,BeginWrite 和 EndWrite以实现异步写入操作)。 不建议新的开发使用此模式。
TAP
基于任务的异步模式 Task-based asynchronous pattern (TAP)使用了 System.Threading.Tasks.Task 命名空间。
使用回调或事件来实现异步编程时,编写的代码不直观, APM 需要 Begin 和 End 方法。 EAP 需要后缀为 Async 的方法,以及一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。这样很容易把代码搞得一团糟。TAP使用单个方法表示异步操作的开始和完成。 这与异步编程模型(APM 或 IAsyncResult)模式和基于事件的异步模式 (EAP) 形成对比。它让编写异步代码变得容易和优雅。通过使用async/await关键字,可以像写同步代码那样编写异步代码,所有的回调和事件处理都交给编译器和运行时帮你处理了。
TAP 方法返回 System.Threading.Tasks.Task 或 System.Threading.Tasks.Task<TResult>,具体取决于相应同步方法返回的是 void 还是类型 TResult。
TAP 方法的参数中不能添加 out 或 ref 参数,需要返回的所有数据应该由 TResult返回。
一个典型的TAP 函数包括以下元素:

async是一个专门给编译器的提示,意思是该函数的实现可能会出现await。
1
2
3
4
5
6
7
8
9
10
Task<int> DelayAndCalculate1(int a, int b)
{
return Task.Delay(1000).ContinueWith(t => a + b);
}
async Task<int> DelayAndCalculate2(int a, int b)
{
await Task.Delay(1000);
return a + b;
}
这两个函数(不算函数名的不同),在函数声明上是完全没有区别的。只是其中一个在实现中使用了await,所以C#语法要求我们必须在标示async。
另外interface的定义中不能写async,因为如上所述,async不是函数声明,而其实编译函数实现的提示。
其实真正重要的是await(和其他异步的实现,如例子中的DelayAndCalculate1),有没有async反而确实不重要。
而await是一个标记,它告诉编译器生成一个等待器来等待可等待类型实例的运行结果。一个await对应一个等待器 ,任务的等待器类型是TaskAwaiter/TaskAwaiter<TResult>。
await task等效于task.GetAwaiter().GetResult()。
task.GetAwaiter() 返回TaskAwaiter/TaskAwaiter
1
2
3
4
5
6
7
8
async Task<int> ComplexWorkFlow()
{
Task<int> task1 = DoTask1();
Task<int> task2 = DoTask2();
Task<int> task3 = DoTask3UseResultOfTask1(await task1);
Task<int> task4 = DoTask4UseResultOfTask2(await task2);
return await DoTask5(await task3, await task4);
}
task1和task2可以并行执行,task3和task4可以并行执行(事实上更好的写法可以让task1->task3完全并行与task2->task4)。核心思路就是只有当某个task的执行结果需要被使用的时候才解开这个task的值(等它执行完毕)。
异步方法:在执行完成前立即返回调用方法,在调用方法继续执行的过程中完成任务。
语法分析:
(1)关键字: 方法头使用 async 修饰。
(2)要求: 包含 N(N>0) 个 await 表达式(不存在 await 表达式的话 IDE 会发出警告),表示需要异步执行的任务。没有的话,就和普通方法一样执行了。
(3)返回类型: 只能返回 3 种类型(void、Task 和 Task<T>)。Task 和 Task<T> 标识返回的对象会在将来完成工作,表示调用方法和异步方法可以继续执行。
(4)参数: 数量不限。但不能使用 out 和 ref 关键字。
(5)命名约定: 方法后缀名应以 Async 结尾。
(6)其它: 匿名方法和 Lambda 表达式也可以作为异步对象;async 是一个上下文关键字;关键字 async 必须在返回类型前。
关于 async 关键字:
它只是标识该方法包含一个或多个 await 表达式,即,它本身不创建异步操作。
结构
异步方法的结构可拆分成三个不同的区域:
(1)表达式之前的部分:从方法头到第一个 await 表达式之间的所有代码。
(2)await 表达式:将被异步执行的代码。
(3)表达式之后的部分:await 表达式的后续部分。
【难点】
①第一次遇到 await 所返回对象的类型。这个返回类型就是同步方法头的返回类型,跟 await 表达式的返回值没有关系。
②到达异步方法的末尾或遇到 return 语句,它并没有真正的返回一个值,而是退出了该方法。
await表达式
在 C# 5.0 中出现的 async 和 await ,让异步编程变得更简单,我们可以像写同步代码一样去写异步代码。
await 表达式指定了一个异步执行的任务。默认情况,该任务在当前线程异步执行。
每一个任务就是一个 awaitable 类的实例。awaitable 类型指包含 GetAwaiter() 方法的类型。
实际上,你并不需要构建自己的 awaitable,一般只需要使用 Task 类,它就是 awaitable。
最简单的方式是在方法中使用 Task.Run() 来创建一个 Task。【注意】它是在不同的线程上执行方法。
Task.Run() 支持 4 中不同的委托类型所表示的方法:Action、Func
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
internal class Program
{
private static void Main(string[] args)
{
var t = Do.GetGuidAsync();
Console.WriteLine("GetGuidAsync is called");
Console.Read();
}
}
public class Do
{
private static Guid GetGuid()
{
return Guid.NewGuid();
}
public static async Task GetGuidAsync()
{
var myFunc = new Func<Guid>(GetGuid);
var t1 = await Task.Run(myFunc);
var t2 = await Task.Run(new Func<Guid>(GetGuid));
var t3 = await Task.Run(() => GetGuid());
var t4 = await Task.Run(() => Guid.NewGuid());
Console.WriteLine($"t1: {t1}");
Console.WriteLine($"t2: {t2}");
Console.WriteLine($"t3: {t3}");
Console.WriteLine($"t4: {t4}");
}
}
返回值类型
Task<T>
调用方法要从调用中获取一个 T 类型的值,异步方法的返回类型就必须是Task<T>。调用方法从 Task 的 Result 属性获取的就是 T 类型的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static void Main(string[] args)
{
Task<int> t = Calculator.AddAsync(1, 2);
//一直在干活
Console.WriteLine("AddAsync is called"); // 这行会立即输出
Console.WriteLine($"result: {t.Result}"); // 等待task执行结束后才会输出
Console.Read();
}
public class Calculator
{
private static int Add(int n, int m)
{
return n + m;
}
public static async Task<int> AddAsync(int n, int m)
{
await Task.Delay(1000);
int val = await Task.Run(() => Add(n, m));
return val;
}
}
Task
调用方法不需要从异步方法中取返回值,但是希望检查异步方法的状态,那么可以选择可以返回 Task 类型的对象。不过,就算异步方法中包含 return 语句,也不会返回任何东西。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void Main(string[] args)
{
Task t = Calculator.AddAsync(1, 2);
//一直在干活
t.Wait();
Console.WriteLine("AddAsync 方法执行完成");
Console.Read();
}
public class Calculator
{
private static int Add(int n, int m)
{
return n + m;
}
public static async Task AddAsync(int n, int m)
{
await Task.Delay(1000);
int val = await Task.Run(() => Add(n, m));
Console.WriteLine($"Result: {val}");
}
}
void
调用方法执行异步方法,但又不需要做进一步的交互。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void Main(string[] args)
{
Calculator.AddAsync(1, 2);
//一直在干活
Console.Read();
}
internal class Calculator
{
private static int Add(int n, int m)
{
return n + m;
}
public static async void AddAsync(int n, int m)
{
await Task.Delay(1000);
int val = await Task.Run(() => Add(n, m));
Console.WriteLine($"Result: {val}");
}
}
取消
CancellationToken 对象包含任务是否被取消的信息;如果该对象的属性 IsCancellationRequested 为 true,任务需停止操作并返回;该对象操作是不可逆的,且只能使用(修改)一次,即该对象内的 IsCancellationRequested 属性被设置后,就不能改动。
【注意】调用 CancellationTokenSource 对象的 Cancel 方法,并不会执行取消操作,而是会将该对象的 CancellationToken 属性 IsCancellationRequested 设置为 true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Program
{
static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
var t = Do2.ExecuteAsync(token);
Console.WriteLine("Do2.ExecuteAsync is called");
Thread.Sleep(3000); //挂起 3 秒
source.Cancel(); //传达取消请求
Console.WriteLine("task is cancelled");
Console.WriteLine($"{nameof(token.IsCancellationRequested)}: {token.IsCancellationRequested}");
Console.Read();
}
}
internal class Do2
{
public static async Task ExecuteAsync(CancellationToken token)
{
if (token.IsCancellationRequested)
{
return;
}
await Task.Run(() => CircleOutput(token), token);
}
private static void CircleOutput(CancellationToken token)
{
Console.WriteLine($"{nameof(CircleOutput)} is called:");
const int num = 5;
for (var i = 0; i < num; i++)
{
if (token.IsCancellationRequested) //监控 CancellationToken
{
return;
}
Console.WriteLine($"{i + 1}/{num} complete");
Thread.Sleep(1000);
}
}
}
任务等待
调用方法可能在某个时间点上需要等待某个特殊的 Task 对象完成,才执行后面的代码。此时,可以采用实例方法 Wait 。
1
2
3
4
5
6
7
8
9
private static void Main(string[] args)
{
var t = CountCharactersAsync();
t.Wait(); //等待任务结束
Console.WriteLine($"Result is {t.Result}");
Console.Read();
}
WaitAll, WaitAny
Wait() 适合用于单一 Task 对象,如果想操作一组对象,可采用 Task 的两个静态方法 WaitAll() 和 WaitAny() 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void Main(string[] args)
{
var t1 = GetRandomAsync(1);
var t2 = GetRandomAsync(2);
Task<int>[] tasks = new Task<int>[] { t1, t2 };
Task.WaitAll(tasks); //等待任务全部完成,才继续执行
//Task.WaitAny(tasks); //等待任一 Task 完成,才继续执行
//IsCompleted 任务完成标识
Console.WriteLine($"t1.{nameof(t1.IsCompleted)}: {t1.IsCompleted}");
Console.WriteLine($"t2.{nameof(t2.IsCompleted)}: {t2.IsCompleted}");
Console.Read();
}
WhenAll, WhenAny
上节说的是如何使用 WaitAll() 和 WaitAny() 同步地等待 Task 完成。这次我们使用 Task.WhenAll() 和 Task.WhenAny() 在异步方法中异步等待任务。
WhenAll() 异步等待集合内的 Task 都完成,不会占用主线程的时间。
Task.Delay
Task.Delay() 方法会创建一个 Task 对象,该对象将暂停其在线程中的处理,并在一定时间之后完成。和 Thread.Sleep 不同的是,它不会阻塞线程,意味着线程可以继续处理其它工作。
在 WinForm 程序中执行异步操作
下面通过窗体示例演示以下操作-点击按钮后:
- 将按钮禁用,并将标签内容改成:“Doing”(表示执行中);
- 线程挂起3秒(模拟耗时操作);
- 启用按钮,将标签内容改为:“Complete”(表示执行完成)。
代码如下:
private void buttonDo_Click(object sender, EventArgs e)
{
buttonDo.Enabled = false;
lblText.Text = “Doing”;
1
2
3
4
5
Thread.Sleep(2000);
buttonDo.Enabled = true;
lblText.Text = "Complete";
}
然而在实际测试中我们发现form hang住了,没有显示Doing,而直接跳到了Complete。
GUI 程序在设计中要求所有的显示变化都必须在主 GUI 线程中完成,如点击事件和移动窗体。Windows 程序时通过 消息来实现,消息放入消息泵管理的消息队列中。点击按钮时,按钮的Click消息放入消息队列。消息泵从队列中移除该消息,并开始处理点击事件的代码,即 buttonDo_Click 事件的代码。
buttonDo_Click 事件会将触发行为的消息放入队列,但在 btnDo_Click 时间处理程序完全退出前(线程挂起 3 秒退出前),消息都无法执行。(3 秒后)接着所有行为都发生了,但速度太快肉眼无法分辨才没有发现标签改成“Doing”。
加入 async/await 特性:
1
2
3
4
5
6
7
8
9
10
private async void buttonDo_Click(object sender, EventArgs e)
{
buttonDo.Enabled = false;
lblText.Text = "Doing";
await Task.Delay(2000);
buttonDo.Enabled = true;
lblText.Text = "Complete";
}
buttonDo_Click 事件处理程序先将前两条消息压入队列,然后将自己从处理器移出,在3秒后(等待空闲任务完成后 Task.Delay )再将自己压入队列。这样可以保持响应,并保证所有的消息可以在线程挂起的时间内被处理。
BackgroundWorker
与 async/await 不同的是,你有时候可能需要一个额外的线程,在后台持续完成某项任务,并不时与主线程通信,这时就需要用到 BackgroundWorker 类。主要用于 GUI 程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public partial class Form1 : Form
{
private readonly BackgroundWorker _worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
//设置 BackgroundWorker 属性
_worker.WorkerReportsProgress = true; //能否报告进度更新
_worker.WorkerSupportsCancellation = true; //是否支持异步取消
//连接 BackgroundWorker 对象的处理程序
_worker.DoWork += _worker_DoWork;
_worker.ProgressChanged += _worker_ProgressChanged;
_worker.RunWorkerCompleted += _worker_RunWorkerCompleted;
}
private void _worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show(e.Cancelled ? $@"进程已被取消:{progressBar.Value}%" : $@"进程执行完成:{progressBar.Value}%");
progressBar.Value = 0;
}
private void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar.Value = e.ProgressPercentage; //异步任务的进度百分比
}
private static void _worker_DoWork(object sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
if (worker == null)
{
return;
}
for (var i = 0; i < 10; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
worker.ReportProgress((i + 1) * 10); //触发 BackgroundWorker.ProgressChanged 事件
Thread.Sleep(250); //线程挂起 250 毫秒
}
}
private async void buttonDo_Click(object sender, EventArgs e)
{
if(!_worker.IsBusy)
{
_worker.RunWorkerAsync();
}
}
}
Reference
走进异步编程的世界 - 剖析异步方法(上)
走进异步编程的世界 - 剖析异步方法(下)
走进异步编程的世界 - 在 GUI 中执行异步操作
基于任务的异步模式
Asynchronous Programming in .NET – Task-based Asynchronous Pattern (TAP)
Introduction to Task-Based Asynchronous Pattern in C# 4.5: Part I
c#中为什么async方法里必须还要有await?
C# TAP 异步编程 二 、await运算符已经可等待类型Awaitable
C# Async / Await - Make your app more responsive and faster with asynchronous programming
C# Advanced Async - Getting progress reports, cancelling tasks, and more
C# 之 Task、async和 await 、Thread 基础使用的Task的简单整理
目录