【问题标题】:Making COM calls from single thread hangs the thread从单个线程进行 COM 调用会挂起线程
【发布时间】:2011-08-04 14:44:51
【问题描述】:

我有一个应用程序通过自动化插件执行一些 excel 自动化。 这个加载项是多线程的,所有线程都设法调用 excel COM 对象。因为 excel 在进行多次调用时有时会返回“正忙”异常,所以我将所有调用都包含在“重试”函数中。但是我觉得这是无用的。 我现在尝试在同一个线程上对 excel 对象进行所有调用,以便所有调用都由我“序列化”,从而降低 excel 返回“正忙”异常的风险。 但是,当此线程尝试访问 excel 对象时,应用程序会挂起。我尝试将线程设置为 STA 或 MTA 无济于事。

我用来从单个线程启动所有内容的代码如下: “违规”部分应该在“DoPass”中,也许我调用代表的方式有点错误。

public static class ExcelConnector
{
    public static Thread _thread;
    private static int ticket;
    public static Dictionary<Delegate, int> actionsToRun = new Dictionary<Delegate, int>();
    public static Dictionary<int, object> results = new Dictionary<int, object>();


    static ExcelConnector()
    {
        LaunchProcess();
    }

    public static int AddMethodToRun(Delegate method)
    {
        lock (actionsToRun)
        {
            ticket++;
            actionsToRun.Add(method, ticket);
        }
        return ticket;
    }

    public static bool GetTicketResult(int ticket, out object result)
    {
        result = null;
        if (!results.ContainsKey(ticket))
            return false;
        else
        {
            result = results[ticket];
            lock (results)
            {
                results.Remove(ticket);
            }

            return true;
        }
    }

    public static void LaunchProcess()
    {
        _thread = new Thread(new ThreadStart(delegate
                                                 {

                                                     while (true)
                                                     {
                                                         DoPass();
                                                     }
                                                 }));
        //    _thread.SetApartmentState(ApartmentState.STA);
        //   _thread.IsBackground = true;

        _thread.Start();
    }

    public static void DoPass()
    {
        try
        {
            Logger.WriteLine("DoPass enter");


            Dictionary<Delegate, int> copy;
            lock (actionsToRun)
            {
                copy = new Dictionary<Delegate, int>(actionsToRun);
            }


            //run
            foreach (var pair in copy)
            {
                object res = pair.Key.Method.Invoke(
                    pair.Key.Target, null);
                lock (results)
                {
                    results[pair.Value] = res;
                }
                lock (actionsToRun)
                {
                    actionsToRun.Remove(pair.Key);
                }

                Thread.Sleep(100);
            }
        }
        catch (Exception e)
        {
            Logger.WriteError(e);
            //mute
        }
    }
}

编辑:错误可以在一个简单的测试中重现(readline 只是为了给 ExcelConnector 线程工作时间):

var excelApp = new Application();
        excelApp = new Application();
        excelApp.Visible = true;
        excelApp.DisplayAlerts = false;

        System.Action act = delegate
                                {
                                    string s = excelApp.Caption;
                                    Console.WriteLine(s);

                                };
        ExcelConnector.AddMethodToRun(act);
        Console.ReadLine();

【问题讨论】:

    标签: c# multithreading excel com interop


    【解决方案1】:

    我对 C# 不是很了解,但我的猜测是在启动新线程时您仍然需要以某种方式初始化 COM,以便在操作完成时有一个消息框可用于向您的线程发出信号.

    【讨论】:

      【解决方案2】:

      您必须在希望使用 COM 库的每个线程中初始化 COM。来自CoInitializeEx 的文档“CoInitializeEx 必须至少调用一次,并且通常只调用一次,对于使用 COM 库的每个线程。”。

      也许您应该查看 .NET 的任务并行库,而不是尝试实现自己的线程。在using COM objects from TPL 上查看此问题。本质上,您只需创建 Task 对象并将它们提交给 StaTaskScheduler 以执行。调度程序管理线程的创建和处置、引发异常等。

      【讨论】:

      • 我会查看 StaTaskScheduler
      【解决方案3】:

      通常不需要在 .NET 中初始化 COM —— 除非您已经完成了一些本地操作,例如 P/Invoking。使用 IDispatch 不需要显式初始化 COM。

      我认为你只是在某个地方死锁了。启动调试器,当它挂起时,插入并为每个可以锁定的对象(actionsToRunresults 和其他对象)键入Monitor.TryEnter(...)

      顺便说一句,您确实应该考虑重组您的应用程序以不同时使用 Excel ——即使您进行了某种“序列化”。即使你得到它的工作,它会回来并永远咬你。官方不鼓励这样做,我根据经验说话。

      【讨论】:

      • 我已经检查了所有的锁,并且还进行了逐步调试,我不相信我会死锁(至少不是我正在管理的锁)。挂起发生在访问 excel 对象时。
      • @JJ15k -- 也许您不应该一次操作多个电子表格。您可以尝试实际显示 Excel 窗口(@98​​7654324@ 属性)——可能会弹出一个对话框并阻止 Excel。
      • 我已经检查了所有这些,但没有发生。该错误发生在传递给“AddMethodToRun”的最简单调用中。我在测试中重现了该错误,该错误仅试图获取 excel 窗口“标题”属性。
      【解决方案4】:

      不幸的是,你正在做的事情毫无意义,这已经在做。 Office 互操作基于进程外 COM。与许多 COM 接口一样,Excel 接口在注册表中被标记为单元线程。说他们不支持线程是一种昂贵的方式。

      COM 自动处理不支持线程的组件,它自动将工作线程上的调用编组到创建 COM 对象的线程。这应该是一个 STA 线程,就像任何具有用户界面的程序的主线程一样。如有必要,它将自动创建一个 STA 线程。这种封送处理的一个副作用是工作线程进行的调用会自动序列化。毕竟,STA 线程一次只能调度一个调用。

      另一个副作用是死锁并不少见。当 STA 线程保持忙碌并且不泵送消息循环时会发生这种情况。封送处理由 COM 管道代码完成,该代码依赖于消息循环来分派调用。这种情况很容易调试,您可以使用 Debug + Break All、Debug + Windows + Threads 并检查 STA(或 Main)线程正在忙什么。

      还要注意,尝试这种线程可能是您首先遇到此互操作异常的 90% 的原因。试图让代码从根本上说是线程不安全的以同时做不止一件事是行不通的。您可以通过联锁您的自己的代码来避免 Excel 中的“忙”异常,标记使 Excel 处于“忙”状态的操作,以便您回退其他线程。这样做当然很痛苦。

      【讨论】:

      • 我确实在代码中的某处“阻塞”了主线程,从而阻止了它处理消息。这导致了我经历的挂起。正如你所说,我一直试图“联锁”我的代码,之前介绍的课程的重点就是这样。看起来(我不是 100% 确定),正如你所说,COM 已经正确序列化了我的调用,并且大多数错误是由于其他插件/插件在我的插件尝试执行操作的同时调用 excel。
      猜你喜欢
      • 1970-01-01
      • 2012-08-02
      • 2013-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-13
      相关资源
      最近更新 更多