C# 同步非同步實作與介紹

前言

在這篇中簡易介紹C#同步與非同步的作法。
一般我們在做一些任務時有時需要:
1.等待上個任務完成
2.同時處理多個任務
這時就會需要用到非同步、同步的作法,可以節省時間(在同個時間內處理多個任務)。
可能用到Thead或是用Task作法asyncawait。在這篇中則以:
https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/async/
作為範例,簡易的操作一個需要處理同步/非同步的範例。

同步範例實作

簡單來說,就是每個任務都是會等待上面的任務完成才依序執行。
下方以製作一份早餐的同步範例,1~7步驟都要等上面的步驟完成才會繼續。

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using System;
using System.Threading.Tasks;

namespace async
{
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");

Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");

Bacon bacon = FryBacon(3);
Console.WriteLine("bacon is ready");

Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");

Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}

private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}

private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");

private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");

private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");

return new Toast();
}

private static Bacon FryBacon(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");

return new Bacon();
}

private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");

return new Egg();
}

private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}

private class Coffee
{
}

private class Egg
{
}

private class Toast
{
}

private class Juice
{
}

private class Bacon
{

}
}
}

結果:

可以發現這樣很難節省時間,所以其實能把某些部分拆成異步來做,有助於節省時間。(雖然同時間可能記憶體耗能會加大)

異步範例實作

準備早餐程式碼的簡單非同步版本看起來像下列程式碼片段:
FryEggsAsync、FryBaconAsync、MakeToastWithButterAndJamAsync更新為Task,函式使用Async
這時會變成先做第一步,之後2、3、(4+5)步是併發執行,並使用while做判斷,依序把完成的Task移除List,仍需處理的Task數量為0就跳出,代表完成了!
再由下個任務6繼續。

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace async
{
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");

var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);

var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
breakfastTasks.Remove(finishedTask);
}

Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);

return toast;
}

private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}

private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");

private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");

private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");

return new Toast();
}

private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");

return new Bacon();
}


private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");

return new Egg();
}

private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");

return new Toast();
}
private class Coffee
{
}

private class Egg
{
}

private class Toast
{
}

private class Juice
{
}

private class Bacon
{

}
}
}

結果:
可以發現為異步執行!
FryEggsAsync:原本需6秒
FryBaconAsync:原本需6秒
MakeToastWithButterAndJamAsync:原本需3秒
以同步執行的話需要:15秒
在異步執行只需取其中最大的秒數:6秒
大幅度的減省時間花費。

TaskAPI 包含兩種方法, Task.WhenAll 和 Task.WhenAny 撰寫非同步程式碼,能在多個背景工作上執行非封鎖等候。
await Task.WhenAny
await Task.WhenAll
更多可參考:
https://gist.github.com/relyky/52b4abf32b44d5d00a674b9cd34ca3a3

用紅筆畫起來部分,表示其併發執行。

結論

以Async為開頭的函數內步才能夠進行併發的操作。
並且搭配可以搭配await做Task的停頓。
即讓每個可Async的Task充分利用時間。

在工作上可能遇到需要併發、可同時執行時會使用,如有N個迴圈任務且沒有順序性,則可以寫入Task,或是需要用後端呼叫HTTP請求,可以一次併發,增加系統效率。或是有順序性執行的任務就很適合採用Task拆成子項目。

如果要再深入探討可以參考官方:
https://docs.microsoft.com/zh-tw/dotnet/csharp/async

參考資料

https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/async/
https://gist.github.com/relyky/52b4abf32b44d5d00a674b9cd34ca3a3
https://docs.microsoft.com/zh-tw/dotnet/csharp/async