[英]Is there any way to work around OS loader lock deadlocks caused by third-party libraries?
我有一個有趣的問題,我沒有在其他地方看到過記錄(至少沒有這個具體問題)。
這個問題是COM,VB6和.NET的結合,使它們發揮得很好。
這就是我所擁有的:
是的,我知道這首先是一個冒險的事情,因為VB6對於多線程應用程序來說並不是非常友好,但不幸的是,這是我目前所困擾的。
我已經修復了許多導致代碼死鎖的事情(例如,確保COM對象實際上是從一個單獨的STA線程創建和調用的,確保在線程退出之前顯式釋放COM對象以防止垃圾收集器和COM Interop代碼之間發生的死鎖等等,但是有一個死鎖情況我似乎無法解決。
在WinDbg的幫助下,我能夠弄清楚發生了什么 ,但我不確定如何(或者是否)解決這個特定的死鎖問題。
發生了什么
如果一個請求處理程序線程正在退出,並且另一個請求處理程序線程同時啟動,則由於VB6運行時初始化和終止例程似乎工作的方式,可能會發生死鎖。
在以下情形中發生死鎖:
正在啟動的新線程正在創建(VB6)COM對象的新實例以處理傳入請求。 此時,COM運行時正在調用以檢索對象的類工廠。 類工廠實現在VB6運行時本身( MSVBVM60.dll )中。 也就是說,它調用VB6運行時的DllGetClassObject函數。 反過來,它調用內部運行時函數( MSVBVM60!CThreadPool::InitRuntime
),它獲取互斥鎖並進入臨界區以完成其部分工作。 此時,它將調用LoadLibrary將oleaut32.dll加載到進程中,同時保留此互斥鎖。 所以,現在它持有這個內部VB6運行時互斥鎖並等待OS加載程序鎖定。
正在退出的線程已經在加載程序鎖定內部運行,因為它已完成執行托管代碼並在KERNEL32!ExitThread函數內執行。 具體來說,它處於該線程上處理MSVBVM60.dll的DLL_THREAD_DETECH
消息的中間,該消息又調用一個方法來終止線程上的VB6運行時( MSVBVM60!CThreadPool::TerminateRuntime
)。 現在,該線程嘗試獲取與已初始化的其他線程已具有的相同的互斥鎖。
經典的僵局。 線程A具有L1並且想要L2,但是線程B具有L2並且需要L1。
問題(如果你跟着我這么遠)是我無法控制VB6運行時在其內部線程初始化和拆卸例程中正在做什么。
理論上,如果我可以強制VB6運行時初始化代碼在OS加載程序鎖內運行,我會阻止死鎖,因為我相當確定VB6運行時所持有的互斥鎖是專門用於初始化和終止例程。
要求
我不能從單個STA線程進行COM調用,因為那時服務將無法處理並發請求。 我不能長時間運行請求阻止其他客戶端請求。 這就是我為每個請求創建一個STA線程的原因。
我需要在每個線程上創建一個COM對象的新實例,因為我需要確保每個實例在VB6代碼中都有自己的全局變量副本(VB6為每個線程提供了自己的所有全局變量的副本)。
我試過的解決方案沒有用
將ActiveX DLL轉換為ActiveX EXE
首先,我嘗試了明顯的解決方案並創建了一個ActiveX EXE(進程外服務器)來處理COM調用。 最初,我編譯它,以便為每個傳入的請求創建一個新的ActiveX EXE(進程),我也嘗試使用Thread Per Object編譯選項(創建一個流程實例,並在一個新的線程內創建每個對象) ActiveX EXE)。
這解決了VB6運行時的死鎖問題,因為VB6運行時永遠不會被正確加載到.NET代碼中。 但是,這導致了一個不同的問題:如果並發請求進入服務,則ActiveX EXE會隨着RPC_E_SERVERFAULT
錯誤而隨機失敗。 我假設這是因為COM編組和/或VB6運行時無法處理ActiveX EXE中的並發對象創建/銷毀或並發方法調用。
強制VB6代碼在OS加載程序鎖內運行
接下來,我切換回使用ActiveX DLL作為COM類。 為了強制VB6運行時在OS加載程序鎖內運行其線程初始化代碼,我創建了一個本機(Win32)C ++ DLL,其代碼用於處理DllMain中的 DLL_THREAD_ATTACH
。 DLL_THREAD_ATTACH
代碼調用CoInitialize然后實例化一個虛擬VB6類以強制加載VB6運行時並強制運行時初始化例程在該線程上運行。
當Windows服務啟動時,我使用LoadLibrary將此C ++ DLL加載到內存中,以便該服務創建的任何線程都將執行該DLL的DLL_THREAD_ATTACH
代碼。
問題是這個代碼運行服務創建的每個線程,包括.NET垃圾收集器線程和異步網絡代碼使用的線程池線程,這不會很好地結束(這似乎導致線程永遠不會正確啟動,我想在GC上初始化COM和線程池線程通常只是一個非常糟糕的主意)。
附錄
我剛才意識到為什么這是一個壞主意(可能是它不起作用的部分原因):當你持有加載器鎖時調用LoadLibrary是不安全的。 請參閱此MSDN文章中的“ 備注”部分: http : //msdn.microsoft.com/en-us/library/ms682583%28VS.85%29.aspx ,具體為:
DllMain中的線程保持加載程序鎖定,因此不能動態加載或初始化其他DLL。
有沒有辦法解決這些問題?
所以,我的問題是,有沒有辦法解決原始的死鎖問題?
我能想到的唯一另一件事是創建我自己的鎖對象並包圍在.NET lock
塊中實例化COM對象的代碼,但是我無法(我知道)將相同的鎖放在(操作系統的)線程退出代碼。
這個問題是否有更明顯的解決方案,或者我在這里運氣不好?
只要所有模塊在一個進程中工作,您就可以通過用包裝器替換一些系統調用來掛鈎Windows API。 然后,您可以將調用包裝在單個臨界區中以避免死鎖。
有幾個庫和樣本可以實現這一點,該技術通常被稱為繞行:
http://www.codeproject.com/Articles/30140/API-Hooking-with-MS-Detours
http://research.microsoft.com/en-us/projects/detours/
當然,包裝器的實現應該在本機代碼中完成,最好是C ++。 .NET繞道也適用於高級API函數(如MessageBox) ,但如果您嘗試在.NET中重新實現LoadLibrary API調用,則可能會出現循環依賴性問題,因為.NET運行時在執行期間內部使用LoadLibrary函數並經常執行此操作。
所以解決方案對我來說就是這樣的:一個單獨的.DLL模塊,它在應用程序的最開始加載。 該模塊通過使用您自己的包裝器修補幾個VB和Windows API調用來修復死鎖問題。 所有包裝器都做一件事:在關鍵部分包裝調用並調用原始API函數來完成實際工作。
我沒有看到任何原因導致您無法在啟動代碼中加載ActiveX控件的額外實例,只是掛起參考。 Presto,由於VB6運行時永遠不會關閉,因此不再出現加載程序鎖定問題。
編輯:回想起來,我認為這不會起作用。 問題是死鎖可能在Win32線程退出的任何時候發生,並且由於Win32線程沒有將1:1映射到.NET線程,我們不能(在.NET內)強制Win32線程獲取鎖之前退出。 除了退出的.NET線程切換到不同的OS線程的可能性之外,可能還有與任何.NET線程(垃圾收集等)無關的OS線程,它們可能隨機啟動和退出。
我能想到的唯一另一件事是創建我自己的鎖對象並包圍在.NET鎖定塊中實例化COM對象的代碼,但是我無法(我知道)將相同的鎖放在(操作系統的)線程退出代碼。
這聽起來像一個很有前途的方法 我從中收集到您可以修改服務的代碼,並且您說每個線程在退出之前顯式釋放COM對象,因此可能您可以在此時聲明鎖定,或者在顯式釋放COM對象之前或之后。 秘訣是選擇一種在持有它的線程退出后隱式釋放的鎖,例如Win32互斥鎖 。
在線程完成所有DLL_THREAD_DETACH調用之前,Win32互斥對象可能不會被放棄,盡管我不知道是否記錄了此行為。 我不熟悉在.NET中的鎖定,但我的猜測是它們不太適合,因為即使存在正確的鎖定,一旦線程到達結束,它可能會被認為是放棄了。托管代碼部分,即在調用DLL_THREAD_DETACH之前。
如果Win32互斥對象不起作用(或者如果您非常合理地不依賴於未記錄的行為),則可能需要自己實現鎖定。 一種方法是使用OpenThread獲取當前線程的句柄,並將其與事件或類似對象一起保存在鎖定對象中。 如果已聲明鎖定並且您希望等待它可用,請使用WaitForMultipleObjects等待直到線程句柄或事件發出信號。 如果事件被發出信號,則表示已經顯式釋放了鎖,如果線程句柄被發出信號,則它被出線的線程隱式釋放。 顯然,實現這一點涉及許多棘手的細節(例如:當一個線程顯式釋放鎖時,你無法關閉線程句柄,因為另一個線程可能正在等待它,所以你必須在鎖定時關閉它接下來聲明了,但要解決這些問題應該不會太難。
大約20年前,我用VB6,VC6編寫了一個相當復雜的代碼,我需要將它移植到visual studio.net。 我只是把我寫的函數和頭文件一起修改了所有的編譯錯誤(很多),然后嘗試加載它。 得到“裝載機鎖閉”然后我決定重做所有文件,從少數其他文件所依賴的文件開始,然后按照我的方式工作,當我去的時候,我只包含那個特定文件所需的頭文件。 它加載的結果現在很好。 沒有更多的裝載機關閉。 對我來說,教訓是不要在特定的cpp文件中包含比絕對必要的更多的頭文件。 希望這可以幫助
來自一個非常開心的露營者!
大衛
由於我還在探索我的選擇,為了簡單起見,我還想看看我是否可以在不使用任何本機代碼的情況下在純.NET代碼中實現解決方案。 我不確定這是否是一個萬無一失的解決方案,因為我還在試圖弄清楚它是否真的給了我需要的互斥,或者它是否只是看起來像它。
歡迎提出任何想法或意見。
代碼的相關部分如下。 一些說明:
HandleRpcRequest
方法 DbRequestProxy
是我正在使用的真正COM類的瘦包裝類 ManualResetEvent
( _safeForNewThread
)來提供互斥。 基本思想是,如果任何一個特定線程即將退出(因此可能即將終止VB6運行時),此事件將保持無信號(阻止其他線程)。 僅在當前線程完全終止后(在Join
調用完成后)再次發出該事件。 這樣,除非現有線程正在退出,否則多個請求處理程序線程仍可以並發執行。 到目前為止,我認為這段代碼是正確的,並且保證兩個線程不再在VB6運行時初始化/終止代碼中死鎖,同時仍然允許它們在大部分執行時間內並發執行,但我可能在這里遺漏了一些東西。
public class ClientHandler {
private static ManualResetEvent _safeForNewThread = new ManualResetEvent(true);
private void HandleRpcRequest(string request)
{
Thread rpcThread = new Thread(delegate()
{
DbRequestProxy dbRequest = null;
try
{
Thread.BeginThreadAffinity();
string response = null;
// Creates a COM object. The VB6 runtime initializes itself here.
// Other threads can be executing here at the same time without fear
// of a deadlock, because the VB6 runtime lock is re-entrant.
dbRequest = new DbRequestProxy();
// Call the COM object
response = dbRequest.ProcessDBRequest(request);
// Send response back to client
_messenger.Send(Messages.RpcResponse(response), true);
}
catch (Exception ex)
{
_messenger.Send(Messages.Error(ex.ToString()));
}
finally
{
if (dbRequest != null)
{
// Force release of COM objects and VB6 globals
// to prevent a different deadlock scenario with VB6
// and the .NET garbage collector/finalizer threads
dbRequest.Dispose();
}
// Other request threads cannot start right now, because
// we're exiting this thread, which will detach the VB6 runtime
// when the underlying native thread exits
_safeForNewThread.Reset();
Thread.EndThreadAffinity();
}
});
// Make sure we can start a new thread (i.e. another thread
// isn't in the middle of exiting...)
_safeForNewThread.WaitOne();
// Put the thread into an STA, start it up, and wait for
// it to end. If other requests come in, they'll get picked
// up by other thread-pool threads, so we won't usually be blocking anyone
// by doing this (although we are blocking a thread-pool thread, so
// hopefully we don't block for *too* long).
rpcThread.SetApartmentState(ApartmentState.STA);
rpcThread.Start();
rpcThread.Join();
// Since we've joined the thread, we know at this point
// that any DLL_THREAD_DETACH notifications have been handled
// and that the underlying native thread has completely terminated.
// Hence, other threads can safely be started.
_safeForNewThread.Set();
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.