昨天碰见了一个怪事,同样的一个方法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()方法如下:
{
if (this._invalidOperationEncountered || (this._disabled && (this._pendingCount == 0)))
{
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。方法体:
{
this._savedContext = HttpContextWrapper.SwitchContext(this._context);
if (setImpersonationContext)
{
this.SetImpersonationContext();
}
this._savedSynchronizationContext = AsyncOperationManager.SynchronizationContext;
AsyncOperationManager.SynchronizationContext = this._context.SyncContext;
Guid requestTraceIdentifier = this._context.WorkerRequest.RequestTraceIdentifier;
if (!(requestTraceIdentifier == Guid.Empty))
{
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)
{
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,方法执行完毕,再替换过来:
try
{
System.ComponentModel.AsyncOperationManager.SynchronizationContext = new System.Threading.SynchronizationContext();
A();
}
finally
{
System.ComponentModel.AsyncOperationManager.SynchronizationContext = context;
}
测试通过。至今天,还没发现什么问题。