翻譯:理解COM套間(第一部分)

 

最近在寫一個 oledb provider,涉及到線程套間的問題,搜到下面的文章,感覺說的透徹,轉了過來。

這個oledb provider是爲了在asp.net程序中供ado.net使用,通過System.Threading.Thread.CurrentThread.ApartmentState可以看到asp.net程序默認是MTA,考慮oledb provider可能也會在STA中使用,因此最好設置oledb provider的線程套間模型爲ThreadingModel=Both, 這樣在STA或者MTA環境,都可以避免散集列集操作。這樣就必須要保證oledb provider中的每個對象都必須是線程安全的,導致了程序的複雜性。

轉自:http://blog.sina.com.cn/s/blog_56dee71a0100nt08.html 

英文原版:http://www.codeguru.com/cpp/com-tech/activex/apts/article.php/c5529/Understanding-COM-Apartments-Part-I.htm

  

COM引入了一種併發機制,可以截獲並串行化對於設計爲只能一次處理一個方法調用的對象的併發調用。這種機制以稱爲“套間(apartments)”的抽象邊界概念爲中心。在解決不能正確工作的COM系統的問題時,我發現大約40%問題的原因是缺乏對於套間的理解。這種知識的缺乏並不意外,因爲套間是COM中最複雜的領域,而且也沒有很好地文檔化。微軟的目的是好的,但是在Windows NT 3.51中爲COM引入套間的時候,他們爲粗心的開發者建立了一個雷區。遵守規則就可以避免踩到地雷,但是如果不知道規則是什麼,就很難遵守規則。

本文是一個兩部分系列文章的第一部分。系列文章將解釋什麼是套間、套間存在於什麼地方,以及如何避免套間引入的問題。文章的第一部分將介紹COM基於套間的併發機制;第二部分將介紹一些規則,以避免隱藏而又令人討厭的Bug

 

套間基礎

套間是一個併發邊界,一個在對象和客戶線程之間的假想的盒子,用以隔離具有不兼容線程特性的COM客戶和COM對象。套間存在的主要目的是讓COM可以串行化對於非線程安全對象的方法調用。如果沒有告訴COM對象是線程安全的,則COM不會允許多個調用同時到達對象。相反地,如果告訴COM對象是線程安全的,則COM會讓對象處理多個線程中的併發調用。

每個使用COM的線程,以及這些線程創建的每個對象,都被分配到某個套間中。套間不能跨越進程邊界,所以如果對象和其客戶位於不同的進程中,則它們也在不同的套間中。客戶創建進程內對象的時候,COM必須決定將其放在創建者套間中,還是放在客戶進程中的另一個套間裏。如果COM將對象和創建對象的線程放在同一個套間中,則客戶將直接訪問對象。如果COM將對象放在另一個不同的套間中,則創建對象的線程對對象的調用將被列集(marshaled)

1展示了線程和對象共享套間,以及線程和對象位於不同套間時二者的關係。線程1中的調用將直接訪問線程創建的對象;線程2中的調用將通過代理(proxy)和樁基(stub)進行。COM在將接口指針列集到線程2的套間時創建代理/樁基對。跨越套間邊界傳遞接口指針時必須對指針進行列集,這是一條規則。這意味着,如果涉及到定製接口,即使是進程內對象,如果需要與位於其他套間中的客戶通信,也還是需要提供與跨進程和跨機器方法調用時一樣的,用於列集支持的代理/樁基DLL(或者選擇類型庫列集時的類型庫)。

翻譯:理解COM套間(第一部分)

1:對其他套間中對象的調用被列集,即使對象和調用者屬於同一個進程

Windows NT 4.0支持兩種類型的套間,Windows 2000則支持三種類型。這三種類型的套間是:

l 單線程套間(Single-threaded apartmentsSTA)(Windows NT 4.0Windows 2000

l 多線程套間(Multithreaded apartmentsMTA)(Windows NT 4.0Windows 2000

l 線程中立套間(Neutral-threaded apartmentsNTA)(僅Windows 2000

 

每個線程只能有一個單線程套間,但是可以容納的對象個數是無限的。而且,COM不限制進程中STA的個數。進程中第一個創建的STA稱爲進程的主STA。對STA中對象的調用在投遞前都先傳遞到STA線程中。因爲所有對對象的調用都在同一個線程中執行,所以基於STA的對象不能同時執行多個調用。COM使用STA來串行化對於非線程安全對象的調用。如果不明確告訴COM對象是線程安全的,則COM將把對象放到STA中,從而讓對象不會被併發訪問。

關於STA操作的一個有趣方面是:COM如何將對基於STA的對象的調用傳遞到STA線程中。COM創建STA的時候,會同時創建一個隱藏窗口,這個窗口的窗口過程知道如何處理代表方法調用的私有消息。目標是STA的方法調用離開COMRPC通道時,COM將向STA窗口投遞一個代表這個調用的消息。STA中的線程收到消息後,將消息分發到隱藏窗口,隱藏窗口的窗口過程將調用投遞給樁基,樁基將最終執行對對象的調用。因爲線程一次只能接收、分發和處理一個消息,STA自然而有效地成爲調用串行機制。如圖2所示,如果同時發起n個對基於STA的對象的調用,則調用將被排隊,然後依次投遞給對象。

翻譯:理解COM套間(第一部分)

2:進入STA的調用被轉化爲消息,投遞到消息隊列。消息隊列中的消息被STA中運行的線程依次轉化回方法調用。

調用離開STA時的情形也同樣重要。COM不能讓線程阻塞在RPC通道中,因爲回調可能導致死鎖:想象一下當STA線程調用其他套間中的對象,而這個對象反過來調用STA中的另一個對象時會發生什麼。如果STA線程阻塞在RPC通道中,則調用永遠不會返回,因爲唯一一個可以處理回調的線程正在RPC通道中等待最初的調用返回。因此,調用離開STA時,COM會阻塞STA線程,但是讓STA線程仍然可以處理回調。爲了讓回調可以發生,COM會跟蹤每個方法調用的因果關係,以便能夠識別何時應該釋放正在RPC通道中等待某方法調用返回的STA線程,讓其處理另一個進入的調用。默認情況下,STA入口有調用到達時,如果STA線程正在等待出調用返回,而且到達的入調用與正在等待返回的出調用不屬於同一個因果鏈,則到達的入調用將阻塞。編寫消息過濾器可以改變這個默認行爲,下一部分對此進行討論。

 

多線程套間完全不同。COM限制每個進程只能有一個MTA,但是沒有限制MTA中線程和對象的個數。MTA沒有隱藏窗口和消息隊列。對MTA中對象的調用被隨機地傳遞給RPC線程池裏的線程,不會串行化(見圖3)。這意味着MTA中的對象最好是線程安全的,因爲沒有外部機制保證基於MTA的對象一次只接收一個調用,對象可能被不同的RPC線程併發地調用。

翻譯:理解COM套間(第一部分)

3:進入MTA的調用被傳遞到RPC線程而不會被串行化

對於離開MTA的調用,COM不會進行特別處理。調用線程可以阻塞在RPC通道中,如果發生回調,不會產生死鎖,因爲回調會被傳遞給另一個RPC線程。

 

Windows 2000引入了第三種套間類型:線程中立套間,即NTACOM限制每個進程只能有一個NTA。不會給NTA分配線程;NTA僅用於容納對象。對基於NTA的對象的調用不會引起線程切換,調用線程會進入NTA。也就是說,從STA或者MTA發起對同一個進程中NTA的調用時,調用線程會暫時離開其套間,直接執行NTA中的代碼。這和基於STAMTA的對象是不同的:其他套間對基於STAMTA的對象的調用總是會導致線程切換的。在不同套間之間列集調用的時候,線程切換會佔用大量開銷。排除線程切換會改進性能,所以NTA是讓跨套間方法調用執行更高效的優化。Windows 2000支持基於活動的外部同步機制,可以指定是否串行化對基於NTA的對象的調用。基於活動的串行化比基於消息的串行化更有效,它可以在對象到對象級別進行。

 

2如何爲線程分配套間

以任何方式使用COM的線程必須首先調用CoInitialize或者CoInitializeEx初始化COM。調用這兩個函數時,線程將被放入到套間中。放入到什麼類型的套間決定於線程調用哪個函數以及如何調用。

l 如果線程調用CoInitialize,則COM創建新的STA並且將線程放入其中:

翻譯:理解COM套間(第一部分)

l 如果線程調用CoInitializeEx並且傳遞參數COINIT_APARTMENTTHREADED,則線程也被放入到一個STA中:

翻譯:理解COM套間(第一部分)

l 如果調用CoInitializeEx並且傳遞參數COINIT_MULTITHREADED,則線程被放入到進程裏唯一的MTA中:

翻譯:理解COM套間(第一部分)

  從大的範圍來看,進程的套間配置取決於進程中的線程如何調用CoInitialize[Ex]。存在不調用CoInitialize[Ex]函數而COM創建套間的情況,但是爲了讓問題簡單,暫時不討論這種情況(but for now we won't muddy the water by considering such circumstances)。

作爲一個例子,假設啓動了一個新進程,進程中的線程1調用CoInitialize

翻譯:理解COM套間(第一部分)

隨後,線程1啓動線程2,3,45,這些線程使用下列語句初始化COM

翻譯:理解COM套間(第一部分)

4展示了最終的套間配置。線程1,25被分配到單獨的STA中,因爲每個STA中只能有一個線程。另一方面,線程34被分配到進程的MTA中。記住:COM不會爲進程創建多個MTA,而是在唯一的一個MTA中放置任何數量的線程。

翻譯:理解COM套間(第一部分)

4:進程有五個線程,分佈於三個STA和一個MTA中。

如果你喜歡刨根問底(if you're a nuts and bolts person),你可能好奇地想知道套間的物理性質,也就是COM如何在內部表示套間。創建新套間時,COM會在堆上分配套間對象,並且用套間ID、套間類型等重要信息進行初始化。爲線程分配套間的時候,COM會在線程本地存儲(TLS)中記錄相應的套間對象的地址。因此,COM在線程中執行時,如果想知道線程是否屬於某個套間,只需要在線程本地存儲中查找套間對象的地址就可以了。

 

如何爲進程內對象分配套間

現在該介紹如何爲對象分配套間了。COM用於決定在哪個套間中創建對象的算法對於進程內對象和進程外對象是不同的。進程內對象更有趣,因爲只有進程內對象是可以創建於創建者套間中的。我們首先討論進程內對象,然後討論進程外對象。

COM通過從註冊表中讀取對象的ThreadingModel值來決定在哪個套間中創建進程內對象。ThreadingModel是分配給用以標識對象DLLInprocServer32子鍵的命名值。下面以REGEDIT格式顯示的註冊表條目標識了CLSID99999999-0000-0000-0000-111111111111DLLMyServer.dllThreadingModelApartment的對象:

翻譯:理解COM套間(第一部分)

ApartmentWindows NT 4.0支持的四種線程模型之一,也是Windows 2000支持的五種線程模型之一。五種線程模型以及支持線程模型的操作系統如下:

翻譯:理解COM套間(第一部分)

Apartment Type列指示COM如何處理具有指定ThreadingModel值的對象。比如說,COM限制沒有ThreadingModel值(ThreadingModel=None)的對象位於進程的主STA中;ThreadingModel=Apartment允許在任何STA(不僅僅是主STA)中創建對象;ThreadingModel=Free限制對象在MTA中;ThreadingModel=Neutral限制對象在NTA中;ThreadingModel=BothCOM可以在STA或者MTA中創建對象。

COM儘量放置進程內對象到創建者線程所屬的套間中。比如說,如果STA線程創建標識爲ThreadingModel=Apartment的對象,COM將在創建者線程的STA中創建對象。如果MTA線程創建ThreadingModel=Free的對象,COM將會把對象放置在MTA中。然而,有時候COM不能將對象放置在創建者的套間中。比如說,如果STA線程創建標識爲ThreadingModel=Free的對象,則對象將在進程的MTA中創建,創建者線程將通過代理和樁基訪問對象。類似地,如果MTA線程創建ThreadingModel=None或者ThreadingModel=Apartment的對象,則來自創建者線程的調用將從MTA列集到對象的STA中。下表顯示了STAMTA線程創建具有任何有效的ThreadingModel值(或者沒有ThreadingModel值)的對象時的情況:

翻譯:理解COM套間(第一部分)

爲什麼ThreadingModel=None限制對象在進程的主STA中?因爲只有這樣COM才能在不知道對象是否是線程安全時讓多個對象安全地執行。假設從同一個DLL創建兩個ThreadingModel=None的對象。如果這兩個對象訪問DLL中的任何全局變量,則COM必須在相同線程中執行所有對這兩個對象的調用,否則兩個對象可能試圖同時讀或者寫同一個全局變量。限制對象在主STA中就是COM讓對象在相同線程中執行的方式。

線程模型對於編碼有重要的指導意義。比如說,標記爲ThreadingModel=Free或者ThreadingModel=Both的對象必須是線程安全的,因爲對基於MTA的對象的調用時不會被串行化。即使是ThreadingModel=Apartment的對象,也應該是部分線程安全的,因爲ThreadingModel=Apartment不能阻止從同一個DLL創建多個對象,從而在共享數據上發生衝突。本文的下一部分將討論這個問題。

 

如何爲進程外對象分配套間

進程外對象沒有ThreadingModel值,因爲COM使用完全不同的算法爲進程外對象分配套間。簡而言之,COM將進程外對象放到與創建對象的服務器進程相同的套間中。大多數進程外(EXECOM服務器以調用CoInitialize或者CoInitializeEx將主線程放入到STA中開始。然後服務器爲其可以創建的對象類型創建類對象並使用CoRegisterClassObject進行註冊。激活請求到達以這種方式初始化的服務器時,請求在進程的STA中被處理。結果,服務器進程創建的對象也在進程的STA中。

可以將進程外對象移動到MTA中,只要將註冊類對象的線程放置在MTA中就可以了。這樣進入的激活請求會到達在服務器進程的MTA中執行的RPC線程。爲響應請求創建的對象也將位於MTA中。

要點是,在EXE類型的COM服務器裏,調用CoRegisterClassObject的線程所在的套間,也是服務器創建的對象所在的套間。當然也存在例外:使用ATLCComAutoThreadModuleCComClassFactoryAutoThread編寫的EXE COM服務器將在服務器進程中創建多個STA,在其中均勻地分佈對象。不過這隻佔現存的EXE COM服務器的很小一部分,可以認爲是例外情況,而不是通常的規則。

 

繼續下一部分

這就完了?本文展示的很多細節看起來很神祕,沒什麼實際價值。然而,要避免大多數常見的、折磨着COM程序員的危險陷阱,理解COM套間絕對是必要的。在下一部分你會明白我的意思的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章