从单线程进行COM调用会挂起线程

时间:2011-04-13 10:54:30

标签: c# multithreading excel com interop

我有一个应用程序可以通过自动化添加来实现一些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();

4 个答案:

答案 0 :(得分:5)

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

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

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

还要注意尝试这种线程可能是90%的原因,你首先得到这个互操作异常。尝试从根本上获得线程不安全的代码同时执行多个操作并不能很好地工作。您可以通过互锁自己的代码来避免Excel中的“忙碌”异常,标记将Excel置于“忙碌”状态的操作,以便您退出其他线程。当然很痛苦。

答案 1 :(得分:1)

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

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

答案 2 :(得分:0)

我对C#并不是很了解,但我的猜测是你仍然需要在启动一个新线程时以某种方式初始化COM,以便有一个消息框,可以在操作完成时用来指示你的线程

答案 3 :(得分:0)

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

我认为你刚刚死在某个地方。启动调试器,挂起,闯入,并为您可以锁定的每个对象键入Monitor.TryEnter(...)actionsToRunresults和其他人。)

顺便说一句,你应该考虑重组你的应用程序,不要同时使用Excel - 即使你做某种“序列化”。即使你让它工作,它也会回来并永远咬你。这是正式的气馁,我是根据经验说的。