考慮到直接講實現一個類Task庫思維有點跳躍,所以本節主要講解Async/Await的本質作用(解決了什麼問題),以及Async/Await的工作原理。實現一個類Task的庫則放在後面講。首先回顧一下上篇部落格的場景。
class Program
{
public static string GetMessage()
{
return Console.ReadLine();
}
public static string TranslateMessage(string msg)
{
return msg;
}
public static void DispatherMessage(string msg)
{
switch (msg)
{
case "MOUSE_MOVE":
{
OnMOUSE_MOVE(msg);
break;
}
case "MOUSE_DOWN":
{
OnMouse_DOWN(msg);
break;
}
default:
break;
}
}
public static void OnMOUSE_MOVE(string msg)
{
Console.WriteLine("開始繪製滑鼠形狀");
}
public static int Http()
{
Thread.Sleep(1000);//模擬網路IO延時
return 1;
}
public static void HttpAsync(Action<int> action,Action error)
{
//這裡我們用另一個執行緒來實現非同步IO,由於Http方法內部是通過Sleep來模擬網路IO延時的,這裡也只能通過另一個執行緒來實現非同步IO
//但記住,多執行緒是實現非同步IO的一個手段而已,它不是必須的,後面會講到如何通過一個執行緒來實現非同步IO。
Thread thread = new Thread(() =>
{
try
{
int res = Http();
action(res);
}
catch
{
error();
}
});
thread.Start();
}
public static Task<int> HttpAsync()
{
return Task.Run(() =>
{
return Http();
});
}
public static void OnMouse_DOWN(string msg)
{
HttpAsync()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.Faulted)
{
}else if(t.Status == TaskStatus.RanToCompletion)
{
Console.WriteLine(1);
//做一些工作
}
})
.ContinueWith(t =>
{
if (t.Status == TaskStatus.Faulted)
{
}
else if (t.Status == TaskStatus.RanToCompletion)
{
Console.WriteLine(2);
//做一些工作
}
})
.ContinueWith(t =>
{
if (t.Status == TaskStatus.Faulted)
{
}
else if (t.Status == TaskStatus.RanToCompletion)
{
Console.WriteLine(3);
//做一些工作
}
});
}
static void Main(string[] args)
{
while (true)
{
string msg = GetMessage();
if (msg == "quit") return;
string m = TranslateMessage(msg);
DispatherMessage(m);
}
}
}
在OnMouse_DOWN這個處理函式中,我們使用Task的ContinueWith函式進行鏈式操作,解決了回撥地獄問題,但是總感覺有點那麼不爽,我們假想有個關鍵字await它能實現以下作用:首先await必須是Task型別,必須是Task型別的(其實不是必要條件,後面會講到)原因是保證必須有ContinueWith這個函式,如果Task沒有返回值,則把await後面的程式碼放到Task中的ContinueWith函式體內,如果有返回值,則把Await後的結果轉化為訪問Task.Result屬性,文字說的可能不明白,看下示例程式碼
//無返回值轉換前
public async void Example()
{
Task t = Task.Run(() =>
{
Thread.Sleep(1000);
});
await t;
//做一些工作
}
//無返回值轉換後
public void Example()
{
Task t = Task.Run(() =>
{
Thread.Sleep(1000);
});
t.ContinueWith(task =>
{
//做一些工作
});
}
//有返回值轉換前
public async void Example()
{
Task<int> t = Task.Run<int>(() =>
{
Thread.Sleep(1000);
return 1;
});
int res = await t;
//使用res做一些工作
}
//有返回值轉換後
public void Example()
{
Task<int> t = Task.Run<int>(() =>
{
Thread.Sleep(1000);
return 1;
});
t.ContinueWith(task =>
{
//使用task.Result做一些工作
});
}
看起來不錯,但至少有以下問題,如下:
- 該種轉換方法不能很好的轉換Try/Catch結構
- 在迴圈結構中使用await不好轉換
- 該實現與Task型別緊密聯絡
一二點是我自己認為的,但第三點是可以從擴充套件async/await這點被證明的。但無論怎樣,async/await只是對方法按照一定的規則進行了變換而已,它並沒有什麼特別之處,具體來講,就是把Await後面要執行的程式碼放到一個類似ContinueWith的函式中,在C#中,它是以狀態機的形式表現的,每個狀態都對應一部分程式碼,狀態機有一個MoveNext()方法,MoveNext()根據不同的狀態執行不同的程式碼,然後每個狀態部分對應的程式碼都會設定下一個狀態欄位,然後把自身的MoveNext()方法放到類似ContinueWith()的函式中去執行,整個狀態機由回撥函式推動。我們嘗試手動轉換以下async/await方法。
public static Task WorkAsync()
{
return Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Done!");
});
}
public static async void Test()
{
Console.WriteLine("步驟1");
await WorkAsync();
Console.WriteLine("步驟2");
await WorkAsync();
Console.WriteLine("步驟3");
}
手動寫一個簡單的狀態機類
public class TestAsyncStateMachine
{
public int _state = 0;
public void Start() => MoveNext();
public void MoveNext()
{
switch(_state)
{
case 0:
{
goto Step0;
}
case 1:
{
goto Step1;
}
default:
{
Console.WriteLine("步驟3");
return;
}
}
Step0:
{
Console.WriteLine("步驟1");
_state = 1;
WorkAsync().ContinueWith(t => this.MoveNext());
return;
}
Step1:
{
_state = -1;
Console.WriteLine("步驟2");
WorkAsync().ContinueWith(t => this.MoveNext());
return;
}
}
}
而Test()方法則變成了這樣
public static void Test()
{
new TestAsyncStateMachine().Start();
}
注意Test()方法返回的是void,這意味這呼叫方將不能await Test()。如果返回Task,這個狀態機類是不能正確處理的,如果要正確處理,那麼狀態機在Start()啟動後,必須返回一個Task,而這個Task在整個狀態機流轉完畢後要變成完成狀態,以便呼叫方在該Task上呼叫的ContinueWith得以繼續執行,而就Task這個類而言,它是沒有提供這種方法來主動控制Task的狀態的,這個與JS中的Promise不同,JS裡面用Reslove函式來主動控制Promise的狀態,並導致在該Promise上面的Then鏈式呼叫得以繼續完成,而在C#裡面怎麼做呢?既然使用了狀態機來實現async/await,那麼在轉換一個返回Task的函式時肯定會遇到,怎麼處理?後面講。
首先解決一下與Task型別緊密聯絡這個問題。
從狀態機中可以看到,主要使用到了Task中的ContinueWith這個函式,它的語義是在任務完成後,執行回撥函式,通過回撥函式拿到結果,這個程式設計風格也叫做CPS(Continuation-Passing-Style, 續體傳遞風格),那麼我們能不能把這個函式給抽象出來呢?語言開發者當然想到了,它被抽象成了一個Awaiter因此編譯器要求await的型別必須要有GetAwaiter方法,什麼樣的型別才是Awaiter呢?編譯器規定主要實現瞭如下幾個方法的型別就是Awaiter:
- 必須繼承INotifyCompletion介面,並實現其中的OnCompleted(Action continuation)方法
- 必須包含IsCompleted屬性
- 必須包含GetResult()方法
第一點好理解,第二點的作用是熱路徑優化,第三點以後講。我們再改造一下我們手動寫的狀態機。
public class TestAsyncStateMachine
{
public int _state = 0;
public void Start() => MoveNext();
public void MoveNext()
{
switch(_state)
{
case 0:
{
goto Step0;
}
case 1:
{
goto Step1;
}
default:
{
Console.WriteLine("步驟3");
return;
}
}
Step0:
{
Console.WriteLine("步驟1");
_state = 1;
TaskAwaiter taskAwaiter;
taskAwaiter = WorkAsync().GetAwaiter();
if (taskAwaiter.IsCompleted) goto Step1;
taskAwaiter.OnCompleted(() => this.MoveNext());
return;
}
Step1:
{
_state = -1;
Console.WriteLine("步驟2");
TaskAwaiter taskAwaiter;
taskAwaiter = WorkAsync().GetAwaiter();
if (taskAwaiter.IsCompleted) MoveNext();
taskAwaiter.OnCompleted(() => this.MoveNext());
return;
}
}
}
可以看到去掉了與Task中ContinueWith的耦合關係,並且如果任務已經完成,則可以直接執行下個任務,避免了無用的開銷。
因此我們可以總結一下async/await:
- async/await只是表示這個方法需要編譯器進行特殊處理,並不代表它本身一定是非同步的。
- Task類中的GetAwaiter主要是給編譯器用的。
第一點我們可以用以下例子來證明,有興趣的朋友可以自己去驗證以下,以便加深理解。
//該型別包含GetAwaiter方法,且GetAwaiter()返回的型別包含三個必要條件
public class MyAwaiter : INotifyCompletion
{
public void OnCompleted(Action continuation)
{
continuation();
}
public bool IsCompleted { get; }
public void GetResult()
{
}
public MyAwaiter GetAwaiter() => new MyAwaiter();
}
一個測試函式,注意必須返回void
public static async void AwaiterTest()
{
await new MyAwaiter();
Console.WriteLine("Done");
}
可以看到這是完全同步進行的。
覺得有收穫的不妨點個贊,有支援才有動力寫出更好的文章。