棘手問題:無法處置 BufferedGraphicsContext,因為緩衝作業正在進行中。

使用者回報一個錯誤訊息:「無法處置 BufferedGraphicsContext,因為緩衝作業正在進行中。」而且是當應用程式處理的資料量大到某個程度時才會出現。用 Google 搜尋可以找到一些討論串,但沒有標準答案。我試了幾種方法,最後雖然解決了,但卻只是避開它而已,嚴格來講,我並沒有找到問題的癥結點。底下是我處理這個問題的過程,如果您也碰到同樣的問題,我的作法也許可以提供一點靈感。

初步測試

首先,我請 user 提供他的資料檔,然後在自己的兩台 Windows Server 2003 機器上測試。結果一台會出錯,一台可順利執行。碰到這種情形,自然會懷疑是因為環境組態的差異造成。由於這是 .NET Windows Forms 應用程式,因此我檢查了兩台機器的 .NET Framework 版本,結果一樣都是 .NET Framework 2.0 with SP1,然後再比對其他用到的軟體元件,版本也都一樣。唯一的差異,似乎就只有兩台機器的記憶體容量,在 3GB RAM 的機器上測試不會出現錯誤,在 2.5GB 機器上測試則有錯誤。難道真的是因為記憶體耗盡?可是工作管理員顯示實體記憶體還有大於 1GB 的可用空間。若把處理的資料量減少,兩台機器又都可以通過測試。

簡單描述一下我的程式處理架構:程式有個 MainForm,當使用者在 MainForm 中執行某個功能時,會進行大量的資料運算及處理(這裡有頻繁、大量的配置記憶體動作);資料處理完後,會建立另外一個 ResultForm,以呈現處理的結果,而錯誤就是出現在呼叫 ResultForm 的 ShowDialog 方法的時候。

就像一個經常熬夜的人,平常都覺得沒事,等到 50 歲以後,有一天突然倒下,才發現肝臟已經嚴重纖維化了。這個問題就有點這個味道,它不像騎車跌倒那樣能夠明確知道出事的地點和原因,而是累積一段時間後,直到執行一行平淡無奇的程式碼才發生錯誤(例如 form.ShowDialog),此時就算在 exception 發生的時候查看 call stack 也沒有什麼有用的線索,這是它棘手的地方。

網路爬文

錯誤訊息的原文是「BufferedGraphicsContext cannot be disposed of because a buffer operation is currently in progress.」用 Google 可以找到一些討論串,例如微軟技術論壇這帖:http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=200483&SiteID=1,但是都沒有直接的答案。也就是說,雖然知道大概跟圖形處理有關,但錯誤發生的原因可能有好多情況。其中一個來自微軟員工的回答更令人沮喪:

This indeed seems like a bug in the framework and we at MS haven't been able to identify it since we have not been able to reproduce this problem internally - again, if someone has a consistent repro please report it to us - I don't expect this to be fixed in 3.0 .

Ok! 看起來像是 .NET 的 bug,微軟也還不知道原因(當時是 2007 年 8 月),而且就算改用 .NET 3.0 恐怕也一樣(合理,.NET 3.0 本來就只是 .NET 2.0 外加其他新元件而成的 superset,骨子裡還是 .NET 2.0)。

歸納一下從網路上爬文所得到的資訊,可以大概猜測問題很可能跟以下因素有關:
  1. 應用程式大量配置記憶體或 GDI 資源;
  2. 某個控制項使用了 double buffer 技術來加速顯示。
使用獨立的 App Domain 來隔離嫌移程式碼

既然只有大概的方向,那只好用消去法了。我將 ResultForm 上面的控制項逐一拿掉,Form_Load 事件裡面的 code 也全移除,到最後只剩下一個 3rd-party 元件:SourceGrid.Grid。如果將這個 grid 也去掉,程式就完全不會出錯。於是我看了一下 SourceGrid.Grid 的原始碼,果然,在它的基礎類別的建構元裡面有用到 double buffer:

SetStyle(ControlStyles.Selectable, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.DoubleBuffer, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.ContainerControl, true);

為了確定問題是否是 Grid 元件引起,我把 Grid 換掉,找了一個可能有用到 doubble buffer 的標準 .NET 元件取代:PictureBox。結果證明,用 .NET 標準元件也會出現同樣的錯誤。這下子更傷腦筋了。我的程式雖然處理資料時非常複雜,也需要大量的記憶體(其實也才 50MB 左右而已,視輸入的資料量而定),但並沒有用到 multi-thread,所以程式碼其實也沒有甚麼特殊的寫法。裡面用到最頻繁的,是一堆可序列化的類別,以及許多動態配置的泛型串列物件。

既然標準元件也會出問題,我只能懷疑是當程式配置了大量記憶體或 GDI 資源時,就會引發 .NET 圖形處理元件的罕見 bug 了。所以我先採取的作法,是將那些處理大量資料的 DLL 組件(姑且統稱為 HeavyTask.DLL 吧)載入到獨立的 App Domain 中執行,也就是說,利用 App Domain 的隔離特性把可疑的程式碼隔絕於主程式(預設 App Domain)之外,而且每次資料處理完就將那個新建立的 App Domain 卸載。程式碼大概像這樣:

AppDomain appdm = AppDomain.CreateDomain("HeavyTaskFacade");
IHeavyTaskFacade hvf = (IHeavyTaskFacade) appdm.CreateInstanceAndUnwrap( "HeavyTaskFacade", "HeavyTaskFacade.Manager");
hvf.DoHeavyTask(bigInputData);
AppDomain.Unload(appdm);


程式改寫之後,執行看看,結果可以順利執行 HeavyTask,程式也沒出錯了。但是我只高興了一下下而已,因為,再執行一次 HeavyTask 就又出錯了。儘管程式有卸載 App Domain,後來甚至用上 GC.Collect 和 GC.WaitForPendingFinalizers 方法,用工作管理員觀察第一次執行 HeavyTask 前後的記憶體用量,發現應用程式佔用的記憶體根本沒釋放多少。不死心,我再將程式改寫,在獨立 App Domain 中以另一條執行緒來執行 HeavyTask,看看是否有影響(前面那個寫法還是只有一條預設的執行緒,在不同 App Domain 間穿梭)。程式運作架構如下圖。



結果......還是一樣 >_<|||

採取更大的隔離範圍:process

利用 App Domain 隔離沒有用,那就試試看把 HeavyTask 改放到單獨的 process 吧。還好原本寫程式時已經元件化、模組化,程式碼要怎麼乾坤大挪移都不算太麻煩。這次是把 HeavyTask 製作成一個 console 應用程式。用這種方法,經過幾天反覆測試,都沒有再出現錯誤訊息了。

雖然還不明白癥結點究竟在哪一行程式碼或哪一個類別裡,
但問題總算是解決了......或者應該說:被隔離了。

沒有留言:

技術提供:Blogger.
回頂端⬆️