编写多线程 ArcObjects 代码

来源 目录

编写多线程 ArcObjects 代码

概述

多线程允许应用程序在单个进程中一次执行多个任务。 本主题详细介绍了多线程在 ArcObjects .NET SDK 环境中的含义,以及将线程正确集成到 ArcObjects 应用程序中所必须遵循的规则。

本主题不教授多线程概念。 相反,它为涉及多线程的日常 ArcObjects 编程问题提供了实用的解决方案。

关于多线程

多线程通常用于提高应用程序的响应能力。 这种响应能力可能是实际性能改进的结果,也可能是对性能改进的感知。 通过在代码中使用多个执行线程,您可以将【数据处理或输入/输出 (I/O) 操作】与【程序 UI 的管理】分开,防止任何长时间的数据处理操作降低 UI 的响应能力。

多线程的性能优势是以增加代码设计和维护的复杂性为代价的。 应用程序的线程共享相同的内存空间,因此您必须确保访问【共享的数据结构】是同步的,以防止应用程序进入无效状态甚至崩溃。 这种同步通常称为并发控制

并发控制可以在两种层次上得到实现:对象层次和应用层次。

  • 当共享对象是线程安全的时,它可以在对象级别实现。这意味着对象强制所有试图访问它的线程等待,直到当前访问对象的线程完成【对“对象的状态”的修改】。
  • 并发控制可以在应用程序级别通过获取共享对象的独占锁来完成,允许一个线程一次修改对象的状态。 使用锁时要小心,因为过度使用它们会保护您的数据,但也会导致性能下降。 在性能和保护之间找到平衡需要仔细考虑您的数据结构和额外线程的预期使用模式。

使用多线程

构建多线程应用程序时,需要考虑两点:线程安全性和可伸缩性。 所有对象都是线程安全的,这很重要。但是仅仅拥有线程安全的对象,并不意味着创建多线程应用程序变得很简单,也不意味着【生成的应用程序】在性能上有所改进。

虽然您可以轻松地在 .NET Framework 的应用程序中生成线程; 但是,编写多线程 ArcObjects 代码时,仍应小心谨慎。 ArcObjects 的底层架构是 COM。 因此,在编写多线程 ArcObjects 应用程序时,需要了解 .NET 多线程和 COM 的线程模型。

使用多线程,并不会使你的代码运行得更快; 在许多情况下,它会增加额外的开销和复杂性,最终会降低代码的执行速度。 仅当增加的复杂性值得付出代价时,才应使用多线程。 一般来说,如果一个任务,可以分解成不同的独立任务,那么,它就适合使用多线程。

ArcObjects 线程模型

所有 ArcObjects 组件都被标记为 STA(single threaded apartment)。 每个 STA 被限制最多只关联一个线程(但每个进程的 STA 数量没有限制)。 当对方法的调用进入 STA 时,它会被转移到 STA 的线程(有且唯一的线程)上执行。 因此,STA 中的对象一次只能接收和处理一个方法调用,并且它接收到的每个方法调用,都在同一个线程中执行。

译者注释:

套间(apartment)是人为创建的、仅在逻辑上存在的一个概念,以建立和描述【线程与 COM 对象之间】的关系,用于简化 COM 对象在多线程方面上的实现。借助套间的概念,发明一套行之有效的机制;使用这个机制,避免了同步问题,简化了编程(然而,这套机制却很复杂)。

机制大致如下:

1. 一个进程有任意数量的 STA,最多一个 MTA;在 COM+ 机制下,还有名为 TNA 的套间,在同一进程内也最多一个TNA。

2. 存在一个名为 SCM(服务控制管理器 ) 的 COM 运行时环境(可视为 COM 运行时);
   SCM 与 COM 对象、服务器(即被调用的 COM 对象)和操作系统交互,并提供客户端(COM 对象)和它们工作的对象之间的透明性。
   
3. 线程必须属于某个套间,但是可以在不同时间内属于不同的套间。

4. 每个 COM 对象都有一个名为 ThreadingModel 的特性(属性),它的值是Apartment、Free、Both和Neutral四个之一,用于指示这个 COM 对象的应该属于哪种类型的套间。

   SCM 通过注册表中 COM 对象的 ThreadingModel 条目的值,决定将对象放置到哪种类型的套间中去。
   一个对象在它的整个生命周期内,都只属于一个套间。COM 对象不会从一个套间迁移到另外一个套间。如果这个套间被释放,那么,这个对象也同时被释放。
   
   如果在某个线程上创建一个 COM 对象,它有可能仅获取到这个 COM 对象的代理(Proxy),而不是 COM 对象的原始指针;因为 COM 对象与线程可能处理于不同的套间中。例如, COM 对象 ThreadingModel 的特性值是 Apartment,必须处于 STA 中,而恰好,调用它的线程处于 MTA 中。
   
   编组(Marshalling)使客户端能够透明地对其他套间中的对象进行接口函数调用【由被调用 COM 对象所在套间的线程负责实际执行,而不是客户端所在套间的线程来执行这种调用,将参数编组过去,又将返回值编组回来】。编组可以发生在不同机器上的 COM 套间之间、不同进程空间中的 COM 套间之间,以及同一进程空间中的不同 COM 套间之间。
   
   如果某个对象编写的时候忘记考虑多线程,或者没有时间考虑,或者没有必要提供实现多线程的支持,这个时候可以将对象指定为 STA,让 COM 机制自动管理对该对象的调用,以保证对象可以被正确调用。
   
5. 线程调用自身所在套间的套间内 COM 对象时,不需要代理(Proxy),它是直接调用对象,和普通 C++ 的虚函数调用并无区别。

ArcObjects 组件都是线程安全的,您可以在多线程环境中使用它们。 在多线程环境中,为了使 ArcObjects 应用程序高效运行,必须考虑 ArcObjects 使用的单元线程模型(threading model)【即线程隔离模型】。 该模型通过消除跨线程通信来工作。 一个线程中的所有 ArcObjects 引用,应该只与同一线程中的对象通信。

为了使该模型能正常工作,ArcGIS 中的单例对象被设计为每个线程的单例,而不是每个进程的单例。因为,在一个进程中托管多个单例的资源开销,被【不使用跨线程通信而产生的性能增益】所抵消。如果单例只在一个线程中创建,然后从其他线程访问,则会发生这种情况(即被上述所说的性能增益抵消掉,即可使用【不使用跨线程通信方式】,即是“单例对象被设计为每个线程的单例”的原因)。有关详细信息,请参阅 与单例对象的交互

作为【可扩展 ArcGIS 系统】的开发人员,所有对象(包括您自己编写的对象)都必须遵守此规则——线程隔离模型——以确保程序正常工作。 如果您在开发过程中,创建单例对象,则必须确保这些对象是每个线程单例,而不是每个进程的单例。

为了在多线程环境中成功使用 ArcObjects,程序员在编写多线程代码时,必须遵循【线程隔离模型】,以避免应用程序失败,例如死锁情况、编组导致的负面性能,以及其他预期外的行为。

多线程场景

实现多线程应用程序,尽管有很多种方法,但以下内容会是您可能遇到的更常见的场景。

在 ArcObjects .NET SDK 中,有几个关于多线程编程示例。在以下代码示例中,我们会引用到它们。

这些示例,涵盖了本主题中描述的场景,展示了现实问题的解决方案,同时展示了最佳编程实践。 虽然这些示例使用多线程是作为【给定问题的解决方案的一部分】,但在某些示例中,您还会发现多线程只是在更广泛场景上的一面。

在后台线程上运行冗长的操作

当您有冗长的操作需要运行时,为了让应用程序可以同时自由处理其他任务,并且始终保持 UI 响应,可以将冗长的操作放在后台线程上运行。这是一种很方便的方式。 例如以下操作,遍历 FeatureCursor 以将信息加载到 DataTable 中,并【在将结果写入新 FeatureClass 时】执行复杂的拓扑操作。

当运行冗长操作时,请记住以下几点:

  • 根据【线程隔离模型】,您不能在线程之间共享 ArcObjects 组件。 相反,利用单例对象是“每个线程的单例”的事实——在后台线程中,实例化所需的所有工厂类(工厂类一般是这样的单例),用于打开要素类、创建新要素类、设置空间参考等。【译注:意思是,需要在本线程中实例化单例对象,而不是使用其他线程中的相应对象】。
  • 传递给线程的所有信息,必须采用简单类型(simple type)或托管类型(managed type)的形式(译注:不能是 ArcObjects COM 对象之类)。
  • 在必须将 ArcObjects 组件从主线程传递到工作线程的情况下,将对象序列化为字符串,将字符串传递给目标线程,然后将对象反序列化( 您可以使用 XmlSerializerClass 来序列化一个对象)。例如,将 workspace 连接属性(一个 [IPropertySet](https: //desktop.arcgis.com/en/arcobjects/latest/net/IPropertySet.htm)),转换成字符串; 将字符串传递给工作线程;在工作线程上,使用 XmlSerializerClass 反序列化,获取到连接属性。 这样,就在后台线程上创建连接属性对象了,避免了跨单元调用。
  • 在运行后台线程时,您可以将任务进度报告到 UI 对话框中。 本主题的 从后台线程更新 UI 部分对此进行了更详细的介绍。

下面的代码示例,演示了一个后台线程,该线程用于遍历 FeatureCursor,并填充 DataTable(在稍后的应用程序中需要使用)。 这使应用程序可以自由运行,而无需等待填充表的完成。

[C#]

//生成线程
Thread t=new Thread(new ThreadStart(PopulateLocationsTableProc));
//将线程标记为 STA,以高效运行 ArcObjects
t.SetApartmentState(ApartmentState.STA);
//启动线程
t.Start();

/// <summary>
/// 将 MajorCities 要素类中的信息加载到 locations 表中
/// </summary>
private void PopulateLocationsTableProc()
{
    //从注册表中获取 ArcGIS 路径
    RegistryKey key=Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ESRI\ArcGIS");
    string path=Convert.ToString(key.GetValue("InstallDir"));

    //打开要素类。 Workspace Factory 必须被实例化,因为它是每个线程的单例。
    IWorkspaceFactory wf=new ShapefileWorkspaceFactoryClass()as IWorkspaceFactory;
    IWorkspace ws=wf.OpenFromFile(System.IO.Path.Combine(path, @
        "DeveloperKit10.0\Samples\Data\USZipCodeData"), 0);
    IFeatureWorkspace fw=ws as IFeatureWorkspace;
    IFeatureClass featureClass=fw.OpenFeatureClass(m_sShapefileName);

    //映射"NAME"和"ZIP"字段。
    int nameIndex=featureClass.FindField("NAME");//名称
    int zipIndex=featureClass.FindField("ZIP");//邮政编码
    string cityName;
    long zip;

    try
    {
        //遍历 features 并将信息添加到表中。
        IFeatureCursor fCursor=null;
        fCursor=featureClass.Search(null, true);
        IFeature feature=fCursor.NextFeature();
        int index=0;

        while (null != feature)
        {
            object obj=feature.get_Value(nameIndex);
            if (obj == null)
                continue;
            cityName=Convert.ToString(obj);

            obj=feature.get_Value(zipIndex);
            if (obj == null)
                continue;
            zip=long.Parse(Convert.ToString(obj));
            if (zip <= 0)
                continue;

            //将当前获取到的城市名称和邮政编码,添加到 m_locations 表中。
            //m_locations 是一个包含了城市名称和邮政编码的数据表。
            DataRow r=m_locations.Rows.Find(zip);
            if (null == r)
            {
                r=m_locations.NewRow();
                r[1]=zip;
                r[2]=cityName;
                lock(m_locations)
                {
                    m_locations.Rows.Add(r);
                }
            }

            feature=fCursor.NextFeature();

            index++;
        }

        //释放 fCursor
        Marshal.ReleaseComObject(fCursor);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Trace.WriteLine(ex.Message);
    }
}

实现独立的 ArcObjects 应用程序

如 MSDN 网站上【Thread.ApartmentState 属性】 页面所述,“在 .NET Framework 2.0 版中,如果新线程在启动前尚未设置其套间状态(apartment state),则将其初始化为 ApartmentState.MTA。应用程序的主线程默认初始化为 ApartmentState.MTA。以前,将应用程序主线程设置为 ApartmentState.STA,是通过在第一行代码上设置 System.Threading.ApartmentState 属性来进行的,现在,改用 STAThreadAttribute。

作为 ArcObjects 开发人员,这意味着,如果您的应用程序未初始化为 STA 应用程序,.NET Framework 将为所有 ArcObjects 创建一个特殊的 STA 线程(因为 ArcObjects 被标记为 STA)。这将导致在每次从应用程序调用 ArcObjects 时,从主线程切换到该线程;反过来,这会强制 ArcObjects 组件编组每个调用;最终调用 COM 组件的速度可能会慢上 50 倍。

幸运的是,这可以通过简单的方式来避免,将 main 函数标记为 [STAThread] 。

以下代码示例,将控制台应用程序标记为 STA:

[C#]

namespace ConsoleApplication1
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            // ...
        }
    }
}

如果您使用【项目向导】创建 Windows 应用程序,它会自动将 [STAThread] 放在 main 函数上。

使用托管 ThreadPool 和 BackGroundWorker 线程

【线程池线程】都是后台线程(译注:此处指由静态类 System.Threading.ThreadPool 负责管理的线程)。线程池通过为您的应用程序提供由系统管理的工作线程,使您能够更有效地使用线程。与为每个任务创建新线程相比,使用线程池的优势在于消除了线程创建和销毁开销,这可以带来更好的性能和更好的系统稳定性。

但是,根据设计,所有【线程池线程】都在 MTA 中,因此,不适用于运行 ArcObjects(ArcObjects 被标记为 STA)【泽注:即 System.Threading.ThreadPool 类”不可用“】。

要解决此问题,您有几个选择:

  • 实现一个标记为 STAThread 的 ArcObjects 专用线程,并将来自 MTA 线程的每个调用,都委托给这个 ArcObjects 专用线程。
  • 使用自定义 STA 线程池,例如,标记为 STAThread 的线程数组来运行 ArcObjects。

以下代码示例,演示了使用 STAThread 线程数组对 RasterDataset 进行子集化(使用不同的线程对每个栅格波段进行子集化):

[C#]

/// <summary>
/// 用于将任务信息传递给工作线程的类
/// </summary>
public class TaskInfo
{
    ... 
    public TaskInfo(int BandID, ManualResetEvent doneEvent)
    {
        m_bandID=BandID;
        m_doneEvent=doneEvent;
    }
    ...
}

... 
public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
    ...
    // 运行子集化线程,该线程将分离出多个单独的子集化任务。
    // 默认情况下,此线程将作为 MTA 运行。这因为它使用了 WaitHandle.WaitAll() 。
    // 这个方法必须在 MTA 中调用。
    Thread t=new Thread(new ThreadStart(SubsetProc));
    t.Start();
    
    //译注:也可以是以下方法开启,Task默认为 MTA 
    //System.Threading.Tasks.Task.Run(()=> SubsetProc());
}

/// <summary>
/// 进行子集化,为栅格的每个波段创建一个专用线程
/// </summary>
private void SubsetProc()
{
    ... 
    //创建线程数组。
    Thread[] threadTask=new Thread[m_intBandCount];

    //每个子线程将 子集化 一个不同的栅格波段。
    //子集化栅格波段所需的所有信息,将由用户定义的 TaskInfo 类传递给任务。
    for (int i=0; i < m_intBandCount; i++)
    {
        TaskInfo ti=new TaskInfo(i, doneEvents[i]);
        ... 
        //为栅格波段 分配 子集化线程,注意运行 ArcObjects 所需的 STA
        threadTask[i]=new Thread(new ParameterizedThreadStart(SubsetRasterBand));
        threadTask[i].SetApartmentState(ApartmentState.STA);
        threadTask[i].Name="Subset_" + (i + 1).ToString();

        //启动任务并传递任务信息
        threadTask[i].Start((object)ti);
    }
    ... 
    //等待所有线程完成它们的任务...
    WaitHandle.WaitAll(doneEvents);
    ...
}

/// <summary>
/// 子集化,需运行 ArcObjects
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
    //必须将状态对象转换为正确的类型,
    //因为 WaitForTimerCallback 委托的签名指定了 Object 类型。
    TaskInfo ti=(TaskInfo)state;

    //反序列化 workspace 的连接属性
    IXMLSerializer xmlSerializer=new XMLSerializerClass();
    object obj=xmlSerializer.LoadFromString(ti.InputRasterWSConnectionProps, null, null);

    IPropertySet workspaceProperties=(IPropertySet)obj;
    ...
}

【泽注,此代码示例,开启了三个层级的线程:运行 OnMouseDown 的最初线程(主线程),1个;运行 SubsetProc 的副线程,1个;运行 SubsetRasterBand 的任务线程,多个(即 m_intBandCount 个)】

同步执行并发运行的线程

在许多情况下,您必须同步正在并发运行的线程。 通常,您必须等待一个或多个线程完成其任务;或在满足特定条件时,向等待中的线程发出信号,以恢复其运行;或测试给定线程是否处于活动、运行的状态;或更改线程优先级;或给出其他一些指示。

在 .NET 中,有几种方法用来管理线程的执行。 用于线程管理的主要类, 有如下这些类:

  • System.Threading.Thread - 用于创建和控制线程,更改线程优先级,以及获取线程状态。

  • System.Threading.WaitHandle - 定义一种信号机制,允许您限制对代码块的访问。WaitHandle.WaitAll() 方法 与 WaitHandle.WaitAny() 将阻止当前线程,直到收到信息。常与 System.Threading.ManualResetEvent 搭配着一起实现 同步功能。

    • 只有被标记为 MTA 的线程,才可以调用 WaitHandle.WaitAll() 方法。

      要想同步多个任务,您首先必须运行一个工作线程(即 MTA 线程),该工作线程又将运行多个子线程(即 STA 线程)。

  • System.Threading.AutoResetEvent and System.Threading.ManualResetEvent - 用于通知等待线程有事件发生,允许线程通过信号相互通信。

  • System.Threading.Monitor - 相似于 System.Threading.WaitHandle 类,提供了一种同步访问对象的机制。Monitor.Enter(object) 锁定一个对象,表示仅当前线程可以访问它,其他如果访问它的话,则需要等待; Monitor.Exit(object) 释放被锁定的对象,其他线程方可得到访问它的权限。

以下代码示例,扩展了上一节的示例:

  • 它演示了使用 WaitHandle 类来等待多个子线程完成它们的任务(通过 ManualResetEvent 来实现)。

  • 它还演示了使用 AutoResetEvent 类来阻止正在运行的子线程,阻止子线程【访问共享资源的代码块】【AutoResetEvent .WaitOne() 方法可以停止当前线程】。

    • 主线程(父线程)发出信号,其中一子线程得以继续执行。

    • 在当前子线程完成其任务时发出信号,以让下一个被阻止的子线程得以继续执行。

[C#]

/// <summary>
/// 主线程传递给子线程的信息。
/// </summary>
public class TaskInfo
{
    //用来通知主线程,子线程任务已完成
    private ManualResetEvent m_done Event;
    
    ... 
    public TaskInfo(int BandID, ManualResetEvent doneEvent)
    {
        m_bandID=BandID;
        m_doneEvent=doneEvent;
    }

    ... 

    public ManualResetEvent DoneEvent
    {
        get
        {
            return m_doneEvent;
        }
        set
        {
            m_doneEvent=value;
        }
    }
}

//用于阻止对共享资源的访问(即栅格数据集),注意:m_autoEvent 是静态的
private static AutoResetEvent m_autoEvent=new AutoResetEvent(false);

... 
public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
    ... 
    // 运行子集化线程,该线程将分离出多个单独的子集化任务。
    // 默认情况下,此线程将作为 MTA 运行。这因为它使用了 WaitHandle.WaitAll() 。
    // 这个方法必须在 MTA 中调用。
}

/// <summary>
/// Main subset method.
/// </summary>
private void SubsetProc()
{
    ... 
    //创建 ManualResetEvent 以通知主线程所有 线程池中的子线程都已完成其任务。
    ManualResetEvent[] doneEvents=new ManualResetEvent[m_intBandCount];

    //为栅格的每个波段,创建一个子线程(在线程池中)。每个子线程将子集化一个不同的栅格波段。 
    //对栅格波段进行子集化所需的所有信息都将由用户自定义的 TaskInfo 类传递给 子线程。
    for (int i=0; i < m_intBandCount; i++)
    {
        //创建 ManualResetEvent,用于向【等待线程(父线程)】发出任务已完成的信号。
        doneEvents[i]=new ManualResetEvent(false);

        TaskInfo ti=new TaskInfo(i, doneEvents[i]);
        ... 
       //为栅格波段 分配 子集化线程,注意运行 ArcObjects 所需的 STA
        threadTask[i]=new Thread(new ParameterizedThreadStart(SubsetRasterBand));
        threadTask[i].SetApartmentState(ApartmentState.STA);
        threadTask[i].Name="Subset_" + (i + 1).ToString();

        //启动任务并传递任务信息
        threadTask[i].Start((object)ti);
    }

    //允许等待线程继续进行
    //让子线程可以访问共享资源(即栅格数据集)
    m_autoEvent.Set();

    // 等待所有线程完成它们的任务...
    WaitHandle.WaitAll(doneEvents);
    ...
}

/// <summary>
/// 子集化,需运行 ArcObjects
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
    //对象 state 转换为正确的类型
    TaskInfo ti=(TaskInfo)state;
    ... 

    //阻止当前子线程,直到 m_autoEvent 收到信号。
    //即在多个子线程中,只有一个子线程,可以获得到独占式访问的权限。
    //第一个信号由主线程发出,即父线程发出信号,其中一个子线程收到信号,从而获得到权限;
    //而其他子线程需要继续等待。
    m_autoEvent.WaitOne();

    //在此处插入代码,运行您的业务逻辑。
    //此处代码,包含对共享资源的访问。

    //通知【下一个可用子线程】,以让其获取写访问权限。
    m_autoEvent.Set();

    //通知主线程,当前子线程已完成其任务。
    ti.DoneEvent.Set();
}

在多个线程间共享托管类型

有时,您的 .NET 应用程序的底层数据结构是诸如 DataTable 或 HashTable 这样的托管对象。这些 .NET 托管对象事实上是允许您在多个线程之间共享它们的(例如数据获取线程和主呈现线程)。但是,不管如何,您都应该查阅 MSDN 网站,以验证托管对象是否是线程安全的。一般来说,对于读取,托管对象是线程安全的,但对于写入,却不一定是。另外,有一些集合类,它们实现了同步的方法,因为该方法提供了一个围绕底层集合的同步包装器。

如果您创建的对象可以被多个线程访问,您应该根据 MSDN 中有关此特定对象的线程安全部分,获取【访问这个对象的】排他锁。可以使用上一节中描述的同步方法,或使用 lock 语句【lock 语句通过获取给定对象的互斥锁,将”代码块“标记为临界区。它确保当前线程被阻塞,直到”给定对象“被其他线程释放(退出锁),或当前线程是第一个访问给定对象的线程】。

The following screen shot demonstrates sharing a DataTable by multiple threads. First, check the DataTable Class on the MSDN Web site to verify if it is thread safe.

以下屏幕截图,演示了多个线程共享 DataTable。

首先,在 MSDN 网站上,检查 DataTable Class 是否是线程安全的。

Screen shot of MSDN DataTable Class.

页面显示:“这种类型对于多线程读取操作是安全的。您必须同步任何写入操作。”

这意味着从 DataTable 中读取信息不是问题,但是当您要写入时,您必须同步线程,阻止其他线程访问。 以下代码示例,显示了如何阻止其他线程:

[C#]

private DataTable m_locations=null;
... 

DataRow rec=m_locations.NewRow();
rec["ZIPCODE"]=zipCode; //邮政编码
rec["CITYNAME"]=cityName; //城市名称

//锁定表并添加新记录
lock(m_locations)
{
    m_locations.Rows.Add(rec);
}

从后台线程更新 UI

在使用“后台线程” 执行冗长操作的时候,您往往希望向用户报告与【后台线程执行的任务】相关的进度、状态、错误或其他信息。 这可以通过更新 UI 上的控件来完成。 但是,在 Windows 中,UI 控件通常都是绑定到特定线程上的,并且不是线程安全的。

因此,您必须将任何【对 UI 控件的调用】委托给(封送给)“控件所属的线程”。

泽注:即“对控件的更新操作”都交由“控件所属的线程”来执行。例如,“控件所属的线程”可能正在重绘控件外观,而“后台线程”此时改变控件外观,就会造成画面混乱。如果发生这样的情况,就会触发异常,异常信息大致是“不能从不是创建该控件的线程调用它”。

“后台线程” 通过调用控件的 Invoke 方法,使”控件所属的线程“执行【委托所关联的方法】。而在此之前,“后台线程” 通过控件的 InvokeRequired 属性,判断是否必须调用 invoke 方法。

下面的代码示例,演示了如何将“后台任务的进度”报告到用户界面中去。

[C#]

public class WeatherItemSelectionDlg : System.Windows.Forms.Form
{
	//1.声明一个委托。通过该委托,您可以将信息传递给控件:
	private delegate void AddListItmCallback(string item);
	//...
    
	//对控件进行线程安全的调用:
    //2.设置更新 UI 控件的方法。注意对 Invoke 的调用。
    //  该方法必须与之前声明的委托具有相同的签名。
	private void AddListItemString(string item)
	{
		// InvokeRequired 将比较【调用线程的 ID】与【创建线程的 ID】,
        // 以判断二者是否同属一个线程中。
		// 如果这些线程不同,则返回 true。
		if (this.lstWeatherItemNames.InvokeRequired)
		{
			//让主线程执行自己
			AddListItmCallback d = new AddListItmCallback(AddListItemString);
			this.Invoke(d, new object[] { item });          
            //译注:此处将 item 作为参数,传递给委托 d 封装的方法。 
            //如下两个函数签名,是.NET 上关于 Invoke 方法的两个重载。
            //public object Invoke(Delegate method);
            //public object Invoke(Delegate method, params object[] args);
		}
		else
		{
			//保证在主 UI 线程上运行:
			this.lstWeatherItemNames.Items.Add(item);
		}
	}
    
    //3.后台线程执行冗长操作,并发起对 UI 控件的更新
    //在本例中,后台线程将执行 AddListItemString 方法中 控件 InvokeRequired 属性为 true 的部分;
    //然后,主线程将执行 InvokeRequired 属性为 false 的部分。
    private void PopulateSubListProc()
	{
		//在此处插入代码,运行您的业务逻辑。
        
        //更新 UI 控件。传递所需的数据,在本例中为字符串 item
		frm.AddListItemString(item);
	}
    
    //...
    
    //4.生成后台线程,传入步骤 3 中编写的方法,并启动:
    private void GenerateThread()
	{
		Thread t = new Thread(new ThreadStart(PopulateSubListProc));
		t.Start();
	}
}

从主线程以外的线程调用 ArcObjects

在许多“多线程应用程序”中,您需要从不同的线程调用(另一个线程的) ArcObjects。 例如,您可能有一个【从 Web 服务获取 response 的】后台线程,该线程“向地图添加新项目的显示”、更改地图范围,或运行 GP 工具以执行某种分析。

另一个非常常见的例子,从计时器(System.Timers.Timer)事件处理程序方法中调用 ArcObjects。 在 ThreadPool 任务上(不是主线程的线程),引发计时器的 Elapsed 事件(达到间隔时发生)。 然而它需要使用 ArcObjects,这似乎需要跨 apartment 调用。但是,通过将 ArcObjects 组件视为 UI 控件,并使用 Invoke 方法,将调用委托给【创建 ArcObjects 组件的主线程】,可以避免这种情况的发生(跨 apartment 调用)。因此,不会进行出现跨 apartment 调用 ArcObjects。

ISynchronizeInvoke(System.ComponentModel.ISynchronizeInvoke)接口包括 Invoke、BeginInvoke 和 EndInvoke 方法,实施这些方法可能是一项艰巨的任务。相反,让您的类直接继承 System.Windows.Forms.Control ,或拥有一个继承自 Control 的辅助类,会是一个轻松的办法。 后述的任一选项,都为调用方法提供了简单而有效的解决方案。

泽注:ISynchronizeInvoke 接口,属于.Net 旧版本中广泛使用的异步编程模型 APM 的一部分。

C#的三种异步的详细介绍及实现:

 * 异步编程模型 (APM,Asynchronous Programming Model) ;
 
 * 基于事件的异步模式 (EAP,Event-based Asynchronous Pattern)
 
 * 基于任务的异步模式 (TAP, Task-based Asynchronous Pattern)

译注:详情可以参阅: C#的三种异步的详细介绍及实现 。此处可以讨论,是否可以使用更新的技术(比如,async/await)来实现相同的功能?该功能能够做到,既不使用 ISynchronizeInvoke 接口,也不使用控件,还能使代码简洁、实现起来方便、易读懂。

以下代码示例,使用用户自定义的 InvokeHelper 类(继承自 Control 的辅助类),来调用计时器的 Elapsed 事件处理程序,以重新居中地图的可见边界,并设置地图的旋转。

除了传递【带信息的结构体】之外,某些应用程序逻辑,也必须在 InvokeHelper 类上完成。

[C#]

 /// <summary>
/// 一个辅助类,让主线程执行委托的方法
/// </summary>
public sealed class InvokeHelper : Control
{
	//声明一个委托,用于将方法传递给主线程。
	public delegate void MessageHandler(NavigationData navigationData);

	//私有成员
	private IActiveView m_activeView;
	private IPoint m_point = null;

	/// <summary>
	/// 一个传入 ActiveView 的构造函数
	/// </summary>
	public InvokeHelper(IActiveView activeView)
	{
		//确保控件已创建,并且具有有效的句柄。
		this.CreateHandle();
		this.CreateControl();

		m_activeView = activeView;
	}

	/// <summary>
	/// 将 CenterMap 方法交给主线程执行。
	/// </summary>
	public void InvokeMethod(NavigationData navigationData)
	{
		if (!this.IsDisposed && this.IsHandleCreated)
		{
			//译注:此处 被调用的 Invoke 方法来自基类 Control
			Invoke(new MessageHandler(CenterMap), new object[] { navigationData });
		}
	}

	/// <summary>
	/// 通过委托而被执行的方法。译注:与声明委托时,函数签名一致。该方法在主线程上执行。
	/// </summary>
	public void CenterMap(NavigationData navigationData)
	{
		//获取当前地图可见范围。
		IEnvelope envelope =
			m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds;
		if (null == m_point)
		{
			m_point = new PointClass();
		}

		//设置新的地图中心坐标。
		m_point.PutCoords(navigationData.X, navigationData.Y);
		//围绕新坐标将地图居中。
		envelope.CenterAt(m_point);
		m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds = envelope;
		//将地图旋转到新的旋转角度。
		m_activeView.ScreenDisplay.DisplayTransformation.Rotation =
			navigationData.Azimuth;//Azimuth 方位角
	}

	/// <summary>
	/// 控制初始化。
	/// </summary>
	private void InitializeComponent() { }
}

/// <summary>
/// 被传递的信息(结构体)
/// </summary>
public struct NavigationData
{
	public double X;
	public double Y;
	public double Azimuth;//方位角

	/// <summary>
	/// 构造器
	/// </summary>
	public NavigationData(double x, double y, double azimuth)
	{
		X = x;
		Y = y;
		Azimuth = azimuth;
	}
}

/// <summary>
/// 此命令触发跟踪功能
/// 点击之后,
/// </summary>
public sealed class TrackObject : BaseCommand
{
	//Class members.
	private IHookHelper m_hookHelper = null;
	//... 
	private InvokeHelper m_invokeHelper = null;
	private System.Timers.Timer m_timer = null;

	//... 

	/// <summary>
	/// 在创建此命令时发生。
	/// </summary>
	/// <param name="hook">一个应用程序的实例</param>
	public override void OnCreate(object hook)
	{
		//... 
		//实例化计时器。
		m_timer = new System.Timers.Timer(60);
		m_timer.Enabled = false;

		//设置计时器的 Elapsed 事件的处理程序。
		m_timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
	}

	/// <summary>
	/// 单击此命令时发生。
	/// </summary>
	public override void OnClick()
	{
		//创建 InvokeHelper 类。
		if (null == m_invokeHelper)
		{
			m_invokeHelper = new InvokeHelper(m_hookHelper.ActiveView);
		}

		//... 
		//启动计时器。
		if (!m_bIsRunning)
			m_timer.Enabled = true;
		else
			m_timer.Enabled = false;
		//...
	}

	/// <summary>
	/// 计时器的 Elapsed 事件的处理程序。
	/// </summary>
	private void OnTimerElapsed(object sender, ElapsedEventArgs e)
	{
		//... 
		//创建 NavigationData 。
		NavigationData navigationData = new NavigationData(currentPoint.X, 
			currentPoint.Y, azimuth);

		//更新地图范围和旋转。
		m_invokeHelper.InvokeMethod(navigationData);
		//...
	}
}
译注:

使用继承自 Control 的辅助类,本质是:作为 STA 线程的调用方,使用“从后台线程更新 UI”的方式,调用另一个 STA 线程的上控件,来实现特定的任务(该任务使用了 ArcObjects)[任务在被调用方所在的线程上执行]。这样就避免了跨 apartment 调用。

更深一点的本质是:在多个STA线程之间,实现对共享资源 m_activeView 的同步。

要点:

 * 由被调用方创建辅助类实例;
 * 调用方调用 辅助类实例的 InvokeMethod 方法。
 
示例中,

 * 运行 OnClick() 的线程,有 m_hookHelper.ActiveView 对象,因为 m_invokeHelper 在这个方法内创建;
 * 运行 OnTimerElapsed() 的线程,调用 辅助类实例 m_invokeHelper 的 InvokeMethod 方法,该线程由计时器 Elapsed 事件的处理程序产生。
	
	运行 OnTimerElapsed() 的线程,由计时器创建。计时器有一个计时线程,为了不阻塞计时线程,必然需要另外的线程来执行 Elapsed 事件的回调方法 OnTimerElapsed,而不是在每次符合触发条件时一直在计时线程上执行回调方法。每次新开线程执行回调方法时,都通过控件的 Invoke 方法,让创建控件的线程来执行控件的更新。这样做,间接地避免了跨 apartment 调用 m_hookHelper.ActiveView 对象。

带地理处理(geoprocessing)的多线程

要在异步或多线程应用程序中使用地理处理,请使用以下选项之一:

  • 在 ArcGIS Server 9.2 及更高版本中,使用 geoprocessing 服务。 这使得桌面应用程序能够以异步执行模式处理 geoprocessing,并相互异步运行多个 geoprocessing 工具。 How to work with geoprocessing services 中描述了【执行服务器工具和听取反馈的】机制。
  • 在 ArcGIS 10 中,使用 Geoprocessor.ExecuteAsync 方法。您可以对 ArcGIS 应用程序异步执行工具。 这意味着当工具在后台进程中执行时,ArcGIS 桌面应用程序或控件(例如,MapControl、PageLayoutControl、GlobeControl 或 SceneControl)保持对用户响应。 换句话说,可以在工具执行输入数据集时查看和查询数据。 这在 Running a geoprocessing tool using background geoprocessing 中有更全面的描述。
posted @ 2022-03-14 13:41  误会馋  阅读(162)  评论(0编辑  收藏  举报