為執行SignalR 2.0,將ASP.NET MVC 4專案目標平台改成.NET 4.5。測試了一陣子,今天才由事件檢視器發現: 雖然已編譯成.NET 4.5,因web.config <system.web><httpRuntime />未指定4.5,這段時間一直是用.NET 4.0執行,導致SignalR 2無法啟用WebSocket! (登楞)

修改web.config還不簡單? 順手調了,ASP.NET MVC也壞了! orz

某個Controller Action出現以下錯誤

InvalidOperationException: An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %>.

InvalidOperationException: 非同步作業目前無法開始。非同步作業只有在非同步處理常式或模組或是頁面生命週期中特定事件期間中才能開始。如果執行頁面時發生此例外狀況,請確認頁面已標示為 <%@ Page Async="true" %>。

研究發現問題出在該Action叫用某個第三方元件,其中引用BackgroundWorker(事實上只要是非同步作業都會導致錯誤,例如WebClient.DownloadStringAsync()),違反ASP.NET 4.5的政策。問題可簡化成以下範例:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
 
namespace Mvc4.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            SomeJobBy3rdPtyLibrary();
            return Content("Done");
        }
 
        private void SomeJobBy3rdPtyLibrary()
        {
            BackgroundWorker worker = new BackgroundWorker();
            AutoResetEvent are = new AutoResetEvent(false);
            worker.DoWork += (sender, e) =>
            {
                Thread.Sleep(3000);
                are.Set();
            };
            worker.RunWorkerAsync();
            are.WaitOne();
        }
 
    }
}

設成<httpRuntime targetFramework="4.0" />OK,一改成<httpRuntime targetFramework="4.5" />馬上出錯!

解決之道要將Action改為async Task<ActionResult> Index(),並在其中使用await同步等待結果。理想上SomeJobBy3rdPtyLibrary()應改為非同步呼叫配合,但基於程式不在控制範圍,故決定採萬用寫法await Task.Run(() => { … });解決。

        public async Task<ActionResult> Index()
        {
            await Task.Run(() =>
            {
                SomeJobBy3rdPtyLibrary();
            });
            return Content("Done");
        }

如果SomeJobBy3rdPtyLibrary()屬可控制範圍,改成以下形式會更理想:

        public async Task<ActionResult> Index()
        {
            await SomeJobBy3rdPtyLibrary();
            return Content("Done");
        }
 
        private Task SomeJobBy3rdPtyLibrary()
        {
            return Task.Factory.StartNew(() =>
            {
                Thread.Sleep(3000);
            });
        }

為什麼加上<httpRuntime targetFramework="4.5" />會有此差異? 而ASP.NET MVC又為何要對非同步作業作出上述限制? MSDN有篇文章提供頗詳細的說明:

當指定<httpRuntime targetFramework="4.5" />時,等同在web.config加入以下設定:

<configuration>
  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
    <add key="ValidationSettings:UnobtrusiveValidationMode" value="WebForms" />
  </appSettings>
    <system.web>
      <compilation targetFramework="4.5" />
      <machineKey compatibilityMode="Framework45" />
      <pages controlRenderingCompatibilityVersion="4.5" />
    </system.web>
</configuration>

而這回遇到的問題,即是受aspnet:UseTaskFriendlySynchronizationContext設定影響。UseTaskFriendlySynchronizationContext會帶來以下好處:

Enables the new await-friendly asynchronous pipeline that was introduced in 4.5. Many of our synchronization primitives in earlier versions of ASP.NET had bad behaviors, such as taking locks on public objects or violating API contracts. In fact, ASP.NET 4's implementation of SynchronizationContext.Post is a blocking synchronous call! The new asynchronous pipeline strives to be more efficient while also following the expected contracts for its APIs. The new pipeline also performs a small amount of error checking on behalf of the developer, such as detecting unanticipated calls to async void methods.
Certain features like WebSockets require that this switch be set. Importantly, the behavior of async / await is undefined in ASP.NET unless this switch has been set. (Remember: setting <httpRuntime targetFramework="4.5" /> is also sufficient.)

相較於ASP.NET 4,新一代的await-Friendly Asynchronous Pipeline、SynchronizationContext移除掉一些會相互阻擋的同步呼叫,改善舊版的一些不良行為,並加入非同步作業的防呆檢查,能提升效率及減少錯誤,WebSocket及ASP.NET async/await特性都需依賴它才能執行。

試著在web.config加入<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />,果然原先無async版的Action就能順利執行,印證的確是該設定造成差異。但由於專案需要WebSocket,故修改為async版Action是唯一解。

[2019-10-28補充] 讀者 JOE 分享一個簡便做法,在 web.config appSetting 加入 <add key="aspnet:AllowAsyncDuringSyncStages" value="true"/> 可關閉防呆檢查避開例外,但需留意允許不建議的非同步呼叫方式可能引發非預期的結果。


Comments

# by JOE

加上一條<add key="aspnet:AllowAsyncDuringSyncStages" value="true"/>就解決了,不要亂教小孩

# by Jeffrey

to JOE,感謝分享,已補充於本文。

Post a comment