[C#]如何正確的不使用await的方式,把async的method改成同步的

[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