【xiaotie】Asp.Net異常Asynchronous 的解決方案

iDotNetSpace發表於2008-07-25
昨天碰見了一個怪事,同樣的一個方法A(),在控制檯程式中呼叫和在Asp.Net頁面中呼叫,結果完全不一樣。在控制檯程式中執行正常,在Asp.Net下不能得到正確結果。

    方法A()的執行過程如下:

    A()呼叫第三方庫的非同步方法B(),在非同步方法的回撥方法C()之中,又呼叫了非同步WebService Request。
 
    經過除錯發現,在C()中,吞噬了系統丟擲的異常。修改程式碼,捕獲到一個異常:

    Asynchronous operations are not allowed in this context. Page starting an asynchronous operation has to have the Async attribute set to true and an asynchronous operation can only be started on a page prior to PreRenderComplete event.

    搜尋 Google 發現,asp.net 對非同步呼叫進行了限制,如要在 Page 中進行非同步操作,需要設定Page的 Async = true; 然而,問題是在Page中觸發的僅僅是方法A,A再呼叫方法B,B的回撥則是由無名的苦工執行緒在幕後執行的,C的回撥又是一個無名的苦工執行緒在幕後執行的,兩個苦工執行緒,和Page執行緒根本不是一個執行緒,因此,在Page中設定Async=true能否影響到後臺苦工執行緒值得懷疑。
 
    在Page中加入 Async =  true,果然,還是不能得出正確結果。無奈之下,猜測能否在配置檔案中設定一項,啟用全站的非同步操作。搜尋設定項,無果,只好尋求第三條道路。
 
    仔細分析異常源,是由一個System.Web.AspNetSynchronizationContext例項的OperationStarted()方法丟擲的,在MSDN中沒搜到這個類。動用.Net Reflector,原來是一個internal類,OperationStarted()方法如下:

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--&gt public override void OperationStarted()
【xiaotie】Asp.Net異常Asynchronous 的解決方案 
{
     
if (this._invalidOperationEncountered || (this._disabled && (this._pendingCount == 0)))
【xiaotie】Asp.Net異常Asynchronous 的解決方案     
{
         
this._invalidOperationEncountered = true;
         
throw new InvalidOperationException(SR.GetString("Async_operation_disabled"));
     }

     Interlocked.Increment(
ref this._pendingCount);
 }

    持有 AspNetSynchronizationContext 的是一個靜態類 System.ComponentModel.AsyncOperationManager的靜態屬性SynchronizationContext。
 
     用reflecter檢視程式碼, SynchronizationContext的getter獲取的是當前執行緒的SynchronizationContext,它的setter存取的是當前執行緒的SynchronizationContext。也就是說,對於丟擲異常的執行緒,它的SynchronizationContext是一個AspNetSynchronizationContext例項。
 
    編寫測試程式發現,控制檯程式的SynchronizationContext使用的SynchronizationContext型別是 System.Threading.SynchronizationContext,Asp.Net程式使用的SynchronizationContext型別是System.Web.AspNetSynchronizationContext。

    .Net 程式中,WebService使用SoapHttpClientProtocol進行Soap Request,而SoapHttpClientProtocol是WebClient類的子類,在WebClient類的許多非同步方法中,都呼叫了System.ComponentModel.AsyncOperationManage.SynchronizationContext.OperationStarted()方法. 如果碰上當前執行緒應用的是AspNetSynchronizationContext,而該AspNetSynchronizationContext又不允許非同步操作,就出現了異常。
 
    瞭解到這些,解決方案呼之欲出,就是在 global.asax 裡 加上 System.ComponentModel.AsyncOperationManager.SynchronizationContext=new System.Threading.SynchronizationContext();
 
    測試一下,啊哈~~~失敗!!
 
    Page中列印出來的SynchronizationContext還是AspNetSynchronizationContext。
 
    用  Reflector 查詢 AsyncOperationManager 的被引用情況,發現方法 System.Web.HttpApplication+ThreadContext.Enter(Boolean) : Void。方法體:

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--&gtinternal void Enter(bool setImpersonationContext)
【xiaotie】Asp.Net異常Asynchronous 的解決方案 
{
     
this._savedContext = HttpContextWrapper.SwitchContext(this._context);
     
if (setImpersonationContext)
【xiaotie】Asp.Net異常Asynchronous 的解決方案     
{
         
this.SetImpersonationContext();
     }

     
this._savedSynchronizationContext = AsyncOperationManager.SynchronizationContext;
     AsyncOperationManager.SynchronizationContext 
= this._context.SyncContext;
     Guid requestTraceIdentifier 
= this._context.WorkerRequest.RequestTraceIdentifier;
     
if (!(requestTraceIdentifier == Guid.Empty))
【xiaotie】Asp.Net異常Asynchronous 的解決方案     
{
         CallContext.LogicalSetData(
"E2ETrace.ActivityID", requestTraceIdentifier);
     }

     
this._context.ResetSqlDependencyCookie();
     
this._savedPrincipal = Thread.CurrentPrincipal;
     Thread.CurrentPrincipal 
= this._context.User;
     
this.SetRequestLevelCulture(this._context);
     
if (this._context.CurrentThread == null)
【xiaotie】Asp.Net異常Asynchronous 的解決方案     
{
         
this._setThread = true;
         
this._context.CurrentThread = Thread.CurrentThread;
     }

 }

    原來SynchronizationContext在這裡被偷樑換柱了。吃飽了撐的。
 
    你不仁,我不義。你將它換過去,我就將它換過來。在頁面的Page_Load()裡面加上:
   System.ComponentModel.AsyncOperationManager.SynchronizationContext=new System.Threading.SynchronizationContext();

    測試通過。

    不過這樣做副作用還不小,比如SynchronizationContext經常會被HttpApplication替換成AspNetSynchronizationContext,在這時進行非同步操作就會出問題,再比如,使用System.Threading.SynchronizationContext而不是AspNetSynchronizationContext,會導致Asp.Net自身出現異常,昨天執行時就有一次HttpApplication拋了個異常。
 
    為減少副作用,那就在呼叫方法A()之前替換一下SynchronizationContext,方法執行完畢,再替換過來:

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--&gt            System.Threading.SynchronizationContext context = System.ComponentModel.AsyncOperationManager.SynchronizationContext;
            
try
【xiaotie】Asp.Net異常Asynchronous 的解決方案            
{
                System.ComponentModel.AsyncOperationManager.SynchronizationContext 
= new System.Threading.SynchronizationContext();
    A();
            }

            
finally
【xiaotie】Asp.Net異常Asynchronous 的解決方案            
{
                System.ComponentModel.AsyncOperationManager.SynchronizationContext 
= context;
            }

    測試通過。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-410180/,如需轉載,請註明出處,否則將追究法律責任。

相關文章