關於註冊表組件線程設置

 

       技術總監問我,在註冊表關於組件多線程設置,在服務器找到已註冊dll發現ThreadingModel鍵值,真不知什麼意思,以下文章我看了真不懂,反正發現值是free,是支持多線程,以下備忘留用。

 

 

 

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

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

關於COM組件線程模型的實驗

 

 

線程模型是COM組件很重要而又不易理解的一個屬性。本文嘗試用簡潔明瞭的描述和簡單的實際例子來介紹COM組件的線程模型。

1 套間

套間,英文爲apartment,有的地方譯作“套間”;有的譯作“公寓”;還有的譯作“單元”。本文采用“套間”這種譯法。

套間是與COM組件密切相關的一個概念。Windows程序中,每個使用COM的線程都屬於某個套間。套間是COM組件運行的邏輯線程上下文,決定了COM基礎設施對位於其中的COM組件實施怎樣的保護,提供怎樣的同步服務。有三種套間類型:

l 單線程套間(STA,Single Thread Apartment)

l 多線程套間(MTA,Multiple Threads Apartment)

l 線程中立套間(TNA,Thread Neutral Apartment)

每個進程可以有多個STA,但是只能有一個MTA,也只能有一個TNA。線程調用CoInitializeEx()初始化COM基礎設施時,第二個參數決定線程所屬的套間類型:

l 如果第二個參數中帶有COINIT_APARTMENTTHREADED標誌,則COM基礎設施爲線程創建一個新的STA,線程將屬於這個新創建的STA。如果這是進程中的第一個STA,則這個STA是進程的主STA。每個STA中有且只有一個線程。

l 如果第二個參數中帶有COINIT_MULTITHREADED標誌,則線程屬於MTA。因爲每個進程只能有一個MTA,所以所有MTA線程都在同一個MTA中運行。

l 線程中立套間中沒有線程,也不是由線程創建的。COM基礎設施爲所有采用線程中立模型的組件創建線程中立套間。

也可以用CoInitialize()初始化COM基礎設施,這個函數將爲線程創建新的STA。

使用OLE功能的線程需要調用OleInitialize()初始化COM基礎設施,這個函數將爲線程創建新的STA。

一般而言,對於單線程套間,由於套間中只有一個線程在運行,所以不需要對其中的組件實施保護;而對於多線程套間,由於套間中有多個線程在運行,所以需要控制多個線程對組件的併發訪問,也就是組件開發者必須編寫代碼處理多線程併發訪問帶來的同步問題。線程中立套間中雖然沒有線程,但是任何線程可以自由地、直接地訪問套間中的組件,所以也需要編寫代碼處理多線程併發訪問帶來的同步問題。

跨越套間的方法調用不能直接進行,而必須通過代理/樁基(proxy/stub)間接進行,這是COM編程的一個基本原則。所以,不能在不同套間直接傳遞接口指針,而必須通過列集/散集(marshal/unmarshal)間接傳遞。列集/散集過程就是讓COM基礎設施創建用於間接訪問的代理/樁基的過程。

2 線程模型

線程模型是COM組件的一種屬性,它決定COM組件可以存在於哪種或者哪些類型的套間中。線程模型不是在開發組件時通過程序代碼指定的,而是通過註冊表中的HKEY_CLASS_ROOT\CLSID\{clsid}\InprocServer32\ThreadingModel鍵值指定的,這是一個字符串類型的註冊表鍵值,其中{clsid}是組件的GUID。當然,配置組件的線程模型時,要考慮組件的代碼編寫方式,而不應該隨意指定。比如說,對於編碼時沒有考慮被多線程併發訪問時數據保護的組件,就不應該配置爲使用多線程模型。

COM組件可以被配置爲使用五種線程模型之一:

2.1 單線程模型(Single)

在註冊表中刪除上述ThreadingModel鍵值,則COM組件被配置爲使用單線程模型。使用單線程模型的組件只能存在於主STA,也就是進程中的第一個STA中。對於具有圖形界面的Windows程序,第一個STA通常由主線程,也就是界面線程創建。具有圖形界面的COM組件,比如說,ActiveX控件,常常使用單線程模型。

2.2 套間線程模型(Apartment)

設置上述ThreadingModel鍵值爲Apartment,則COM組件被配置爲使用套間線程模型。使用套間線程模型的組件只能存在於STA中。套間線程模型是在Visual Studio中使用ATL開發COM組件時默認的線程模型。

單線程模型和套間線程模型的共同點是在任何時刻只有一個線程可以直接訪問組件,這個線程就是創建組件所在的STA的線程(不一定是調用CoCreateInstance創建組件的線程)。其他線程對組件的調用都是通過這個線程間接進行的:COM基礎設施爲STA創建一個隱藏的窗口,將其他線程對STA中組件的調用請求轉化爲發送給這個窗口的消息,然後由套間中唯一的線程處理消息,返回調用結果。所以,使用單線程模型和套間線程模型的組件要求消息隊列,其他線程對組件的調用都是間接地通過消息隊列進行的。這一點很重要。本文後面將通過代碼驗證這一點。

2.3 自由線程模型(Free)

設置上述ThreadingModel鍵值爲Free,則COM組件被配置爲使用自由線程模型。使用自由線程模型的組件只能存在於MTA中,可以被處於MTA中的多個線程“自由”地調用。不在MTA中的線程調用MTA組件時,COM基礎設施隨機選擇RPC線程池中的某個RPC線程代爲間接處理(RPC線程池是COM基礎設施的組成部分)。由於COM基礎設施沒有提供任何同步方面的幫助,多個線程可以併發地調用組件的方法,所以需要編寫代碼對組件實施必要的保護,就像多線程編程中需要對共享資源實施保護一樣。

2.4 雙線程模型(Both)

設置上述ThreadingModel鍵值爲Both,則COM組件被配置爲使用雙線程模型。此時組件與創建組件的線程存在於相同的套間中:既可能是STA,也可能是MTA。因爲組件可能存在於MTA中,被多個線程併發訪問,所以需要編寫代碼對組件實施必要的保護。

自由線程模型和雙線程模型有一個重要的差別:採用自由線程模型的組件可以創建能夠直接調用組件的工作線程;而採用雙線程模型的組件不能。因爲採用雙線程模型的組件可能位於STA中,如果組件創建的工作線程可以直接訪問組件,則工作線程也必須位於STA中(套間之外的線程對組件的調用不能直接進行),這就違反了STA中只能有一個線程的規則,破壞了COM線程模型的同步機制。

2.5 線程中立模型(Neutral)

設置上述ThreadingModel鍵值爲Neutral,則COM組件被配置爲使用線程中立模型。使用線程中立模型的組件位於TNA中,可以被任何線程自由地、直接地訪問。調用線程訪問這種類型的組件時將暫時離開所屬的STA或者MTA,進入TNA,直接對組件進行方法調用,調用完成後返回STA或者MTA。與採用自由線程模型和雙線程模型的組件一樣,必須編寫代碼對組件實施必要的保護,以防止多線程併發訪問可能出現的問題。線程中立模型是運行在組件服務中的,不需要用戶界面的組件的最優選擇。

3 示例程序

筆者編寫了一個簡單的程序,使用生產者-消費者問題來演示COM組件線程模型與線程套間、消息循環的關係。程序中有一個生產者線程、多個消費者線程:生產者線程創建生產者COM組件,並用其創建產品——隨機整數;每個消費者線程創建一個消費者COM組件,並通過該組件消費產品。生產者生產的產品和消費者消費的產品都會顯示到程序界面上。而且,在啓動生產者和消費者線程之前,可以通過程序界面指定各個線程採用的套間類型以及是否使用消息循環,還可以指定各個組件的線程模型。這樣,通過觀察程序輸出,就可以瞭解到生產者和消費者是否正確地進行了同步,從而認識組件的線程模型與線程的套間類型、消息循環之間的關係。

3.1 生產者組件

生產者組件有兩個方法:ProduceProduct()和GetNextProduct()。

生產者線程調用組件的ProduceProduct()方法生產一個產品,也就是生成一個隨機整數,代碼如下:

關於COM組件線程模型的實驗

消費者線程中的消費者調用生產者的GetNextProduct()方法獲取一個產品,代碼如下:

關於COM組件線程模型的實驗



注意這兩個方法的代碼都沒有對共享數據進行同步訪問控制。這樣,在多個線程併發地調用這兩個方法時,可能會發生同步方面的錯誤。

注意這裏對Sleep()的調用:多線程程序設計中,即使程序沒有正確進行同步,有時候似乎也不會發生問題。爲了讓沒有正確進行同步時候的程序錯誤更快地暴露出來,這裏增加了Sleep()調用。這樣,一個消費者線程調用GetNextProduct(),執行到Sleep()語句時,進入休眠狀態,休眠期間其他消費者線程可能再次調用GetNextProduct(),從而發生錯誤:再次消費同一個產品。

3.2 消費者組件

消費者組件只有一個方法ConsumeProduct(),代碼如下:

關於COM組件線程模型的實驗

方法調用生產者組件的GetNextProduct()方法獲取下一個供消費的產品。多個消費者組件調用的是同一個生產者組件,而GetNextProduct()方法沒有進行同步控制,所以多個消費者線程併發地調用時可能會取得錯誤的數據,也就是取得已經被其他消費者消費過的產品。

3.3 生產者線程

生產者線程創建生產者組件,每隔一定時間調用其ProduceProduct()方法生產一個產品,也就是產生一個隨機整數,並且把這個整數輸出到程序界面上的一個列表控件中。

生產者線程首先調用CoInitializeEx()初始化COM基礎設施,其中第二個參數是從程序界面獲取的,它指定了生產者線程採用的套間類型。

然後代碼創建生產者並且調用CoMarshalInterThreadInterfaceInStream()將生產者接口指針列集到流接口指針pData->pStream中,以便可以傳遞到隨後創建的消費者線程中。COM編程中一個重要原則就是:不能直接在不同套間之間傳遞原始接口指針,而應該通過列集、散集來間接傳遞。使用CoMarshalInterThreadInterfaceInStream()和CoUnmarshalInterface()是列集、散集方法之一,此外還可以通過全局接口表(GIT,Global Interface Table)進行列集、散集。

關於COM組件線程模型的實驗

隨後代碼調用生產者每隔一定時間生產一個產品,並將其輸出到界面上:

關於COM組件線程模型的實驗

這段代碼的關鍵在於MsgWaitForMultipleObjects()函數的參數及其返回值:

l 參數pData->hReqExitEvent是一個事件句柄,界面通過設置它爲授信狀態來指示請求生產者線程退出;

l 參數pData->dwProduceInterval是從程序界面獲取的生產時間間隔。如果這個時間內沒有其他條件滿足使得MsgWaitForMultipleObjects()返回,則函數返回WAIT_TIMEOUT表示等待超時,隨後代碼會調用ProduceProduct()生產一個產品,並且輸出到界面上;

l 參數pData->dwProduceWakeMask用以指示是否使用消息隊列:如果在界面上指定了使用消息隊列,則其值爲QS_ALLEVENTS,表示如果消息隊列中有新消息等待處理,則MsgWaitForMultipleObjects()會返回WAIT_OBJECT_0 + 1,隨後代碼會調用PeekMessage()獲取消息,調用TranslateMessage()和DispatchMessage()處理消息。如果界面上沒有指定使用消息隊列,則參數pData->dwProduceWakeMask的值爲0,表示MsgWaitForMultipleObjects()不會因爲有新消息等待處理而返回,也就是不使用消息循環。

3.4 消費者線程

消費者線程會執行下列處理:

l 創建下一個消費者線程;

l 創建消費者組件,通過其獲取下一個待消費的產品,並且將其輸出到界面上。

代碼首先初始化COM基礎設施,然後對流對象指針進行散集,取得生產者接口指針;隨後再次對生產者接口指針進行列集,傳遞給下一個消費者線程;最後代碼創建消費者對象,每隔一定時間獲取消費一個產品,將其輸出到界面上。

消費者線程主要代碼如下:關於COM組件線程模型的實驗

關於COM組件線程模型的實驗

3.5 實驗及結果分析

程序界面如下:

關於COM組件線程模型的實驗

可以通過各個控件進行各項設置,設置好之後點擊【開始】則程序創建生產者線程和消費者線程,各個線程將在下方的列表中進行輸出。列表的每一行代表一個產品,其中生產者列代表生產者生產了一個產品;各個消費者列代表一個消費者消費了一個產品。一段時間後點擊【停止】,則退出各個線程。

關於COM組件線程模型的實驗

通過觀察列表控件的內容可以判斷生產者和消費者是否正確地進行了同步,程序是否正確工作。如果如上圖所示的那樣,對於每一行,有且僅有一個消費者列的值與生產者列的值相等,則說明生產者生產的每個產品都僅僅被某個消費者消費了一次,程序是正確工作的。

3.5.1 單線程模型和套間線程模型

程序啓動後不修改任何設置,直接點擊【開始】,一段時間後點擊【停止】,觀察列表中的輸出。可以發現:對於每一行,有且僅有一個消費者列的值與生產者列的值相等。這就說明了生產者和消費者之間正確地進行了同步。

如果不勾選生產者那一行後面的【使用消息循環】,然後點擊【開始】,則程序輸出20行後停止輸出,而且消費者沒有輸出,這是爲什麼?

上文已經論述過,使用單線程模型的組件要求使用消息循環,因爲組件所在套間之外的線程對組件的調用是通過消息間接進行的。如果選擇不使用消息循環,則COM基礎設施無法正確處理跨線程的調用,所以消費者線程無法正確工作;而生產者在生產的產品填滿緩衝區之後也無法繼續生產了,從而停止輸出。

如果選擇生產者線程使用多線程套間,不使用消息循環,點擊【開始】後可以發現程序會正常工作:輸出持續進行,並且每個產品只被某個消費者消費一次。程序正確工作的原因在於:生產者線程創建生產者對象的時候,COM基礎設施發現請求創建組件的線程的套間類型與組件的線程模型不兼容。此時COM基礎設施會創建一個新的STA,並且將新創建的生產者對象放到這個STA中,從而讓生產者對象可以正確處理來自其他線程的調用請求。

如果選擇生產者組件的線程模型是“套間線程模型(STA)”,程序的行爲也是一樣的。

單線程模型和套間線程模型非常相似:組件只能存在於STA中,只能有一個線程可以直接訪問組件;從其他線程發起的對組件的調用,都是通過消息間接進行的,只有組件所在的STA中的線程正確處理了消息,調用才能正常進行。

單線程模型和套間線程模型的差別在於:採用套間線程模型的組件可以存在於任何STA中:可以是創建組件的線程所屬的STA,也可以是COM基礎設施幫助創建的STA;而採用單線程模型的組件只能存在於主STA中,也就是所有這種類型的組件都存在於進程中的第一個STA中,只能被創建第一個STA的線程直接訪問。這種差別也可以用程序來驗證:選擇生產者組件使用單線程模型,生產者線程使用STA,不使用消息循環,但是選擇界面線程使用單線程套間,觀察發現程序可以正確工作。原因在於界面線程首先創建的進程中的第一個STA成爲主STA,生產者組件將位於這個STA中,可以被其中的工作線程,也就是界面線程直接訪問,而界面線程是有消息循環的,所以其他線程對生產者組件的訪問可以正確地通過界面線程的消息循環間接進行。如果選擇界面線程不使用COM,或者使用多線程套間,則會發現程序不能正確工作。

3.5.2 多線程模型

自由線程模型、雙線程模型和線程中立模型都屬於多線程模型,即可以有多個線程併發地、直接地訪問組件,對組件進行方法調用。此時如果組件中沒有處理同步的代碼,則在訪問共享數據的時候可能發生錯誤。以示例程序爲例,選擇生產者組件使用自由線程模型,則程序輸出類似於下圖:

關於COM組件線程模型的實驗

有些行的各個消費者列沒有內容,說明這一行對應的產品沒有被消費;而有些行中有多個消費者列的值與生產者列的值相同,說明一個產品被消費了多次,也就是多個消費者線程之間沒有進行正確的同步。這是因爲示例程序中的生產者組件沒有對併發訪問進行同步。

如果選擇生產者組件使用雙線程模型,則可以發現:

1如果選擇生產者線程使用單線程套間,在選擇不使用消息循環時,程序輸出20行之後就停止輸出,而且各個消費者列沒有內容;如果選擇使用消息循環,則程序會產生正確的輸出。

2 如果選擇生產者線程使用多線程套間,則程序會產生錯誤的輸出。

原因在於,使用雙線程模型時,組件總是與創建組件的線程在相同套間中。這樣,選擇生產者線程使用單線程套間時,生產者組件在單線程套間中創建,只能被生產者線程直接訪問;其他線程對生產者組件的調用需要通過生產者線程間接進行,而且要求生產者線程具有消息循環。如果沒有消息循環,則生產者組件不能正確處理來自其他套間的調用,也就是消費者無法正確調用生產者組件來獲取產品,所以消費者列沒有輸出;而生產者組件在產品緩衝區填滿之後就停止生產了,所以也不再輸出。如果選擇生產者線程使用多線程套間,則生產者組件在多線程套間中創建,來自其他套間的調用由RPC線程池中的隨機線程代爲處理,可以被併發地調用。

上述分析的焦點在於生產者組件:由於生產者組件的代碼沒有對併發訪問進行同步處理,所以在被併發訪問時程序會產生錯誤的輸出。但是,如果關注下消費者組件,則會發現用本文前面所論述的理論無法正確解釋下圖所示的情況:

關於COM組件線程模型的實驗

這裏,消費者線程使用單線程套間,消費者組件使用單線程模型。那麼,所有消費者組件都在進程的主STA,也就是第一個消費者線程所在的STA中創建;只能被主STA中的線程,也就是第一個消費者線程直接訪問。其他消費者線程由於在各自的STA中,無法直接訪問自己創建的、位於主STA中的組件,而只能通過主STA線程間接訪問。上圖所示的情況沒有選擇消費者線程使用消息循環,那麼,第一個消費者線程,也就是主STA線程沒有消息循環,應該無法處理來自其他套間中的消費者線程對於消費者組件的調用。然而觀察上圖發現,各個消費者列都有輸出,也就是其他消費者線程成功地對位於第一個消費者所在的STA(主STA)中的消費者組件進行了調用。這是問題一。拋開這個問題,就算其他消費者線程成功地對位於第一個消費者所在的STA(主STA)中的消費者組件進行了調用,這種調用也應該都是通過第一個消費者線程進行的,不存在併發訪問的問題。然而上圖所示的情況是:很多行的消費者列沒有內容,而有的行裏面各個消費者列都有內容,顯然存在併發訪問的情況。這是問題二。

筆者被這兩個問題困擾了好幾天,百思不得其解。後來,在完成博文《翻譯:理解COM+套間(第一部分)》之後,結合文章關於STA出調用的論述,經過思考,才爲上圖所示的情況找到了合理的解釋。消費者組件的CConsumer::ConsumeProduct()方法會調用生產者組件的CProducer::GetNextProduct()方法,而消費者組件和生產者組件位於不同的套間,所以這是一個跨套間方法調用,相對於消費者組件所在的STA來說,是一個“出調用”。博文《翻譯:理解COM+套間(第一部分)》對於STA的出調用有以下論述:“調用離開STA時,COM會阻塞STA線程,但是讓STA線程仍然可以處理回調。爲了讓回調可以發生,COM會跟蹤每個方法調用的因果關係,以便能夠識別何時應該釋放正在RPC通道中等待某方法調用返回的STA線程,讓其處理另一個進入的調用。默認情況下,STA入口有調用到達時,如果STA線程正在等待出調用返回,而且到達的入調用與正在等待返回的出調用不屬於同一個因果鏈,則到達的入調用將阻塞。”COM怎樣實現STA在等待出調用返回的同時,仍然可以處理回調的呢?由於STA線程是通過消息隊列處理其他套間對於STA的入調用的,所以筆者猜測是通過消息等待函數MsgWaitForMultipleObjects或者MsgWaitForMultipleObjectsEx實現的。這樣,STA線程在等待出調用返回的時候,仍然可以處理消息隊列中新到達的消息。結合本文討論的示例程序來看,第一個消費者線程在等待對於生產者組件的調用(出調用)返回的同時,可以處理消息隊列中新到達的消息,也就是可以處理其他消費者線程對於主STA中消費者組件的調用。這樣,多個消費者組件(各個消費者線程只調用自己創建的消費者組件)被併發地調用,然後多個消費者組件又併發地調用同一個生產者組件,所以就產生了上面的程序輸出。當然,這裏“併發”不太明顯:所有對於多個消費者組件的調用最終都是由第一個消費者線程直接進行的,不存在併發。然而,生產者組件在MTA中,而消費者組件在STA中,因此消費者組件對於生產者組件的調用是通過RPC線程池裏的隨機RPC線程處理的,對生產者組件的多次調用很可能是由多個不同的RPC線程代爲執行的。從這個角度看,就是“併發”調用了。

上述對於程序運行情況的解釋,只是筆者的猜測,不一定正確。然而,使用上述思路可以解釋生產者組件使用線程中立套間時程序的運行情況:

關於COM組件線程模型的實驗

這一幅圖展示的運行配置,與前一幅圖只有一處不同:生產者組件的線程模型從“多線程模型”改成了“線程中立模型”。這一處不同使得只有第一個消費者線程有輸出。原因在於,第一個消費者線程在調用使用線程中立模型的生產者組件時,臨時離開線程所在的STA,進入到生產者組件所在的TNA中,直接對生產者組件進行調用。這樣就不存在等待出調用返回的問題,也就不存在上述使用消息等待函數MsgWaitForMultipleObjects或者MsgWaitForMultipleObjectsEx的過程了。於是,其他消費者線程調用位於第一個消費者線程所在的STA(主STA)中的消費者組件時所投遞的消息就一直在第一個消費者線程的消息隊列中等待處理,使得其他消費者線程對於消費者組件的調用無法完成,不會返回,所以第一個消費者之外的其他消費者沒有輸出。

 

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