前言
C#异步编程有几种实现方式,异步方法就是其中的一种。异步方法是 C#5.0 才有的新特性,主要采用 async、await 关键字声明为异步方法,完成对方法的异步调用。C#5.0 对应的 VS 版本是 VS2012,对应的 .NET Framework 版本是 v4.5,所以需要在此基础上才支持。(否则可能报:找不到“async”修饰符所需的所有类型。目标框架版本是否不正确,或者缺少对程序集的引用?)
什么是异步方法
- 异步方法,是指在执行当前方法的同时,可以异步的去调用其他方法(异步方法),并且不会阻塞当前方法的线程。
- 使用了 async 修饰符的方法称为异步方法,通常配合 await 运算符和 Task 异步任务一起使用。
- 如果方法使用了 async 修饰符,则方法中需要包含一个以上 await 运算符,否则将以同步执行。
- 反之,如果方法中包含一个以上 await 运算符,则必须声明为一个异步方法,即使用 async 修饰符。
- Task 分为两种:
- Task,表示可以执行一个异步操作,声明如下:
public class Task : IAsyncResult, IDisposable { }
2) Task<TResult>,表示可以执行带有返回值的异步操作,声明如下:
public class Task<TResult> : Task { }
- 异步方法的返回类型必须为 void、Task、Task<TResult> 中的其中一种。
- void,表示无返回值,不关心异步方法执行后的结果,一般用于仅仅执行某一项任务,但是不关心结果的场景。
- Task,表示异步方法将返回一个 Task 对象,该对象通常用于判断异步任务是否已经完成,可以使用 taskObj.Wait() 方法等待,或者 taskObj.IsCompleted 判断。
- Task<TResult>,表示异步方法将返回一个 Task<TResult> 对象,该对象的 Result 属性则是异步方法的执行结果,调用该属性时将阻塞当前线程(异步方法未执行完成时)。
归纳一下:void 不关心结果;Task 只关心是否执行完成;Task<TResult> 不止关心是否执行完成,还要获取执行结果。
下面通过几个生活中比较形象的例子来理解异步方法的使用
-
模拟扔垃圾(不关心结果,返回 void 类型)
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 1/// <summary>
2/// 扔垃圾
3/// </summary>
4public void DropLitter()
5{
6 Console.WriteLine("老婆开始打扫房间,线程Id为:{0}", GetThreadId());
7 Console.WriteLine("垃圾满了,快去扔垃圾");
8 CommandDropLitter();
9 Console.WriteLine("不管他继续打扫,线程Id为:{0}", GetThreadId());
10 Thread.Sleep(100);
11 Console.WriteLine("老婆把房间打扫好了,线程Id为:{0}", GetThreadId());
12}
13
14/// <summary>
15/// 通知我去扔垃圾
16/// </summary>
17public async void CommandDropLitter()
18{
19 Console.WriteLine("这时我准备去扔垃圾,线程Id为:{0}", GetThreadId());
20 await Task.Run(() =>
21 {
22 Console.WriteLine("屁颠屁颠的去扔垃圾,线程Id为:{0}", GetThreadId());
23 Thread.Sleep(1000);
24 });
25 Console.WriteLine("垃圾扔了还有啥吩咐,线程Id为:{0}", GetThreadId());
26}
27
运行以上代码:
以上代码在 CommandDropLitter() 方法上加了 async 修饰符,并且使用 await 运算符开启了一个新的 Task 去执行另一个任务。注意:当前线程遇到 await 时,则立刻跳回调用方法继续往下执行。而 Task 执行完成之后将执行 await 之后的代码,并且与 await 之前的线程不是同一个。
2.模拟打开电源开关(关心是否执行完成,返回 Task 类型)
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 1/// <summary>
2/// 打开电源开关
3/// </summary>
4public void OpenMainsSwitch()
5{
6 Console.WriteLine("我和老婆正在看电视,线程Id为:{0}", GetThreadId());
7 Console.WriteLine("突然停电了,快去看下是不是跳闸了");
8 Task task = CommandOpenMainsSwitch();
9 Console.WriteLine("没电了先玩会儿手机吧,线程Id为:{0}", GetThreadId());
10 Thread.Sleep(100);
11 Console.WriteLine("手机也没电了只等电源打开,线程Id为:{0}", GetThreadId());
12
13 //task.Wait(); //所以这里将被阻塞,直到任务完成
14 //或者
15 while (!task.IsCompleted) { Thread.Sleep(100); }
16
17 Console.WriteLine("又有电了我们继续看电视,线程Id为:{0}", GetThreadId());
18}
19
20/// <summary>
21/// 通知我去打开电源开关
22/// </summary>
23public async Task CommandOpenMainsSwitch()
24{
25 Console.WriteLine("这时我准备去打开电源开关,线程Id为:{0}", GetThreadId());
26 await Task.Run(() =>
27 {
28 Console.WriteLine("屁颠屁颠的去打开电源开关,线程Id为:{0}", GetThreadId());
29 Thread.Sleep(1000);
30 });
31
32 Console.WriteLine("电源开关打开了,线程Id为:{0}", GetThreadId());
33}
34
运行以上代码:
- 可见,调用 Wait() 方法后,当前线程被阻塞了,直到 Task 执行完成后,当前线程才继续执行。
- 注意:由于 CommandOpenMainsSwitch() 是一个异步方法,虽然返回类型为 Task 类型,但是在我们代码中并没有写(也不能写) return task 语句,这是为什么呢?可能是这种返回类型比较特殊,或者编译器自动帮我们完成了吧!就算写也只能写 return 语句,后面不能跟对象表达式。
-
模拟去买盐(不止关心是否执行完成,还要获取执行结果。返回 Task<TResult> 类型)
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 1/// <summary>
2/// 做饭
3/// </summary>
4public void CookDinner()
5{
6 Console.WriteLine("老婆开始做饭,线程Id为:{0}", GetThreadId());
7 Console.WriteLine("哎呀,没盐了");
8 Task<string> task = CommandBuySalt();
9 Console.WriteLine("不管他继续炒菜,线程Id为:{0}", GetThreadId());
10 Thread.Sleep(100);
11 string result = task.Result; //必须要用盐了,等我把盐回来(停止炒菜(阻塞线程))
12 Console.WriteLine("用了盐炒的菜就是好吃【{0}】,线程Id为:{1}", result, GetThreadId());
13 Console.WriteLine("老婆把饭做好了,线程Id为:{0}", GetThreadId());
14}
15
16/// <summary>
17/// 通知我去买盐
18/// </summary>
19public async Task<string> CommandBuySalt()
20{
21 Console.WriteLine("这时我准备去买盐了,线程Id为:{0}", GetThreadId());
22
23 string result = await Task.Run(() =>
24 {
25 Console.WriteLine("屁颠屁颠的去买盐,线程Id为:{0}", GetThreadId());
26 Thread.Sleep(1000);
27 return "盐买回来了,顺便我还买了一包烟";
28
29 });
30
31 Console.WriteLine("{0},线程Id为:{1}", result, GetThreadId());
32
33 return result;
34}
35
运行以上代码:
- 以上代码 task.Result 会阻塞当前线程,与 task.Wait() 类似。
- 注意:与前面返回类型为 Task 的 CommandOpenMainsSwitch() 方法一样,虽然 CommandBuySalt() 方法返回类型为 Task<string>,但是我们的返回语句是 return 字符串。
其他示例
-
在前面(模拟去买盐)的示例中,异步方法中只开启了一个 Task,如果开启多个 Task 又是什么情况,看代码:
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 1public void AsyncTest()
2{
3 Console.WriteLine("AsyncTest() 方法开始执行,线程Id为:{0}", GetThreadId());
4 Task task = Test1();
5 Console.WriteLine("AsyncTest() 方法继续执行,线程Id为:{0}", GetThreadId());
6 task.Wait();
7 Console.WriteLine("AsyncTest() 方法结束执行,线程Id为:{0}", GetThreadId());
8}
9
10public async Task Test1()
11{
12 Console.WriteLine("Test1() 方法开始执行,线程Id为:{0}", GetThreadId());
13 await Task.Factory.StartNew((state) =>
14 {
15 Console.WriteLine("Test1() 方法中的 {0} 开始执行,线程Id为:{1}", state, GetThreadId());
16 Thread.Sleep(1000);
17 Console.WriteLine("Test1() 方法中的 {0} 结束执行,线程Id为:{1}", state, GetThreadId());
18 }, "task1");
19
20 await Task.Factory.StartNew((state) =>
21 {
22 Console.WriteLine("Test1() 方法中的 {0} 开始执行,线程Id为:{1}", state, GetThreadId());
23 Thread.Sleep(3000);
24 Console.WriteLine("Test1() 方法中的 {0} 结束执行,线程Id为:{1}", state, GetThreadId());
25 }, "task2");
26
27 Console.WriteLine("Test1() 方法结束执行,线程Id为:{0}", GetThreadId());
28}
29
运行以上代码:
当异步方法中有多个 await 时,会依次执行所有的 Task,只有当所有 Task 执行完成后才表示异步方法执行完成,当前线程才得以执行。
-
同样以前面(模拟去买盐)的示例,如果发现其实家里还有盐,这是就要告诉我不用买了(取消异步操作),怎么实现?这就要借助 System.Threading.CancellationTokenSource 和 System.Threading.Tasks.CancellationToken 对象来完成。
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 1/// <summary>
2/// 做饭(买盐任务取消)
3/// </summary>
4public void CookDinner_CancelBuySalt()
5{
6 Console.WriteLine("老婆开始做饭,线程Id为:{0}", GetThreadId());
7 Console.WriteLine("哎呀,没盐了");
8 CancellationTokenSource source = new CancellationTokenSource();
9 Task<string> task = CommandBuySalt_CancelBuySalt(source.Token);
10 Console.WriteLine("不管他继续炒菜,线程Id为:{0}", GetThreadId());
11 Thread.Sleep(100);
12
13 string result = "家里的盐";
14 if (!string.IsNullOrEmpty(result))
15 {
16 source.Cancel(); //传达取消请求
17 Console.WriteLine("家里还有盐不用买啦,线程Id为:{0}", GetThreadId());
18 }
19 else
20 {
21 //如果已取消就不能再获得结果了(否则将抛出 System.Threading.Tasks.TaskCanceledException 异常)
22 //你都叫我不要买了,我拿什么给你?
23 result = task.Result;
24 }
25
26 Console.WriteLine("既然有盐我就继续炒菜【{0}】,线程Id为:{1}", result, GetThreadId());
27 Console.WriteLine("老婆把饭做好了,线程Id为:{0}", GetThreadId());
28 Console.WriteLine("最终的任务状态是:{0},已完成:{1},已取消:{2},已失败:{3}",
29 task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
30}
31
32/// <summary>
33/// 通知我去买盐(又告诉我不用买了)
34/// </summary>
35public async Task<string> CommandBuySalt_CancelBuySalt(CancellationToken token)
36{
37 Console.WriteLine("这时我准备去买盐了,线程Id为:{0}", GetThreadId());
38
39 //已开始执行的任务不能被取消
40 string result = await Task.Run(() =>
41 {
42 Console.WriteLine("屁颠屁颠的去买盐,线程Id为:{0}", GetThreadId());
43 Thread.Sleep(1000);
44 }, token).ContinueWith((t) => //若没有取消就继续执行
45 {
46 Console.WriteLine("盐已经买好了,线程Id为:{0}", GetThreadId());
47 Thread.Sleep(1000);
48
49 return "盐买回来了,顺便我还买了一包烟";
50 }, token);
51
52 Console.WriteLine("{0},线程Id为:{1}", result, GetThreadId());
53
54 return result;
55}
56
运行以上代码:
-
刚开始我以为调用 source.Cancel() 方法后会立即取消 Task 的执行,仔细一想也不太可能。如果需要在 Task 执行前或者执行期间完成取消操作,我们自己写代码判断 cancellationToken.IsCancellationRequested 属性是否为 true(该属性在调用 source.Cancel() 后或者 source.CancelAfter() 方法到达指定时间后为 true),如果为 true 结束执行即可。
-
这里所说的“传达取消请求”的意思是,每个 Task 在执行之前都会检查 cancellationToken.IsCancellationRequested 属性是否为 true,如果为 true 则不执行 Task,并将设置 Status、IsCompleted、IsCanceled 等。
-
所以,在 Task 的源码中有这样一段代码
1
2
3
4
5
6 1if (cancellationToken.IsCancellationRequested)
2{
3 // Fast path for an already-canceled cancellationToken
4 this.InternalCancel(false);
5}
6
-
乘热打铁,我们再来看看多个 CancellationTokenSource 取消异步任务,以及注册取消后的回调委托方法,继续以(模拟去买盐)为例:
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 1/// <summary>
2/// 做饭(多个消息传达买盐任务取消)
3/// </summary>
4public void CookDinner_MultiCancelBuySalt()
5{
6 Console.WriteLine("老婆开始做饭,线程Id为:{0}", GetThreadId());
7 Console.WriteLine("哎呀,没盐了");
8 CancellationTokenSource source1 = new CancellationTokenSource(); //因为存在而取消
9 CancellationTokenSource source2 = new CancellationTokenSource(); //因为放弃而取消
10
11 CancellationTokenSource source = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);
12
13 //注册取消时的回调委托
14 source1.Token.Register(() =>
15 {
16 Console.WriteLine("这是因为{0}所以取消,线程Id为:{1}", "家里还有盐", GetThreadId());
17 });
18
19 source2.Token.Register((state) =>
20 {
21 Console.WriteLine("这是因为{0}所以取消,线程Id为:{1}", state, GetThreadId());
22 }, "不做了出去吃");
23
24 source.Token.Register((state) =>
25 {
26 Console.WriteLine("这是因为{0}所以取消,线程Id为:{1}", state, GetThreadId());
27 }, "没理由");
28
29 //这里必须传递 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 对象
30 Task<string> task = CommandBuySalt_MultiCancelBuySalt(source.Token);
31
32 Console.WriteLine("等等,好像不用买了,线程Id为:{0}", GetThreadId());
33 Thread.Sleep(100);
34
35 string[] results = new string[] { "家里的盐", "不做了出去吃", "没理由" };
36 Random r = new Random();
37 switch (r.Next(1, 4))
38 {
39 case 1:
40 source1.Cancel(); //传达取消请求(家里有盐)
41 //source1.CancelAfter(3000); //3s后才调用取消的回调方法
42 Console.WriteLine("既然有盐我就继续炒菜【{0}】,线程Id为:{1}", results[0], GetThreadId());
43 break;
44 case 2:
45 source2.Cancel(); //传达取消请求(不做了出去吃)
46 //source2.CancelAfter(3000); //3s后才调用取消的回调方法
47 Console.WriteLine("我们出去吃不用买啦【{0}】,线程Id为:{1}", results[1], GetThreadId());
48 break;
49 case 3:
50 source.Cancel(); //传达取消请求(没理由)
51 //source.CancelAfter(3000); //3s后才调用取消的回调方法
52 Console.WriteLine("没理由就是不用买啦【{0}】,线程Id为:{1}", results[2], GetThreadId());
53 break;
54 }
55
56 Console.WriteLine("最终的任务状态是:{0},已完成:{1},已取消:{2},已失败:{3}",
57 task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
58}
59
60/// <summary>
61/// 通知我去买盐(又告诉我不用买了,各种理由)
62/// </summary>
63public async Task<string> CommandBuySalt_MultiCancelBuySalt(CancellationToken token)
64{
65 Console.WriteLine("这时我准备去买盐了,线程Id为:{0}", GetThreadId());
66
67 //已开始执行的任务不能被取消
68 string result = await Task.Run(() =>
69 {
70 Console.WriteLine("屁颠屁颠的去买盐,线程Id为:{0}", GetThreadId());
71 Thread.Sleep(1000);
72 }, token).ContinueWith((t) => //若没有取消就继续执行
73 {
74 Console.WriteLine("盐已经买好了,线程Id为:{0}", GetThreadId());
75 Thread.Sleep(1000);
76 return "盐买回来了,顺便我还买了一包烟";
77 }, token);
78
79 Console.WriteLine("{0},线程Id为:{1}", result, GetThreadId());
80
81 return result;
82}
83
运行以上代码:
- 当调用 source.Cancel() 方法后,会立即取消并调用 token 注册的回调方法;而调用 existSource.CancelAfter() 方法则会等到达指定的毫秒数后才会取消。
- 注意:传递给异步方法的 token 对象,必须是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 对象,否则取消将无效。
- 回调的委托方法始终只有两个,一个是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 对象的注册委托,另一个是调用 Cancel()/CancelAfter() 方法的 Token 对象的注册委托。
- 如果以上代码调用的是 CancelAfter(3000) 方法,运行结果如下:
原文链接:https://www.cnblogs.com/abeam/p/11227935.html
前言
C#异步编程有几种实现方式,异步方法就是其中的一种。异步方法是 C#5.0 才有的新特性,主要采用 async、await 关键字声明为异步方法,完成对方法的异步调用。C#5.0 对应的 VS 版本是 VS2012,对应的 .NET Framework 版本是 v4.5,所以需要在此基础上才支持。(否则可能报:找不到“async”修饰符所需的所有类型。目标框架版本是否不正确,或者缺少对程序集的引用?)
什么是异步方法
- 异步方法,是指在执行当前方法的同时,可以异步的去调用其他方法(异步方法),并且不会阻塞当前方法的线程。
- 使用了 async 修饰符的方法称为异步方法,通常配合 await 运算符和 Task 异步任务一起使用。
- 如果方法使用了 async 修饰符,则方法中需要包含一个以上 await 运算符,否则将以同步执行。
- 反之,如果方法中包含一个以上 await 运算符,则必须声明为一个异步方法,即使用 async 修饰符。
- Task 分为两种:
- Task,表示可以执行一个异步操作,声明如下:
public class Task : IAsyncResult, IDisposable { }
2) Task<TResult>,表示可以执行带有返回值的异步操作,声明如下:
public class Task<TResult> : Task { }
- 异步方法的返回类型必须为 void、Task、Task<TResult> 中的其中一种。
- void,表示无返回值,不关心异步方法执行后的结果,一般用于仅仅执行某一项任务,但是不关心结果的场景。
- Task,表示异步方法将返回一个 Task 对象,该对象通常用于判断异步任务是否已经完成,可以使用 taskObj.Wait() 方法等待,或者 taskObj.IsCompleted 判断。
- Task<TResult>,表示异步方法将返回一个 Task<TResult> 对象,该对象的 Result 属性则是异步方法的执行结果,调用该属性时将阻塞当前线程(异步方法未执行完成时)。
归纳一下:void 不关心结果;Task 只关心是否执行完成;Task<TResult> 不止关心是否执行完成,还要获取执行结果。
下面通过几个生活中比较形象的例子来理解异步方法的使用
-
模拟扔垃圾(不关心结果,返回 void 类型)
///
<summary>
/// 扔垃圾
///
</summary>
public
void
DropLitter()
{
Console.WriteLine(
"老婆开始打扫房间,线程Id为:{0}", GetThreadId());
Console.WriteLine(
"垃圾满了,快去扔垃圾");
CommandDropLitter();
Console.WriteLine(
"不管他继续打扫,线程Id为:{0}", GetThreadId());
Thread.Sleep(
100);
Console.WriteLine(
"老婆把房间打扫好了,线程Id为:{0}", GetThreadId());
}
///
<summary>
/// 通知我去扔垃圾
///
</summary>
public
async
void
CommandDropLitter()
{
Console.WriteLine(
"这时我准备去扔垃圾,线程Id为:{0}", GetThreadId());
await Task.Run(() =>
{
Console.WriteLine(
"屁颠屁颠的去扔垃圾,线程Id为:{0}", GetThreadId());
Thread.Sleep(
1000);
});
Console.WriteLine(
"垃圾扔了还有啥吩咐,线程Id为:{0}", GetThreadId());
}
运行以上代码:
以上代码在 CommandDropLitter() 方法上加了 async 修饰符,并且使用 await 运算符开启了一个新的 Task 去执行另一个任务。注意:当前线程遇到 await 时,则立刻跳回调用方法继续往下执行。而 Task 执行完成之后将执行 await 之后的代码,并且与 await 之前的线程不是同一个。
2.模拟打开电源开关(关心是否执行完成,返回 Task 类型)
///
<summary>
/// 打开电源开关
///
</summary>
public
void
OpenMainsSwitch()
{
Console.WriteLine(
"我和老婆正在看电视,线程Id为:{0}", GetThreadId());
Console.WriteLine(
"突然停电了,快去看下是不是跳闸了");
Task task = CommandOpenMainsSwitch();
Console.WriteLine(
"没电了先玩会儿手机吧,线程Id为:{0}", GetThreadId());
Thread.Sleep(
100);
Console.WriteLine(
"手机也没电了只等电源打开,线程Id为:{0}", GetThreadId());
//task.Wait(); //所以这里将被阻塞,直到任务完成
//或者
while (!task.IsCompleted) { Thread.Sleep(
100); }
Console.WriteLine(
"又有电了我们继续看电视,线程Id为:{0}", GetThreadId());
}
///
<summary>
/// 通知我去打开电源开关
///
</summary>
public
async Task
CommandOpenMainsSwitch()
{
Console.WriteLine(
"这时我准备去打开电源开关,线程Id为:{0}", GetThreadId());
await Task.Run(() =>
{
Console.WriteLine(
"屁颠屁颠的去打开电源开关,线程Id为:{0}", GetThreadId());
Thread.Sleep(
1000);
});
Console.WriteLine(
"电源开关打开了,线程Id为:{0}", GetThreadId());
}
运行以上代码:
- 可见,调用 Wait() 方法后,当前线程被阻塞了,直到 Task 执行完成后,当前线程才继续执行。
- 注意:由于 CommandOpenMainsSwitch() 是一个异步方法,虽然返回类型为 Task 类型,但是在我们代码中并没有写(也不能写) return task 语句,这是为什么呢?可能是这种返回类型比较特殊,或者编译器自动帮我们完成了吧!就算写也只能写 return 语句,后面不能跟对象表达式。
-
模拟去买盐(不止关心是否执行完成,还要获取执行结果。返回 Task<TResult> 类型)
///
<summary>
/// 做饭
///
</summary>
public
void
CookDinner()
{
Console.WriteLine(
"老婆开始做饭,线程Id为:{0}", GetThreadId());
Console.WriteLine(
"哎呀,没盐了");
Task<
string> task = CommandBuySalt();
Console.WriteLine(
"不管他继续炒菜,线程Id为:{0}", GetThreadId());
Thread.Sleep(
100);
string result = task.Result;
//必须要用盐了,等我把盐回来(停止炒菜(阻塞线程))
Console.WriteLine(
"用了盐炒的菜就是好吃【{0}】,线程Id为:{1}", result, GetThreadId());
Console.WriteLine(
"老婆把饭做好了,线程Id为:{0}", GetThreadId());
}
///
<summary>
/// 通知我去买盐
///
</summary>
public
async Task<
string>
CommandBuySalt()
{
Console.WriteLine(
"这时我准备去买盐了,线程Id为:{0}", GetThreadId());
string result =
await Task.Run(() =>
{
Console.WriteLine(
"屁颠屁颠的去买盐,线程Id为:{0}", GetThreadId());
Thread.Sleep(
1000);
return
"盐买回来了,顺便我还买了一包烟";
});
Console.WriteLine(
"{0},线程Id为:{1}", result, GetThreadId());
return result;
}
运行以上代码:
- 以上代码 task.Result 会阻塞当前线程,与 task.Wait() 类似。
- 注意:与前面返回类型为 Task 的 CommandOpenMainsSwitch() 方法一样,虽然 CommandBuySalt() 方法返回类型为 Task<string>,但是我们的返回语句是 return 字符串。
其他示例
-
在前面(模拟去买盐)的示例中,异步方法中只开启了一个 Task,如果开启多个 Task 又是什么情况,看代码:
public
void
AsyncTest()
{
Console.WriteLine(
"AsyncTest() 方法开始执行,线程Id为:{0}", GetThreadId());
Task task = Test1();
Console.WriteLine(
"AsyncTest() 方法继续执行,线程Id为:{0}", GetThreadId());
task.Wait();
Console.WriteLine(
"AsyncTest() 方法结束执行,线程Id为:{0}", GetThreadId());
}
public
async Task
Test1()
{
Console.WriteLine(
"Test1() 方法开始执行,线程Id为:{0}", GetThreadId());
await Task.Factory.StartNew((state) =>
{
Console.WriteLine(
"Test1() 方法中的 {0} 开始执行,线程Id为:{1}", state, GetThreadId());
Thread.Sleep(
1000);
Console.WriteLine(
"Test1() 方法中的 {0} 结束执行,线程Id为:{1}", state, GetThreadId());
},
"task1");
await Task.Factory.StartNew((state) =>
{
Console.WriteLine(
"Test1() 方法中的 {0} 开始执行,线程Id为:{1}", state, GetThreadId());
Thread.Sleep(
3000);
Console.WriteLine(
"Test1() 方法中的 {0} 结束执行,线程Id为:{1}", state, GetThreadId());
},
"task2");
Console.WriteLine(
"Test1() 方法结束执行,线程Id为:{0}", GetThreadId());
}
运行以上代码:
当异步方法中有多个 await 时,会依次执行所有的 Task,只有当所有 Task 执行完成后才表示异步方法执行完成,当前线程才得以执行。
-
同样以前面(模拟去买盐)的示例,如果发现其实家里还有盐,这是就要告诉我不用买了(取消异步操作),怎么实现?这就要借助 System.Threading.CancellationTokenSource 和 System.Threading.Tasks.CancellationToken 对象来完成。
///
<summary>
/// 做饭(买盐任务取消)
///
</summary>
public
void
CookDinner_CancelBuySalt()
{
Console.WriteLine(
"老婆开始做饭,线程Id为:{0}", GetThreadId());
Console.WriteLine(
"哎呀,没盐了");
CancellationTokenSource source =
new CancellationTokenSource();
Task<
string> task = CommandBuySalt_CancelBuySalt(source.Token);
Console.WriteLine(
"不管他继续炒菜,线程Id为:{0}", GetThreadId());
Thread.Sleep(
100);
string result =
"家里的盐";
if (!
string.IsNullOrEmpty(result))
{
source.Cancel();
//传达取消请求
Console.WriteLine(
"家里还有盐不用买啦,线程Id为:{0}", GetThreadId());
}
else
{
//如果已取消就不能再获得结果了(否则将抛出 System.Threading.Tasks.TaskCanceledException 异常)
//你都叫我不要买了,我拿什么给你?
result = task.Result;
}
Console.WriteLine(
"既然有盐我就继续炒菜【{0}】,线程Id为:{1}", result, GetThreadId());
Console.WriteLine(
"老婆把饭做好了,线程Id为:{0}", GetThreadId());
Console.WriteLine(
"最终的任务状态是:{0},已完成:{1},已取消:{2},已失败:{3}",
task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
}
///
<summary>
/// 通知我去买盐(又告诉我不用买了)
///
</summary>
public
async Task<
string>
CommandBuySalt_CancelBuySalt(
CancellationToken token)
{
Console.WriteLine(
"这时我准备去买盐了,线程Id为:{0}", GetThreadId());
//已开始执行的任务不能被取消
string result =
await Task.Run(() =>
{
Console.WriteLine(
"屁颠屁颠的去买盐,线程Id为:{0}", GetThreadId());
Thread.Sleep(
1000);
}, token).ContinueWith((t) =>
//若没有取消就继续执行
{
Console.WriteLine(
"盐已经买好了,线程Id为:{0}", GetThreadId());
Thread.Sleep(
1000);
return
"盐买回来了,顺便我还买了一包烟";
}, token);
Console.WriteLine(
"{0},线程Id为:{1}", result, GetThreadId());
return result;
}
运行以上代码:
- 刚开始我以为调用 source.Cancel() 方法后会立即取消 Task 的执行,仔细一想也不太可能。如果需要在 Task 执行前或者执行期间完成取消操作,我们自己写代码判断 cancellationToken.IsCancellationRequested 属性是否为 true(该属性在调用 source.Cancel() 后或者 source.CancelAfter() 方法到达指定时间后为 true),如果为 true 结束执行即可。
- 这里所说的“传达取消请求”的意思是,每个 Task 在执行之前都会检查 cancellationToken.IsCancellationRequested 属性是否为 true,如果为 true 则不执行 Task,并将设置 Status、IsCompleted、IsCanceled 等。
- 所以,在 Task 的源码中有这样一段代码
if (cancellationToken.IsCancellationRequested)
{
// Fast path for an already-canceled cancellationToken
this.InternalCancel(
false);
}
-
乘热打铁,我们再来看看多个 CancellationTokenSource 取消异步任务,以及注册取消后的回调委托方法,继续以(模拟去买盐)为例:
///
<summary>
/// 做饭(多个消息传达买盐任务取消)
///
</summary>
public
void
CookDinner_MultiCancelBuySalt()
{
Console.WriteLine(
"老婆开始做饭,线程Id为:{0}", GetThreadId());
Console.WriteLine(
"哎呀,没盐了");
CancellationTokenSource source1 =
new CancellationTokenSource();
//因为存在而取消
CancellationTokenSource source2 =
new CancellationTokenSource();
//因为放弃而取消
CancellationTokenSource source = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);
//注册取消时的回调委托
source1.Token.Register(() =>
{
Console.WriteLine(
"这是因为{0}所以取消,线程Id为:{1}",
"家里还有盐", GetThreadId());
});
source2.Token.Register((state) =>
{
Console.WriteLine(
"这是因为{0}所以取消,线程Id为:{1}", state, GetThreadId());
},
"不做了出去吃");
source.Token.Register((state) =>
{
Console.WriteLine(
"这是因为{0}所以取消,线程Id为:{1}", state, GetThreadId());
},
"没理由");
//这里必须传递 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 对象
Task<
string> task = CommandBuySalt_MultiCancelBuySalt(source.Token);
Console.WriteLine(
"等等,好像不用买了,线程Id为:{0}", GetThreadId());
Thread.Sleep(
100);
string[] results =
new
string[] {
"家里的盐",
"不做了出去吃",
"没理由" };
Random r =
new Random();
switch (r.Next(
1,
4))
{
case
1:
source1.Cancel();
//传达取消请求(家里有盐)
//source1.CancelAfter(3000); //3s后才调用取消的回调方法
Console.WriteLine(
"既然有盐我就继续炒菜【{0}】,线程Id为:{1}", results[
0], GetThreadId());
break;
case
2:
source2.Cancel();
//传达取消请求(不做了出去吃)
//source2.CancelAfter(3000); //3s后才调用取消的回调方法
Console.WriteLine(
"我们出去吃不用买啦【{0}】,线程Id为:{1}", results[
1], GetThreadId());
break;
case
3:
source.Cancel();
//传达取消请求(没理由)
//source.CancelAfter(3000); //3s后才调用取消的回调方法
Console.WriteLine(
"没理由就是不用买啦【{0}】,线程Id为:{1}", results[
2], GetThreadId());
break;
}
Console.WriteLine(
"最终的任务状态是:{0},已完成:{1},已取消:{2},已失败:{3}",
task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
}
///
<summary>
/// 通知我去买盐(又告诉我不用买了,各种理由)
///
</summary>
public
async Task<
string>
CommandBuySalt_MultiCancelBuySalt(
CancellationToken token)
{
Console.WriteLine(
"这时我准备去买盐了,线程Id为:{0}", GetThreadId());
//已开始执行的任务不能被取消
string result =
await Task.Run(() =>
{
Console.WriteLine(
"屁颠屁颠的去买盐,线程Id为:{0}", GetThreadId());
Thread.Sleep(
1000);
}, token).ContinueWith((t) =>
//若没有取消就继续执行
{
Console.WriteLine(
"盐已经买好了,线程Id为:{0}", GetThreadId());
Thread.Sleep(
1000);
return
"盐买回来了,顺便我还买了一包烟";
}, token);
Console.WriteLine(
"{0},线程Id为:{1}", result, GetThreadId());
return result;
}
运行以上代码:
- 当调用 source.Cancel() 方法后,会立即取消并调用 token 注册的回调方法;而调用 existSource.CancelAfter() 方法则会等到达指定的毫秒数后才会取消。
- 注意:传递给异步方法的 token 对象,必须是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 对象,否则取消将无效。
- 回调的委托方法始终只有两个,一个是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 对象的注册委托,另一个是调用 Cancel()/CancelAfter() 方法的 Token 对象的注册委托。
- 如果以上代码调用的是 CancelAfter(3000) 方法,运行结果如下: