這段時間做ArcgisEngine,因為在做圖形交叉分析時,計算數據分多個線程分別計算不同的圖形,發現計算錯誤。后來初步了接了是由于所有的ArcObjects組件都被標記為單線程單元(STA參考VS幫助文檔)。每個STA都限制在一個線程中,但是COM并不限制每個進程中STA的數目。當一個方法調用進入一個STA,它被轉移到STA的唯一線程。因此,在STA中的一個對象將一次只接收和處理一個方法調用,它接收的每個方法調用會到達同一線程。初步認為調用COM部分應加鎖。
這里列出幾個可以參考的鏈接或內容:
轉自:(1)ArcGIS Engine 中的多線程使用??
? ? ? ?(2)COM多線程原理與應用
ArcGIS Engine 中的多線程使用??
2010-09-24 16:49:08|??分類:?GIS博文?|??標簽:線程??多線程??arcobjects??應用程序??delegate??|字號?訂閱
??????? 一直都想寫寫AE中多線程的使用,但一直苦于沒有時間,終于在中秋假期閑了下來。呵呵,閑話不說了,進入正題!
??????? 大家都了解到ArcGIS中處理大數據量時速度是相當的慢,這時如果你的程序是單線程的,那可就讓人著急壞了,不知道處理到什么地步,不能操作其他的功能,無奈~~如果在這時你能夠想到用多線程技術,那就來試試該如何完成吧。
?????? 首先,你得有點VS的多線程經驗或學習經驗,得知道什么多線程,代理(Delegate)是什么,同步與異步又是什么,等等。這些在VS的幫助文檔中都有詳細解釋,在這里我就不越俎代庖了。我們其中精神去理解ArcGIS中多線程吧。
????? ?在ArcgIS中,我們分幾個部分闡述多線程。
?????? 1、何時使用多線程
在創建多線程應用程序是應注意兩點:線程的安全性和線程的伸縮性。線程安全對于所有的對象都是非常重要的,但是僅僅只有線程安全的對象并不意味著成功創建多線程應用程序,或者說線程安全能夠提高應用程序的性能。
.NET框架允許你在應用程序中能夠迅速的創建線程,但是,在編寫ArcObjects代碼的多線程必須要小心。ArcObjects最根本的結構是組件對象模型(COM)。從這一點來說,編寫ArcObjects的多線程的代碼需要既了解.NET多線程,又要了解COM多線程模型。
多線程并不總是使你的程序跑的很快,在許多情況下,它還會增加開支和復雜性,這些最終會減慢程序的執行速度。當增加的復雜性是值得的,那么多線程才能使用。一個基本的原則是,如果一個任務可以分解為不同的獨立任務時,那這個任務是適合多線程的。
2、ArcObjects線程模型
所有的ArcObjects組件都被標記為單線程單元(STA參考VS幫助文檔)。每個STA都限制在一個線程中,但是COM并不限制每個進程中STA的數目。當一個方法調用進入一個STA,它被轉移到STA的唯一線程。因此,在STA中的一個對象將一次只接收和處理一個方法調用,它接收的每個方法調用會到達同一線程。
ArcObjects組件是線程安全的,開發者可把他們在多線程環境下使用。對于AO應用程序在多線程環境下有效運行,由AO所使用的線程單元模型,即獨立線程,必須加以考慮。該模型的工作原理是消除跨線程通信。一個線程內所有ArcObjects對象的引用應當只與在同一個線程的對象進行通信。
對于此模型的運行,在ArcGIS 9.X中單個對象都被設計為線程唯一,而非進程唯一。在進程中管理多個對象的資源消耗超過由制止跨線程通信所獲得的性能提升幅度。
對于擴展ArcGIS系統的開發者,所有對象甚至包括你創造的對象都必須遵循這一規則,孤立線程工作。如果你創建的對象做為開發的一部分,你必須確保它們是線程唯一,而不是進程唯一。線程唯一就是防止跨線程通信,這里ArcGIS Engine中多線程的首要規則。
3、多線程方案
盡管有很多實現多線程應用程序的方式,但以下幾種方案是開發者經常使用的方式。
3.1、后臺線程執行長事務
當要求需要長事務進行工作時,在后臺執行長事務是可取的,并且同時讓應用程序靈活的操作其他任務,并讓界面處于響應狀態。這一操作的例子很多,如:使用FeatureCursor來重復向DataTable裝載數據,進行復雜的拓撲計算并寫入新的FeatureClass。為了完成這類任務,請記住以下幾點:
a. 根據在孤立模型中的線程,你不能在線程之間共享ArcObjects的組件。相反,你需要考慮的是,單個對象都在各自線程中,并在后臺線程中,例如所有工廠需要打開FeatureClass,創造新的FeatureClass,設置空間參考等等。
b.傳遞給線程的所有信息必須是簡單類型或托管類型的形式。
c.萬一在某種情況下,你要從主線程向工作線程傳遞ArcObjects組件,可以將對象序列化成字符串,再將字符串傳遞給目標線程,然后再反序列化還原到對象。例如,你可以使用XmlSerializerClass序列化對象成為字符串,如工作區間(Workspace)連接屬性(IPropertySet),再將這一字符串傳遞給目標線程,然后在工作線程中使用XmlSerializerClass反序列化連接屬性。這樣,就將連接屬性對象在后臺再次創造出來,從而避免了跨線程訪問。
當運行后臺線程,你能夠在用戶界面了解任務的進度。
3.2、實施單機ArcObjects的應用程序
正如微軟開發人員網絡(MSDN)網站上所說,“在.NET Framework版本2.0中,如果線程的單元狀態在啟動前尚未確定,新的線程就初始化為ApartmentState.MTA。主應用程序線程默認初始化為ApartmentState.MTA。您不能通過設置代碼的第一行Thread.ApartmentState屬性再設置主應用程序線程到ApartmentState.STA。而應使用STAThreadAttribute代替。”
作為ArcObjects的開發人員,這意味著,如果您的應用程序不被視為一個單一線程應用程序初始化的,.NET框架將為所有的ArcObjects創建一個特殊的單線程單元(STA)線程,因為他們被標記STA。這將導致對每一個從應用程序調用ArcObjects的線程切換到這個特定的線程上來。反過來,這迫使ArcObjects組件合在一起調用,并最終以COM組件調用可能慢了約50倍。幸運的是,這可避免通過簡單地標記主要功能為[STAThread]。
3.3、使用托管線程池和BackgroundWorker的線程
線程池線程都是后臺線程。線程池通過提供一個由系統管理的應用程序線程池使你使用線程更有效率。利用為每個任務創建一個新線程的線程池的優點是線程創建和銷毀的開銷是可忽略的,它可以帶來更好的性能和更好的系統穩定性。
然而,設計的所有ThreadPool線程是在多線程單元(MTA),因此不應該被用來運行ArcObjects,它們是單線程單元。若要解決此問題,您有幾種選擇。一個是實現一個專用ArcObjects的線程,它被標記為STAThread并委派每個從MTA線程調用這個專用ArcObjects線程。另一種解決方案是使用自定義的STA線程池的實現,如標記為STA線程的線程數組來運行 ArcObjects。
3.4、同步運行線程的并發執行
在許多情況下,您必須同步執行的并發運行的線程。通常,你要等待一個或多個線程完成他們的任務,當一定條件下得到滿足,一個等待線程的信號恢復其任務,條件如:測試是給定線否程激活和運行,改變線程優先級,或給予其他一些條件。
在.NET中有幾種方法來管理運行線程的執行。可用來幫助線程管理的主要幾類如下:
System.Threading.Thread;
System.Threading.WaitHandle;
System.Threading.Monitor;
System.Threading.AutoResetEvent and System.Threading.ManualResetEvent。
3.5、在多個線程共享一個托管類型
有時候你的.NET應用程序的底層數據結構將是一個如DataTable或哈希表管理的對象。這些.NET托管對象允許你在多個線程共享數據獲取,如線程和主線程渲染他們。但是,您應該咨詢MSDN Web站點以驗證這一點是否是線程安全的。在許多情況下,一個對象是線程讀安全,而寫并不安全。有些集合實施同步方法,它提供了一個底層集合的同步包裝。
在你的對象被多個線程訪問的情況下,根據MSDN關于這種情況的對象線程安全規則,你應該獲得一個獨占鎖。取得這樣的獨占鎖能夠完成上面所描述的同步方法,或使用lock語句,它通過獲取給定對象的相互排他鎖標簽一個關鍵塊。它可以確保,如果另一個線程試圖訪問對象時,它會被阻塞,直到該對象被釋放(退出鎖)。
3.6、從后臺線程更新用戶界面
在大多數情況下,您正在使用一個后臺線程來執行長時間的操作,你想向用戶報告進度,狀態,錯誤或其他與該線程執行的任務相關的信息。這可以通過更新一個應用程序的用戶界面控件來實現。但是,在Windows中,窗體控件綁定到一個特定的線程(通常是主線程),并且不是線程安全的。因此,你必須委派,從而結合,任何調用UI控件的線程來控制它的所屬。該委托是通過調用Control.Invoke方法,該方法在線程上執行委托,該委托擁有控件的基礎窗口句柄。要驗證調用者是否必須調用Invoke方法,你可以使用屬性Control.InvokeRequired。您必須確保該控件的句柄再嘗試調用Control.Invoke或Control.InvokeRequired之前已經創建。
3.7、從一個線程調用ArcObjects而不是主線程
在許多多線程應用程序中,你將需要從不同線程調用AO組件。例如,你可能有一個后臺線程來獲取Web服務,這反過來,應該增加新的項目到地圖顯示,響應更改地圖,或運行的geoprocessing(gp)的工具來執行某些類型分析。
一個非常常見的情況是從一個計時器事件處理方法調用ArcObjects。計時器的Elapsed事件是在一個線程池的任務提出,這不是一個主線程。然而,它需要使用ArcObjects,這好像是需要跨單元調用。然而,這可以避免處理ArcObjects的組件,就好像AO組件是一個用戶界面控件和使用Invoke來調用委派到創建ArcObjects組件的主線程中。因此,沒有跨單元調用。
ISynchronizeInvoke接口包括的方法有Invoke,BeginInvoke,和EndInvoke。自己實現這些方法可能是一個艱巨的任務。相反,你應該有你直接從System.Windows.Forms.Control繼承的類或者你應該有一個助手類,它繼承自控件。要么選擇將提供一個簡單而有效的對于調用方法的解決方案。
?
delegate?SomethingClassType SomeDelegate(IArray array);
Func()
{
………………
??????????? SomeDelegate del = new SomeDelegate(AnotherFunc);//AnotherFunc與SomeDelegate同樣的形式
??????????? IAsyncResult ireslt = del.BeginInvoke(array, null, null);//異步操作
?
??????????? ProgressbarForm form = new ProgressbarForm();//異步操作中的進度條窗體
??????????? form.setProgressBar("提示", "正在處理數據...", 10, 0, 100);
?
??????????? form.Show();
??????????? System.Windows.Forms.Application.DoEvents();
? ??????????while (!ireslt.IsCompleted)
??????????? {
??????????????? System.Windows.Forms.Application.DoEvents();
??????????? }
??????????? SomethingClassType something= del.EndInvoke(ireslt);
??????????? form.Close();
………
}
以上是理論方面的闡述及一個本人開發過程中的一個代碼片段,希望這些能夠幫助你完成你的多線程程序。參考的資料如下:Windows MSDN,ESRI 的開發者網站。
轉載請注明出處!謝謝!
?
目錄:
COM多線程原理與應用
目錄:
前言:
套間:
套間的定義:
套間的分類:
套間的進入和退出:
對象的同步:
組件對象的同步:
COM對象線程模型:
進程內對象線程模型的種類:
ATL對多線程的支持:
對象引用的保護:
成員變量的保護:
COM+導致的變化:
上下文概述:
上下文對象:
調用對象:
?
前言:
COM多線程一直是個不容易弄清的問題,我也被困擾了很久,特別是COM在線程方面的術語總是不能統一。本文是為了將我所學所用得做一個總結,本文不保證一定正確,但是會隨著時間的推移逐漸完善改正。
?
套間:
套間的定義:
???????我個人認為<<COM技術內幕>>中關于套間的定義是錯誤的,應采用<<COM本質論>>中的定義。
<<COM技術內幕>>中-----
套間(Apartment),一個由用戶界面線程(套間線程)和一個消息循環構成的概念性實體。
<<COM本質論>>中------
套間定義了一組對象的邏輯組合,這些對象共享同一組并發性和重入限制。一個線程要想使用COM,必須先進入一個套間。COM規定,只有運行在對象套間中的線程才能訪問該對象。
套間的分類:
COM定義了兩種類型的套間,STA(單線程套間)和MTA(多線程套間)。
STA的特點是套間內永遠只有一個線程運行,并且一定是創建該套間的初始線程。因而開發只運行在STA中的組件不需要考慮線程同步等問題。
STA中包含了一個不可見的窗口,所以透過窗口的消息循環機制對消息隊列的處理,保證了同一時刻只有一個調用請求被執行,這就是組件不需要處理同步的原因。
?
MTA的特點是套間內可以有多個線程運行,并且還可以創建新的線程。在MTA中運行的組件必須自己實現線程的同步。
?
?
套間的進入和退出:
線程通過CoInitializeEx函數進入套間,該函數的第二個參數通過傳遞COINIT_APARTMENTTHREADED和COINIT_MULTITHREADED標志了套間類型。如果傳遞COINIT_APARTMENTTHREADED,該線程將創建自己私有的套間,別人不能進入。如果傳遞COINIT_MULTITHREADED,該線程將進入當前進程范圍內的MTA。
線程調用CoUninitialize函數用于退出所在的套間。只有退出后才可以進入其它類型的套間。
?
對象的同步:
組件對象的同步:
根據是否支持多線程同步,組件對象可以分為單線程對象和多線程對象。
COM對象線程模型:
每個COM對象都可以決定自己將運行在什么樣的套間內,這稱為COM對象的線程模型。
組件將運行在什么樣的套間里面,這是由組件開發者決定的,調用組件的客戶程序不需要關心。但是如果調用線程所在的套間和組件運行的套間不是同一套間,COM將插入代理/存根機制,調用線程在自身的套間內調用代理上的方法使用對象方法,而對象將運行在它需要的套間內。
進程內對象線程模型的種類:
線程模型的種類取決于注冊表中ThreadingModel的值----------
1)??Both?表示組件可以運行在STA和MTA中
2)??Free?表示組件只能運行在MTA中
3)??Apartment表示組件只能運行在STA中
4)??為空表示組件只能運行在進程第一個創建的STA(主STA中)中。
如果調用線程的套間能夠滿足進程內組件的線程模型的要求,則直接創建組件對象,并不需要代理。
如果調用線程的套間不能滿足進程內組件的線程模型的要求,則COM會在另一個合適的套間內(如果沒有合適的套間,COM會創建一個)創建對象,然后給調用線程返回代理。
如果組件和客戶程序不在同一個進程或者不在同一臺機器,那必然是在兩個套間中,COM會創建代理/存根。
如果組件對象的線程模型是Both或者Free,組件必須是多線程對象。
如果組件對象的線程模型是空,則組件可以是單線程對象或者多線程對象。這時候多線程對象內部的同步機制其實沒有意義,因為不會有多個線程同時訪問對象。
如果組件對象的線程模型是Apartment,情況將分兩種:
a)???????組件內部沒有全局變量和靜態變量,組件可以是單線程對象
b)??????組件內部有全局變量和靜態變量,組件應該是多線程對象,并對全局變量和靜態變量進
行訪問保護。
?
ATL對多線程的支持:
ATL中對COM對象中同步的支持有自己的考慮:如果COM對象本身就是不考慮同步的單線程對象,ATL就不應該對該對象的任何數據成員包括對象引用變量m_cRef進行同步保護。因為同步保護是需要額外的耗費資源的。ATL中的原則是只在應該需要保護的場合進行同步保護。這樣做是符合ATL的設計目的----一切為了效率。
對象引用的保護:
CComSingleThreadModel、CComMultiThreadModel、CComMultiThreadModelNoCS類里面都定義了靜態成員函數Increment和Decrement。CComObjectRootEx類的模板參數接受以上三個類并使用他們的靜態成員函數,而我們的組件實現類從CComObjectRootEx類派生,從而獲得了根據我們組件是否支持多線程而對對象引用進行同步的功能。一句話,我們只要派生具有合適的模板參數的CComObjectRootEx類就可以了。如下面我們的類:
class?ATL_NO_VTABLE CMIS :
?????public?CComObjectRootEx<CComSingleThreadModel>,
CMIS類不需要考慮對象引用的同步保護,因為我們的組件對象是單線程對象,線程模型為空或者為Apartment。
???????但是請注意ATL工程的組件類腳本中默認線程模型是both,這就會帶來問題,所以我們手動修改它。
???????如果我們改動代碼如下:
class?ATL_NO_VTABLE CMIS :
?????public?CComObjectRootEx<?CComMultiThreadModel?>,那么我們的組件對象引用計數就受到同步保護,而且我們的組件對象自己負責同步保護。這樣如果CMIS類里有成員變量,那么我們要對成員變量進行同步保護。
成員變量的保護:
???????采用臨界區方式進行同步保護我們要利用類CComMultiThreadModel。我們的組件實現類派生自CComObjectRootEx<?CComMultiThreadModel?>類。使用的方法極其簡單,我們只要在需要防止多個線程同時執行的函數內部開始處加上如下代碼:
???????ObjectLock Lock(this);
?????如果想使用多個臨界區的話使用方法就沒這么簡單了,但是除非必須,否則我不建議這樣做,因為很容易引起死鎖。一定要用的話必須先閱讀<<Windows核心編程>>和<<Win32多線程程序設計>>這兩本書。
?
COM+導致的變化:
上下文概述:
COM+對于套間的概念進一步的擴展,要求每個對象都必須運行在上下文中。通過上下文來控制對象的線程同步。
套間被細分為一個或者若干個上下文。一個上下文只能在一個套間中。一個套間至少包含一個默認上下文。
位于同一個套間中的不同上下文之間的對象調用必須經過輕量級代理的列集。輕量級代理不同于套間之間的代理,因為性能損失較小。
?
一個沒有被COM+托管的傳統COM組件不使用輕量級代理。但是該COM組件將被認為是放到了它所生存的套間的默認上下文內部,該套件的其他上下文(如果有的話)訪問默認上下文不需要輕量級代理,默認上下文的引入只是為了和COM+上下文的概念一致。
?
上下文對象:
???????上下文對象具體代表了每個上下文。CoGetObjectContext用來獲取當前對象所在的上下文對象。
???????上下文對象暴露了IObjectContextInfo、IContextState、IObjectContext、IObjectContextActivity四個接口。后面兩個接口是向后兼容MTS,可以忽略。
???????IObjectContextInfo::GetContextId方法可以獲得上下文ID,調試時使用很方便。但是注意,只有COM組件被COM+管理后才能獲得上下文對象,否則CoGetObjectContext將返回E_NOINTERFACE并返回NULL接口指針。所以如果我們開發的組件要同時適用于COM+和傳統COM兩種情況,就必須對CoGetObjectContext返回值進行查詢并且作區分處理。
?
調用對象:
???????當客戶通過COM+服務調用COM對象時,COM+將創建名為“調用對象”的臨時對象代表客戶對COM對象的調用。調用對象將在方法返回后被銷毀。
???????調用對象暴露了兩個與安全性設置相關的接口。
???????CoGetCallContext函數可以讓對象獲取自己的調用對象,如果返回RPC_E_CALL_COMPLETE,則說明目前沒有客戶調用自己。