簡體   English   中英

有沒有辦法解決由第三方庫引起的OS加載程序鎖定死鎖?

[英]Is there any way to work around OS loader lock deadlocks caused by third-party libraries?

我有一個有趣的問題,我沒有在其他地方看到過記錄(至少沒有這個具體問題)。

這個問題是COM,VB6和.NET的結合,使它們發揮得很好。

這就是我所擁有的:

  • 遺留的VB6 ActiveX DLL(由我們編寫)
  • 用C#編寫的多線程Windows服務,通過網絡處理來自客戶端的請求並發回結果。 它通過創建一個新的STA線程來處理每個請求。 每個請求處理程序線程實例化一個COM對象(在ActiveX DLL中定義)來處理請求並獲得結果(傳入一串XML,然后返回一個XML字符串),顯式釋放COM對象,以及退出。 然后,該服務將結果發送回客戶端。
  • 使用異步網絡(即線程池線程)處理所有網絡代碼。

是的,我知道這首先是一個冒險的事情,因為VB6對於多線程應用程序來說並不是非常友好,但不幸的是,這是我目前所困擾的。

我已經修復了許多導致代碼死鎖的事情(例如,確保COM對象實際上是從一個單獨的STA線程創建和調用的,確保在線程退出之前顯式釋放COM對象以防止垃圾收集器和COM Interop代碼之間發生的死鎖等等,但是有一個死鎖情況我似乎無法解決。

在WinDbg的幫助下,我能夠弄清楚發生了什么 ,但我不確定如何(或者是否)解決這個特定的死鎖問題。

發生了什么

如果一個請求處理程序線程正在退出,並且另一個請求處理程序線程同時啟動,則由於VB6運行時初始化和終止例程似乎工作的方式,可能會發生死鎖。

在以下情形中發生死鎖:

  • 正在啟動的新線程正在創建(VB6)COM對象的新實例以處理傳入請求。 此時,COM運行時正在調用以檢索對象的類工廠。 類工廠實現在VB6運行時本身( MSVBVM60.dll )中。 也就是說,它調用VB6運行時的DllGetClassObject函數。 反過來,它調用內部運行時函數( MSVBVM60!CThreadPool::InitRuntime ),它獲取互斥鎖並進入臨界區以完成其部分工作。 此時,它將調用LoadLibraryoleaut32.dll加載到進程中,同時保留此互斥鎖。 所以,現在它持有這個內部VB6運行時互斥鎖並等待OS加載程序鎖定。

  • 正在退出的線程已經在加載程序鎖定內部運行,因為它已完成執行托管代碼並在KERNEL32!ExitThread函數內執行。 具體來說,它處於該線程上處理MSVBVM60.dllDLL_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方法
  • 這將觸發一個單獨的STA線程,以便它可以安全地進行COM調用
  • 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM