ASP.NET MVC 如何在一個同步方法(非async)方法中等待async方法


問題

首先,在ASP.NET MVC 環境下對async返回的Task執行Wait()會導致線程死鎖。例:

        public ActionResult Asv2()
        {
            //dead lock
            var task = AssignValue2();
            task.Wait();
            return Content(_container);
        }
 
        private void Assign()
        {
            _container = "Hello World";
        }

        public async Task AssignValue2()
        {
            await Task.Delay(500);
            await Task.Run(() => Assign());
        }   

這是由於async方法注冊的回調要求返回到調用async的線程——而在主線程(action方法所在線程)中又對Task執行了Wait(),相互等待,導致了死鎖。

Wait一個async方法是否一定導致死鎖(ASP.NET MVC)

否。Task類型的對象有一個ConfigureAwait方法,將參數置為false可以防止回調返回當前線程。但是有一個前提條件:async方法的調用鏈中的所有async方法都必須指定ConfigureAwait(false)。例:

        public async Task AssignValue2()
        {
            await Task.Delay(500);
            await Task.Run(() => Assign());
        }

        public async Task AssignValue()
        {
            await Task.Run(() => Assign()).ConfigureAwait(false);
        }

        public async Task Wrapped()
        {
            await AssignValue().ConfigureAwait(false);
        }

        public async Task Wrapped2()
        {
            await AssignValue2().ConfigureAwait(false);
        }

        public async Task Update()
        {
            await Task.Run(() => { _container = _container + "Hello "; });
        }

        public async Task Update2()
        {
            await Task.Run(() => { _container = _container + "World"; });
        }

        #region 基本調用

        /// <summary>
        /// 調用層次最深的異步方法(第一個調用的異步方法)不要求返回到主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Asv1()
        {
            var task = AssignValue();
            task.Wait();
            return Content(_container);
        }

        /// <summary>
        /// ...返回主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Asv2()
        {
            //dead lock
            var task = AssignValue2();
            task.Wait();
            return Content(_container);
        }

        /// <summary>
        /// 所有都不要求返回主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Wrp()
        {
            var task = Wrapped();
            task.Wait();
            return Content(_container);
        }

        /// <summary>
        /// 其中一個要求返回主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Wrp2()
        {
            //dead lock
            var task = Wrapped2();
            task.Wait();
            return Content(_container);
        }

        #endregion
對比

可以看到,在Asv()與Wrp()中,分別直接Wait了AssignValue()與Wrapped()返回的Task,並沒有造成死鎖。因為這兩個方法的調用鏈中的所有的async操作都被配置為不返回當前線程(Action所在線程)。另外兩個標記為2的對比方法則不然,結果也相反。

對async調用鏈的一點理解

首先,await之后的代碼,是作為回調,被注冊到一個Awaitor對象中。其次,async中的Task是被成為promised風格的,也就是,被await的async方法承諾:“我會來回調你后面的這些邏輯(不是現在)”。那么對於以下的偽代碼:

        public async Task A()
        {
            await Task.Run(() => { });
        }

        public async Task B()
        {
            await A();
            //B.L
        }

        public async Task C()
        {
            await B();
            //C.L
        }

如果我們調用了C(),運行期間的事情是:運行B()->運行A(),然后:將//B.L部分代碼注冊到A的回調當中->將//C.L部分代碼注冊到B的回調當中。也就是說,await之前的操作和注冊的操作都是在當前線程完成的。那么,如果沒有ConfigureAwait(false),所有的回調操作都會期望返回到主線程。所以會導致各種線程死鎖。

總的來說,async這個關鍵字像是給C#開了點了新技能吧,以非常清新的方式就讓方法“天然”支持了異步(想想各種StartNew各種ContinueWith,嵌套層次一深的時候,那簡直...)。另外,ContinueWith會切換線程,也會帶來開銷。

在同步方法中Wait

async與await幾乎是自成體系的,只要await一個async方法,就會被要求將本方法標記為async,隨着不斷地接觸,個人感覺這是可以理解的(然而我解釋不來)。

根據上面的分析,之所以會導致線程鎖,主要原因是回調要求返回到調用線程(主線程),而作為一個同步方法,主線程必然是要等待的。所以解決方案也比較明確:想辦法別讓回調返回到主線程——即:在另外一個線程中調用async方法。先看看失敗的例子:

        #region 次線程創建,主線程wait
        //高概率 dead lock

        public ActionResult TcreateMwait()
        {
            Task task = null;
            Task.Run(() => { task = AssignValue2(); }).Wait();
            task.Wait();
            return Content(_container);
        }

        public ActionResult TcreateMunwait()
        {
            //主線程不等待的對比組
            Task task = null;
            Task.Run(() => { task = AssignValue2(); }).Wait();
            return Content(_container);
        }

        #endregion

我無法理解為何這個會失敗——肯定是我對Task以及線程的理解有問題,我回去補補課,這個先放這里。然后是成功的例子:

#region 次線程創建,次次線程wait(continue with),主線程wait次次線程

        public ActionResult Twait()
        {
            Task task = null;
            Task.Run(() => { task = AssignValue(); })
                .ContinueWith(token => task.Wait())
                .Wait();
            return Content(_container);
        }

        public ActionResult Twait2()
        {
            Task.Run(() => AssignValue2())
                .ContinueWith(task => { task.Wait(); })
                .Wait();
            return Content(_container);
        }

        public ActionResult Swait()
        {
            AsyncHelper.InvokeAndWait(AssignValue2);
            return Content(_container);
        }

        #endregion

然后,這里提供了一個輔助方法:

        public static void InvokeAndWait(Func<Task> asyncMethod)
        {
            Task.Run(() => asyncMethod())
                .ContinueWith(task => task.Wait())
                .Wait();
        }

小插曲:Resharp會提示你把()=>asyncMethod()直接使用asyncMethod代替,別信。

最后是對這個輔助方法的一些測試:

       #region waitsafely examples

        public ActionResult Inorder()
        {
            AsyncHelper.InvokeAndWait(Update);
            AsyncHelper.InvokeAndWait(Update2);
            return Content(_container);
        }

        public ActionResult NotInOrder()
        {
            AsyncHelper.InvokeAndWait(() => Task.WhenAll(Update(), Update2()));
            return Content(_container);
        }

        #endregion

最后,這里是使用的所有代碼,歡迎指點:

namespace Dpfb.Manage.Controllers
{
    public class AsyncsController : Controller
    {
        private void Assign()
        {
            _container = "Hello World";
        }

        private static string _container;

        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            _container = string.Empty;
        }

        public async Task AssignValue2()
        {
            await Task.Delay(500);
            await Task.Run(() => Assign());
        }

        public async Task AssignValue()
        {
            await Task.Run(() => Assign()).ConfigureAwait(false);
        }

        public async Task Wrapped()
        {
            await AssignValue().ConfigureAwait(false);
        }

        public async Task Wrapped2()
        {
            await AssignValue2().ConfigureAwait(false);
        }

        public async Task Update()
        {
            await Task.Run(() => { _container = _container + "Hello "; });
        }

        public async Task Update2()
        {
            await Task.Run(() => { _container = _container + "World"; });
        }

        #region 基本調用

        /// <summary>
        /// 調用層次最深的異步方法(第一個調用的異步方法)不要求返回到主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Asv1()
        {
            var task = AssignValue();
            task.Wait();
            return Content(_container);
        }

        /// <summary>
        /// ...返回主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Asv2()
        {
            //dead lock
            var task = AssignValue2();
            task.Wait();
            return Content(_container);
        }

        /// <summary>
        /// 所有都不要求返回主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Wrp()
        {
            var task = Wrapped();
            task.Wait();
            return Content(_container);
        }

        /// <summary>
        /// 其中一個要求返回主線程
        /// </summary>
        /// <returns></returns>
        public ActionResult Wrp2()
        {
            //dead lock
            var task = Wrapped2();
            task.Wait();
            return Content(_container);
        }

        #endregion

        #region 次線程創建,次次線程wait(continue with),主線程wait次次線程

        public ActionResult Twait()
        {
            Task task = null;
            Task.Run(() => { task = AssignValue(); })
                .ContinueWith(token => task.Wait())
                .Wait();
            return Content(_container);
        }

        public ActionResult Twait2()
        {
            Task.Run(() => AssignValue2())
                .ContinueWith(task => { task.Wait(); })
                .Wait();
            return Content(_container);
        }

        public ActionResult Swait()
        {
            AsyncHelper.InvokeAndWait(AssignValue2);
            return Content(_container);
        }

        #endregion

        #region 次線程創建,主線程wait
        //高概率 dead lock

        public ActionResult TcreateMwait()
        {
            Task task = null;
            Task.Run(() => { task = AssignValue2(); }).Wait();
            task.Wait();
            return Content(_container);
        }

        public ActionResult TcreateMunwait()
        {
            //主線程不等待的對比組
            Task task = null;
            Task.Run(() => { task = AssignValue2(); }).Wait();
            return Content(_container);
        }

        #endregion

        #region waitsafely examples

        public ActionResult Inorder()
        {
            AsyncHelper.InvokeAndWait(Update);
            AsyncHelper.InvokeAndWait(Update2);
            return Content(_container);
        }

        public ActionResult NotInOrder()
        {
            AsyncHelper.InvokeAndWait(() => Task.WhenAll(Update(), Update2()));
            return Content(_container);
        }

        #endregion


        public async Task A()
        {
            await Task.Run(() => { });
        }

        public async Task B()
        {
            await A();
            //B.L
        }

        public async Task C()
        {
            await B();
            //C.L
        }
    }
}
code_full


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM