在 C#1 的時候就包含了APM,在 APM 模型中,非同步操作通過 IAsyncResult 介面實現,包括兩個方法 BeginOperationName 和 EndOperationName ,分別表示開始和結束非同步操作。 ...
在 C#1 的時候就包含了APM,在 APM 模型中,非同步操作通過 IAsyncResult 介面實現,包括兩個方法 BeginOperationName 和 EndOperationName ,分別表示開始和結束非同步操作。
Demo
我們先來看一個同步示例。新建WPF程式,在界面上放一個按鈕。點擊按鈕訪問外網,會有一定時間的阻塞。
private void SyncBtn_Click(object sender, RoutedEventArgs e)
{
// 記錄時間
Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
// 訪問外網網站網站
var req = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");
req.GetResponse();
// 記錄時間
Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}
當我們點擊按鈕後,因為web請求是同步的,會阻塞UI線程一定時間。從輸出日誌上看阻塞時間是 1 秒鐘左右,此時界面呈卡死狀態。
日誌輸出如下:
13:16:09.5031834,ThreadID = 1
13:16:10.5220362,ThreadID = 1
從運行效果和日誌,我們可以看出:
- WebRequest方法調用前後都是在同一個線程上執行-UI線程
- WebReqeust方法阻塞了UI線程,導致“假死”現象
WebRequest也提供了非同步方法,BeginGetResponse,EndGetResponse。我們修改一下代碼,新增一個按鈕。
private void APM_Btn_Click(object sender, RoutedEventArgs e)
{
// 記錄時間
Debug.WriteLine("1-" + DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
// 訪問外網網站網站
var req = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");
req.BeginGetResponse(new AsyncCallback(t => { WebRequestCallback(t,req); }), null);
// 記錄時間
Debug.WriteLine("3-" + DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}
/// <summary>
/// 非同步回調
/// </summary>
/// <param name="result"></param>
private void WebRequestCallback(IAsyncResult result, WebRequest request)
{
var response = request.EndGetResponse(result);
// 獲取返回數據流
var stream = response.GetResponseStream();
using(StreamReader reader = new StreamReader(stream))
{
StringBuilder sb = new StringBuilder();
while(!reader.EndOfStream)
{
var content = reader.ReadLine();
sb.Append(content);
}
// 記錄時間
Debug.WriteLine("2-" + DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}
}
運行效果如下:
日誌輸出如下:
1-13:10:01.7734197,ThreadID = 1
3-13:10:01.8826176,ThreadID = 1
2-13:10:03.2614022,ThreadID = 14
從運行效果和日誌,我們可以看出:
- 非同步方法不會阻塞調用方法,調用後立刻返回
- 非同步方法會在另外一個線程上執行
IAsyncResult
BeginOperationName 方法會返回一個實現了 IAsyncResult 介面的對象。該對象存儲了關於非同步操作的信息。
轉到定義,我們可以看到介面中都包含哪些內容:
自定義非同步方法
實現該介面,定義自己的非同步方法。
public class MyWebRequestResult : IAsyncResult
{
/// <summary>
/// 用戶定義屬性,可以存放數據
/// </summary>
public object? AsyncState => throw new NotImplementedException();
/// <summary>
/// 獲取用於等待非同步操作完成的 WaitHandle
/// </summary>
public WaitHandle AsyncWaitHandle => throw new NotImplementedException();
/// <summary>
/// 表示非同步操作是否是同步完成
/// </summary>
public bool CompletedSynchronously => throw new NotImplementedException();
/// <summary>
/// 表示非同步操作是否完成
/// </summary>
public bool IsCompleted => throw new NotImplementedException();
}
我們需要新建一個回調函數:
public class MyWebRequestResult : IAsyncResult
{
/// <summary>
/// 非同步回調函數
/// </summary>
private AsyncCallback _callback;
public string Result { get; private set; }
// 構造函數
public MyWebRequest(AsyncCallback asyncCallback, object state)
{
_callback = asyncCallback;
}
// 設置結果
public void SetComplete()
{
AsyncState = result;
Result = result;
if(null != _callback)
{
_callback(this);
}
}
// ...
}
在次之後就可以自定義 APM 非同步模型了:
public IAsyncResult BeginMyWebRequest(AsyncCallback callback)
{
// 1. 先給 IAsyncResult 進行賦值
var myResult = new MyWebRequestResult(callback, null);
var request = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");
// 2. 新建線程,執行耗時任務
new Thread(() => {
using (StreamReader sr = new StreamReader(request.GetResponse().GetResponseStream()))
{
var str = sr.ReadToEnd();
// 3. 耗時任務結束後 調用回調函數 & 保存結果
myResult.SetComplete(str);
}
}).Start();
return myResult;
}
public string EndMyWebRequest(IAsyncResult asyncResult)
{
MyWebRequestResult myResult = asyncResult as MyWebRequestResult;
return myResult.Result;
}
新增一個按鈕,進行調用:
private void MyAPM_Btn_Click(object sender, RoutedEventArgs e)
{
// 記錄時間
Debug.WriteLine("1-" + DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
// 調用 Begin 方法
BeginMyWebRequest(new AsyncCallback(MyAPM_Callback));
// 記錄時間
Debug.WriteLine("3-" + DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}
private void MyAPM_Callback(IAsyncResult result)
{
// 從這裡可以獲得 非同步操作的結果
var myResult = result as MyWebRequestResult;
var msg = EndMyWebRequest(myResult);
// 記錄時間
Debug.WriteLine("2-" + DateTime.Now.TimeOfDay.ToString() +
",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}
運行效果如下:
日誌輸出如下:
1-14:48:42.7278184,ThreadID = 1
3-14:48:42.7311174,ThreadID = 1
2-14:48:45.1049069,ThreadID = 6
結合效果和日誌,我們可以得出如下結論:
- 自定義的非同步方法沒有導致 UI 卡頓
- APM就是把耗時的任務交給新線程去做,然後利用委托進行回調
普通方法的非同步
如果是普通方法,也可以通過 委托非同步(BeginInvoke, EndInvoke):
public void MyAction()
{
var func = new Func<string, string>(t => {
Thread.Sleep(2000);
return t;
});
func.BeginInvoke("inputStr", t => {
string result = func.EndInvoke(t);
},null);
}
總結
- APM 模型是基於IAsyncResult來實現非同步操作的
- 非同步操作開始時,把委托傳遞給 IAsyncResult
- 在新線程上執行耗時操作
- 耗時操作結束後,修改 IAsyncResult 里的結果數據,並調用 IAsyncResult 里的委托回調
- 在回調里獲取 非同步操作 的結果