[C#]如何正確的不使用await的方式,把async的method改成同步的
前言
如果有去了解過Async Await的使用方式,大約都會知道一旦我們使用Async Await就要從頭到尾都使用,比如說我們從Repository層級,就是使用async結尾的方法,那我們直到web api輸出的時候,一樣都得使用Await的方式去對應,千萬不要在非主控台或window服務之外,使用Result或Wait的方式,以免造成應用程式死當掉的問題,但是有些時候因為需求改變,所以改了某個buesiness code的時候,卻發現只有async的方法可以用,但是這個準備要修改的方法,已經有好幾十個方法呼叫它了,那我們如果一定得改成async await的方式,不就要連動改了呼叫此方法的全部方法,接下來就想談談要用什麼方式去解決此類的問題。
模擬情境
這邊以之前async文章的例子當成範例,去呼叫政府的公開資料,使用HttpClient的方式來模擬async,先建立TaipeiData
public class TaipeiData
{
public Result result { get; set; }
public class Result
{
public int offset { get; set; }
public int limit { get; set; }
public int count { get; set; }
public string sort { get; set; }
public Result1[] results { get; set; }
}
public class Result1
{
public string _id { get; set; }
public string deptName { get; set; }
public string data_id { get; set; }
public string post_date { get; set; }
public string news_from { get; set; }
public string authority { get; set; }
public string contact { get; set; }
public string contact_phone { get; set; }
public string news_class { get; set; }
public string news_title { get; set; }
public string news_content { get; set; }
public string start_date { get; set; }
public string end_date { get; set; }
public string actstart_date { get; set; }
public string actend_date { get; set; }
public string s_time { get; set; }
public string d_time { get; set; }
public string regis { get; set; }
public string county { get; set; }
public string address { get; set; }
public string cycle_type { get; set; }
public string cycle_value { get; set; }
public string news_scope { get; set; }
public string keyword { get; set; }
public string link { get; set; }
public string file { get; set; }
public string Extra1 { get; set; }
public string fctupublic { get; set; }
public string transKey { get; set; }
public object vgroup { get; set; }
public string longitude { get; set; }
public string latitude { get; set; }
}
}
再來建立另一個TaipeiWeater
public class TaipeiWeater
{
public Result result { get; set; }
public class Result
{
public int offset { get; set; }
public int limit { get; set; }
public int count { get; set; }
public string sort { get; set; }
public Result1[] results { get; set; }
}
public class Result1
{
public string _id { get; set; }
public string locationName { get; set; }
public DateTime startTime { get; set; }
public DateTime endTime { get; set; }
public string parameterName1 { get; set; }
public string parameterValue1 { get; set; }
public string parameterName2 { get; set; }
public string parameterUnit2 { get; set; }
public string parameterName3 { get; set; }
public string parameterUnit3 { get; set; }
}
}
再來就是呼叫政府資料的類別方法TaipeiDataDao.cs
public class TaipeiDataDao
{
public async Task<TaipeiWeater> GetWeater()
{
using (var httpClient = new HttpClient())
{
var result = httpClient.GetAsync("http://data.taipei/opendata/datalist/apiAccess?scope=resourceAquire&rid=e6831708-02b4-4ef8-98fa-4b4ce53459d9").Result;
return await result.Content.ReadAsAsync<TaipeiWeater>();
}
}
public async Task<TaipeiData> GetTaipeiData()
{
using (var httpClient = new HttpClient())
{
var result = httpClient.GetAsync("http://data.taipei/opendata/datalist/apiAccess?scope=resourceAquire&rid=e6831708-02b4-4ef8-98fa-4b4ce53459d9").Result;
return await result.Content.ReadAsAsync<TaipeiData>();
}
}
}
然後由service去呼叫dao層
//假設原本是呼叫ado.net的,現在需求改成要呼叫web api來取得資料,就會變成只有async的方法
//但是因為有好幾十個web api的action都呼叫了這兩個方法,我們無法把web api改成async await的方式
public class TaipeiDataService
{
TaipeiDataDao dao = new TaipeiDataDao();
public TaipeiWeater GetWeater()
{
return dao.GetWeater().Result;
}
public TaipeiData GetTaipeiData()
{
return dao.GetTaipeiData().Result;
}
}
接著是呼叫的web api
public class ValuesController : ApiController
{
public IHttpActionResult Get()
{
var httpClientExample = new HttpClientExample();
var result = new
{
Weaters = httpClientExample.GetWeater(),
TaipeiData = httpClientExample.GetTaipeiData()
};
return Ok(result);
}
}
以目前程式碼的方式,在非主控台之外的應用程式執行,有很大的機率會造成程式死當。
用更安全的方式把async的方法改為同步
以目前的例子,如果我們不需要回傳值,也就是Task的方式,我們其實只要使用如下語法就行了
result.Content.ReadAsAsync<TaipeiWeater>().ConfigureAwait(false); //這個只能是Task也就是void,不能有回傳型別
但是用這種方式會造成根本就取不到資料,那就自定一個AsyncHelper來搭配使用吧
public static class AsyncHelper
{
private static readonly TaskFactory MyTaskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return MyTaskFactory
.StartNew(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
public static void RunSync(Func<Task> func)
{
MyTaskFactory
.StartNew(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
}
接著改造一下我們的TaipeiDataService的部份
//假設原本是呼叫ado.net的,現在需求改成要呼叫web api來取得資料,就會變成只有async的方法
//但是因為有好幾十個web api的action都呼叫了這兩個方法,我們無法把web api改成async await的方式
public class TaipeiDataService
{
TaipeiDataDao dao = new TaipeiDataDao();
public TaipeiWeater GetWeater()
{
return AsyncHelper.RunSync<TaipeiWeater>(()=>dao.GetWeater());
}
public TaipeiData GetTaipeiData()
{
return AsyncHelper.RunSync<TaipeiData>(() => dao.GetTaipeiData());
}
}
結論
其實這篇的方式是在非不得已的狀況下才使用,正常狀況下即然我們使用了async的方式,就要充分的享受async的好處,不過確實會有很多無奈的情境,逼得我們不能使用async await的方式,那至少請用AsyncHelper的方式,以免發生任何無法預估的狀況發生,希望此篇對讀者有幫助,如果有任何更好的做法,再請告知筆者。
參考來源
https://bitbucket.org/snippets/detangleddigital/ReKG/c-asynchelper