做了這麼長時間的COM開發,對於COM編程中的多線程開發還是一知半解的;這也不符合天秤座的脾氣,所以,今天一定要對COM編程中的多線程好好的總結一番。
COM線程
搞Windows開發,應該熟悉Windows的線程和同步了,如果不熟悉的話,可以再去看看《Windows核心編程》。在Win32種,線程分為兩種,UI線程和工作線程;UI線程是一種與一個窗口綁定的線程,其特定是包含一個窗口、一個消息循環和一個窗口過程,由於消息循環的存在,就導致了其天生就具有一種同步機制:任何發送到該線程的消息都會被消息循環同步,不會有任何兩個或以上的消息同時被窗口過程處理,所有消息都會被消息循環串行化;工作線程則可以認為是一個函數在一個線程上的一次運行,這種線程不具備任何自帶的同步機制,如果要對兩個工作者線程實施某種同步,則只能使用Win32的同步對象,如CriticalSection或者Event等等。COM線程與Win32並沒有什麼差別,但是COM線程的難點在於參數調整和線程同步。在接著總結之前,是非常有必要對一個概念進行總結的,那就是套間。
套間
要是說套間,你肯定不明白;要是說總統套間,你一下子就明白了。這裡的套間和房子的概念差不多。它的周圍都是牆,所以當我們要進入這個房間的時候就必須使用一種手段;在這個房間裡放的就是一個或者多個COM對象。對於套間更理論的解釋是,在一個套間內可以有一個或者多個COM組件,而套間之間存在有一個明確的界限,並且套間內只存在唯一的一個套間線程,這個套間線程存在一個類似於消息循環(實際上是存在一個隱藏的窗口消息循環)來保證其天生所具有的同步性。套間就是一個只有一個主線程的Windows窗口應用程序進程。所以套間就是一個UI線程。作為UI線程,它自然就能完成同步的功能。在《COM本質論》中隊套間是這樣定義的:套間定義了一組對象的邏輯組合,這些對象共享同一組並發性和重入限制。一個線程要想使用COM,必須先進入一個套間。COM規定,只有運行在對象套間中的線程才能訪問該對象。
套間分類
COM定義了兩種類型的套間,STA(單線程套間)和MTA(多線程套間)。
STA的特點是套間內永遠只有一個線程運行,並且一定是創建該套間的初始線程。因而開發只運行在STA中的組件不需要考慮線程同步等問題。STA中包含了一個不可見的窗口,所以透過窗口的消息循環機制對消息隊列的處理,保證了同一時刻只有一個調用請求被執行,這就是組件不需要處理同步的原因。
MTA的特點是套間內可以有多個線程運行,並且可以創建新的線程。在MTA中運行的組件必須自己實現線程的同步。
套間的進入和退出
線程通過CoInitializeEx函數進入套間,該函數的第二個參數通過傳遞COINIT_APARTMENTTHREADED和COINIT_MULTITHREADED標志了套間類型。如果傳遞COINIT_APARTMENTTHREADED,該線程將創建自己私有的套間,別人不能進入;如果傳遞COINIT_MULTITHREADED,該線程將進入當前進程范圍內的MTA。線程調用CoUninitialize函數用於退出所在的套間。只有退出後才可以進入其它類型的套間。
線程模型
每個COM對象都可以決定自己運行在什麼樣的套間內,這稱為COM對象的線程模型。當我們創建ATL工程時,COM線程模型分為以下幾類:單線程(Single)、套間(Apartment)、兩者(Both)和自由(Free)。
單線程線程模型(Single)
前面講過這個模型最好是被翻譯成單套間的好,因為這種多線程模型並不是說COM組件只能被用在單線程程序裡頭的,相反組件還是可以被正常的用在多線程程序裡的。這種模型的真實意義是即使你的程序是多線程的並且在每個線程裡都調用了CoInitalize(0,COINIT_APARTMENT,事實上在你的程序進程裡頭也只創建一個套間,並且把所有的組件都放到這個套間裡頭並由這個套間所擁有的消息循環來保證同步性。
或許這樣講不全面,但是上面一段確實講了一種最簡單的情況,就是所有的組件都按Single模型來創建。如果不是這樣會什麼情況呢,舉個例子說A、B、C三個組件按Single模型創建,D按Apartment模型創建,並且四個組件分別在TA、TB、TC、TD四個線程裡創建實例(每個線程都調用CoInitalize(0,COINIT_APARTMENT)來創建環境),那麼組件A、B、C運行在由TA創建的套間裡(TB、TC都沒有創建套間,TA是這個套間的套間線程),而組件D則獨立運行在TD創建的套間裡(TD是這個套間的套間線程),這裡一共就有了兩個套間。這樣應該是完整的情況了,再復雜的情況我想你都能推出來了。
套間線程模型(Apartment)
這種模型與前一種模型很相似,可以都被認為是創建WIN32概念上的UI線程,但是不同的在於,Single模型無論你在多少個線程裡調用多少次CoInitalize(0,COINIT_APARTMENT)都只創建一個套間,套間的套間線程是你第一次調用CoInitalize(0,COINIT_APARTMENT)的線程,而Apartment模型則是你在一個線程上調用一個CoInitalize(0,COINIT_APARTMENT)就創建一個套間,並且把這個線程作為套間線程。
兩者線程模型(Both)
這種模型保證了組件即能在套間模型使用也能在自由模型使用。例如說組件自身被創建為Apartment或者Single,但是使用者用CoInitalizeEx(0,COINIT_MULITITHREAD)來創建環境,那麼COM自己會再創建一個線程用CoInitalizeEx(0,COINIT_APARTMENT)來創建環境供組件運行,反之亦然。
自由線程模型(Free)
這種模型就是工作者線程了,COM不再用消息循環來提供同步機制了。你要在多線程裡使用,OK,那你自己給他做同步機制(或者由組件開發者把組件做成線程安全的)。你要單線程裡使用,那更好無論如何都不需要同步了。
套間調度
從上面的描述可以看到在Single和Apartment這兩種模型裡頭套間內調用或者通過COM機制套間之間的通信都會被同步化,但是如何跨套間調用呢,比如說我們經常做這種事情,把一個存在在套間內組件的接口指針作為線程參數傳遞到另外一個線程中使用,或者兩個組件存在於不同的套間中,但是由於連接點或者回調的原因需要互相調用。這個時候我們就需要使用proxy/stub機制,在傳遞接口指針之前調用CoMashalInterThreadInterface這個函數來包裝接口指針給PS dll來傳遞,然後再那個調用的線程或者套間裡使用CoGetInterfaceAndReleaseStream來重新獲得被調度過的接口指針,這樣就能確保COM的線程同步機制能夠正常的運行。
總結
關於COM的多線程編程不是看完這裡的這篇文章就能全部掌握的。我在掌握這部分時是吃了很多的苦頭的,到現在也就是了解的程度。我這裡只是總結了理論上應該知道的東西。只有經過了實際項目的錘煉才能更好的去理解COM的多線程,理解多套間之間的調度關系。個人建議看看《ATL開發指南》,如果在以後的項目中進行多線程的問題時,就需要往這方面想想,這也是經驗之談,一般在這裡比較容易出問題。好了,關於COM編程系列就更新到此了。以後如果有實際的項目需要,還會在這個的基礎上繼續更新,現階段就到此結束了。COM,一個不朽的神話,也希望大家對我的總結感到滿意。