C# COM ArcgisEngine 多线程相关

这段时间做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,则说明目前没有客户调用自己。

posted @ 2013-11-12 22:14  焦涛  阅读(1059)  评论(0编辑  收藏  举报