由於項目需要,剛才打算為ASP.NET MVC應用程式增強ControllerFactory的功能,因此翻出了ASP.NET MVC的原始碼開始閱讀其DefaultControllerFactory。代碼不多,很容易理解,不過讀著讀著便發現了問題,因為我發現DefaultControllerFactory不是安全執行緒的。
安全執行緒,故名思義便是在多線程的環境下,是否可以正常工作的意思。以前也看過ASP.NET MVC的原始碼,印象中在預設情況下,整個應用程式會共用同一個DefaultControllerFactory執行個體(剛才又確認了一下的確是這樣的),這便要求這個類的所有操作——至少是對外公開的介面都需要是安全執行緒的。
安全執行緒與否有時候並不容易發現,但是某些狀況下我們還是可以總結出一些“實現模式”,遵循或違反這些模式,則這個組件很可能不是安全執行緒的。例如,如果滿足了Share Nothing,至少是No Shared Writing,這個組件很可能就是安全執行緒的。不過DefaultControllerFactory可不是這樣,例如它的CreateController方法:
public virtual IController CreateController(RequestContext requestContext, string controllerName) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } if (String.IsNullOrEmpty(controllerName)) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); } RequestContext = requestContext; Type controllerType = GetControllerType(controllerName); IController controller = GetControllerInstance(controllerType); return controller;}
請看這行紅色代碼,這是一句為自身執行個體屬性RequestContent賦值的語句,它自然是要被接下來的GetControllerType和GetControllerInstance方法所訪問。當我們發現了類似的“寫屬性”的語句應該有所警覺,因為它往往意味著這個組件不是安全執行緒的。如果是安全執行緒的代碼,則往往需要將資料統統作為方法的參數進行傳遞。
那麼,既然DefaultControllerFactory不是安全執行緒的,那麼為什麼在使用過程中卻沒有發生任何問題呢?幸運的是,在普通使用過程中,我們幾乎無法遇到這樣的邏輯。例如在GetControllerType中,對RequestContext屬性讀取的邏輯是這樣的:
protected internal virtual Type GetControllerType(string controllerName) { ... // first search in the current route's namespace collection object routeNamespacesObj; Type match; if (RequestContext != null && RequestContext.RouteData.DataTokens.TryGetValue("Namespaces", out routeNamespacesObj)) { ... } ...}
RequestContext永遠不可能為null,而且我也從來沒有見過誰向RouteData中的DataTokens集合中填充過資料(這需要在應用程式一開始進行Route Mapping時指定),因此這段邏輯“永遠”是略過的。GetControllerInstance方法情況略有不同:
protected internal virtual IController GetControllerInstance(Type controllerType) { if (controllerType == null) { throw new HttpException(404, String.Format( CultureInfo.CurrentUICulture, MvcResources.DefaultControllerFactory_NoControllerFound, RequestContext.HttpContext.Request.Path)); } ...}
只有在ControllerType為null的時候,為了拋出一個404異常才會涉及到ReqeustContext屬性——只是,這時候又有誰去注意這個呢?
就是這樣,這個問題被“幸運”地繞開了。
如果您覺得這並不是什麼嚴重的問題,自然可以放任不管。如果您覺得這個問題需要解決的話,其實也只需要在Application Start時加入這樣一段話就可以了:
ControllerBuilder.Current.SetControllerFactory(typeof(DefaultControllerFactory));
於是在每次請求時,ControllerBuilder都會使用Activator.CreateInstance來建立一個新的對象,這樣就不會出現多線程方面的問題了。只可惜,ControllerBuilder在預設情況下使用的是另一種方式:
public class ControllerBuilder { ... public ControllerBuilder() { SetControllerFactory(new DefaultControllerFactory() { ControllerBuilder = this }); } ...}
由於使用了SetControllerFactory方法的另一個重載並直接提供了一個DefaultControllerFactory執行個體,因此每個請求都會共用同一個對象了。
在修改了代碼之後,我們每個請求都會建立一個新的DefaultControllerFactory執行個體,但是由於它的緩衝(儲存了controller名稱與特定類型的對應關係)容器是static的,您也無需擔心效能上的問題。
值得一提的是,這個問題已經在ASP.NET MVC 2 Preview 1中修複了,而它的做法是將requestContext對象作為參數傳遞到GetControllerType和GetControllerInstance方法中去,這自然就沒有問題了:
public virtual IController CreateController(RequestContext requestContext, string controllerName) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } if (String.IsNullOrEmpty(controllerName)) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); } Type controllerType = GetControllerType(requestContext, controllerName); IController controller = GetControllerInstance(requestContext, controllerType); return controller;}
產生這個的原因,我只能猜想微軟沒有把Code Review的工作進行到位了,我不相信這麼明顯的問題微軟的大牛們會發現不了。什嗎?您說單元測試?這其實才是我打算和大家討論的東西。對於多線程的應用程式,我們如何能夠通過單元測試來驗證一個組件是否有安全執行緒方面的問題呢?我也相信微軟為這部分有問題的代碼編寫了單元測試(雖然我沒有去找過),但是要知道目前所有的單元測試都是單線程啟動並執行,而在單線程的情況下是無法讓安全執行緒問題暴露出來的。更有意思的是,即使是線程不安全的代碼,在多線程的環境下,也有可能工作正常。
那麼,我們又該怎麼辦呢?大家一起討論一下吧。