同步與異步
同步:如果一個程序調用某個方法,等待其執行所有處理後才繼續執行,我們就稱這樣的方法是同步的,這是默認的形式
異步:異步的方法在處理完成之前就返回到調用方法,C#的async/await特性可以創建並使用異步方法。
async/await特性的組成
(1)調用方法:該方法調用異步方法,然後在異步方法(可能在相同的線程,也可能在不同的線程)執行其任務的時候繼續執行
(2)異步(async)方法:該方法異步執行其工作,然後立即返回到調用方法
(3)await表達式:用於異步方法內部,指明需要異步執行的任務,一個異步方法可以包含任意多個await表達式,不過如果一個都不包含的話編譯器會發出警告。
異步方法的定義規則
(1)方法頭中包含async方法修飾符,它在返回類型之前。
(2)包含1個或多個await表達式,表示可以異步完成的任務
(3)方法的返回類型爲void、Task、Task<T>中的一種,Task和Task<T>的返回對象表示將在未來完成的工作,調用方法和異步方法可以繼續執行。
void:如果調用方法僅僅想執行異步方法,而不需要與它做任何進一步的交互時,異步方法可以返回void類型,這時即使異步方法中包含任何return語句,也不會返回任何東西。
Task:如果調用方法不需要從異步方法中返回某個值,但需要檢查異步方法的狀態,那麼異步方法可以返回一個Task類型的對象,這時,即使異步方法中出現了return語句,也不會返回任何東西。
Task<T>:如果方法要從調用中獲取一個T類型的值,異步方法的返回類型就必須是Task<T>,調用方法將通過讀取Task的Result屬性來獲取這個T類型的值。
(4)任何返回Task<T>類型的異步方法其返回值必須爲T類型或可以隱式轉換爲T的類型
(5)異步方法的參數可以爲任意類型任意數量,但不能爲out、ref參數
(6)按照約定,異步方法的名稱應該以Async爲後綴
(7)除了方法以外,Lambda表達式和匿名方法也可以作爲異步對象。
異步方法結構
(1)第一個await表達式之前的部分:從方法開頭到第一個await表達式之間的所有代碼,這一部分應該只包含少量且無需長時間處理的代碼。
(2)await表達式:表示被異步執行的任務
(3)後續部分:在await表達式以後出現的方法中的其餘代碼,包括其執行環境,如所在線程信息、目前作用域的變量值,以及當await表達式完成後要重新執行所需的其他信息。
異步方法控制流
調用異步方法後,從第一個await表達式之前的代碼開始,正常(同步)執行直到遇見第一個await,這一區域實際上在第一個await表達式處結束,此時await任務還沒有完成(大多數情況下)。當await任務完成時,方法將繼續同步執行,如果還有其他await,就重複上述過程。
當達到await表達式時,異步方法將控制流返回到調用方法,如果方法的返回類型爲Task或Task<T>類型,將創建一個Task對象,表示需異步完成的任務和後續,然後將該Task返回到調用方法。
目前有倆個控制流:異步方法內和調用方法內的,異步方法內的代碼完成以下工作,異步執行await表達式的空閒任務,當await表達式完成時,執行後續部分,後續部分本身也可能包含其他await表達式,這些表達式也將按照相同的方式處理,即異步執行await表達式,然後執行後續部分。當後續部分遇到return語句或到達方法末尾時,如果方法返回類型爲void,控制流將退出,如果方法返回類型爲Task,後續部分設置Task的屬性並退出,如果返回類型爲Task<T>,後續部分還將設置Task對象的Result屬性。
同時,調用方法中的代碼將繼續其進程,從異步方法獲取Task對象,當需要其實際值時,就引用Task對象的Result屬性,屆時,如果異步方法設置了該屬性,調用方法就能獲得該值並繼續,否則將暫停並等待屬性被設置,然後在執行。
class A
{
static public void DoRun()
{
var t = foo();
Console.WriteLine(t.Result);
}
static private async Task<int> foo()
{
var t1 = Task.Run(() =>
{
Thread.Sleep(100);
return 100; //設置Task對象的Result屬性
});
var t2 = Task.Run(() =>
{
Thread.Sleep(200);
return 200; //設置Task對象的Result屬性
});
await Task.WhenAll(new List<Task<int>>() { t1, t2 }); //異步等待集合內的Task都完成,不會佔用主線程的時間
return t1.Result+t2.Result;
}
}
public class Program
{
static void Main(string[] args)
{
A.DoRun();
}
}
await表達式
await表達式指定了一個異步執行的任務,由await關鍵字和一個空閒對象(稱爲任務)組成,這個任務可能是一個Task類型,也可能不是,默認情況下,這個任務在當前線程異步運行。
一個空閒對象即是一個awaitable類型的實例,awaitable類型是指包含GetAwaiter方法的類型,該方法沒有參數,返回一個稱爲awaiter類型的對象,awaiter類型包含以下成員:
(1)bool IsCompleted {get;}
(2)void OnCompleted(Action);
它還包含以下成員之一:
(1)void GetResult();
(2)T GetResult(); (T爲任意類型)
實際上,並不需要構建自己的awaitable,相反,應該使用Task類,它是awaitable類型,作爲await表達式的任務,最簡單的方式是使用Task.Run方法來創建一個Task,關於Task.Run,有一點非常重要,即它是在不同的線程上運行你的方法。
Task Run(Func<TReturn> func) 這個是Run函數衆多重載版本中的其中一個,參數爲一個委託類型,這個委託沒有參數,返回值類型爲TReturn,因此要將你的方法傳遞給Task.Run方法時,需要基於該方法創建一個委託。
取消一個異步操作
當我們想要終止異步執行時,可以通過類CancellationToken和類CancellationTokenSource來實現,擁有CancellationToken對象的任務需要定期檢查其令牌(token)狀態,如果CancellationToken對象的IsCancellationRequested屬性爲true,任務需停止其操作並返回。CancellationToken是不可逆的,並且只能使用一次,也就是說,一旦IsCancellationRequested屬性被設置爲true,就不能更改了。
CancellationTokenSource對象創建可分配給不同任務的CancellationToken對象,任何持有CancellationTokenSource的對象都可以調用其Cancel方法,這會將CancellationToken的IsCancellationRequested屬性設置爲true。
class MyClass
{
public async Task RunAsync(CancellationToken ct)
{
if (ct.IsCancellationRequested)
{
Console.WriteLine(1111);
return;
}
await Task.Run(() => CycleMethod(ct), ct);
}
void CycleMethod(CancellationToken ct)
{
Console.WriteLine("Starting CyclyMethod");
const int max = 5;
for (int i = 0; i < max; i++)
{
if (ct.IsCancellationRequested)
{
Console.WriteLine("退出");
return; //退出
}
Thread.Sleep(1000);
Console.WriteLine("{0} of {1} iterations completed", i + 1, max);
}
}
}
public 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); //等待3秒
cts.Cancel(); //取消異步
t.Wait(); //等待這個Task完成執行過程
Console.WriteLine("Was Cancelled:{0}", token.IsCancellationRequested);
}
}
輸出爲:
Starting CyclyMethod
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
退出
Was Cancelled:True
t.wait會等待這個Task結束後在繼續向下執行,如果沒有這行代碼,主程序會提前打印最後一樣代碼,然後退出,這樣就會少輸出一定內容,這和之前所說的一樣,假如異步方法返回的是Task<int>類型,那麼如果在主程序中如果使用到該返回的Result字段,則會等待這個值被賦值了之後再繼續向下走,否則主程序不會等待異步方法。
在調用方法中同步地等待任務(Wait)
調用方法可以調用任意多個異步方法並接收它們返回的Task對象,然後你的代碼會繼續執行其他任務,但在某個點上可能需要等待某個特殊Task對象完成,然後再繼續,爲此Task類提供了一個實例方法Wait,可以在Task對象上調用該方法。
Wait:用於等待單一Task對象
WaitAll(靜態方法):用於等待一組Task中所有任務都結束
WaitAny(靜態方法):用於等待一組Task中某一個任務結束
在異步方法中異步地等待任務(WhenAll、WhenAny)
有時在異步方法中,會希望用await表達式來等待Task,調用到await時異步方法會返回到調用方法,然後異步方法執行自己的流程,可以通過Task.WhenAll和Task.WhenAny來實現等待一個或所有任務完成,然後接着執行後面語句。例如tasks是多個任務的集合,當調用到await Task.WhenAll(tasks); 則等待集合內所有任務都完成再繼續走下面的代碼,如果換成Task.WhenAny則集合內只要有一個任務完成則就走下面的代碼。
Task.Delay方法
Task.Delay方法創建一個Task對象,該對象將暫停其在線程中的處理,並在一定時間之後完成,與Thread.Sleep阻塞線程不同的是,Task.Delay不會阻塞線程,線程可以繼續處理其他工作。
Task.Yield
Task.Yield方法創建一個立即返回的awaitable,等待一個Yield可以讓異步方法在執行後續部分的同時返回到調用方法。可以將其理解成離開當前的消息隊列,回到隊列末尾,讓處理器有時間處理其他任務。
static class DoStuff
{
public static async Task<int> FindSeriesSum(int i1)
{
int sum = 0;
for(int i=0;i<i1;i++)
{
sum += i;
if (i % 10 == 0)
{
Console.WriteLine("前");
await Task.Yield();
Console.WriteLine("後");
}
}
return sum;
}
}
public class Program
{
static void Main(string[] args)
{
Task<int> value = DoStuff.FindSeriesSum(100);
Console.WriteLine(111);
Console.WriteLine(value.Result);
Console.ReadLine();
}
}