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

发布于 2024-11-01 08:18:03 字数 3276 浏览 1 评论 0原文

我有一个应用程序,可以通过自动化插件执行一些 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
        }
    }
}

编辑:可以在一个简单的测试中重现该错误(读取行只是为了给 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();

I have an application that does some excel automation through an automation add in.
This add-in is multithreaded, and all the threads manage to make calls to the excel COM objects. Because excel can sometimes return a "is busy" exception when making multiple calls, i have wrapped all my calls in a "retry" function. However i feel this is inneficient.
I am now trying to make all the calls to excel objects on the same thread, so that all calls are "serialized" by me, therefore reducing the risk of excel returning a "is busy" exception.
However when this thread tries to access an excel object, the application hangs. I have tried setting the thread to STA or MTA to no avail.

The code i use to launch everything from a single thread is as follows:
The "offending" part should be in "DoPass",maybe the way i am invoking the Delegate is somehow wrong.

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
        }
    }
}

EDIT: the error can be reproduced in a simple test (the readline is just there to give time to the ExcelConnector thread to work):

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();

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

木格 2024-11-08 08:18:03

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

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

另一个副作用是死锁并不罕见。当 STA 线程保持忙碌且不启动消息循环时,就会发生这种情况。封送是由 COM 管道代码完成的,该代码依赖于消息循环来分派调用。这种情况很容易调试,您可以使用“调试 + 全部中断”、“调试 + Windows + 线程”并检查 STA(或主)线程正在忙什么。

另请注意,尝试这种线程处理可能是导致此互操作异常的 90% 的原因。尝试使用本质上线程不安全的代码来同时执行多件事是行不通的。您可以通过互锁您自己的代码来避免 Excel 中的“忙”异常,标记一个将 Excel 置于“忙”状态的操作,以便您退避其他线程。做起来当然很痛苦。

Unfortunately there is no point in what you are doing, this is already being done. The Office interop is based on out-of-process COM. Like many COM interfaces, the Excel interfaces are marked as apartment threaded in the registry. Which is an expensive way of saying they don't support threads.

COM automatically takes care of components that don't support threading, it automatically marshals calls made on a worker thread to the thread that created the COM object. Which should be a thread that's STA, like the main thread of any program that has a user interface. It will create an STA thread automatically if necessary. One side effect of this marshaling is that the calls made by the worker threads are automatically serialized. After all, the STA thread can only dispatch one call at a time.

Another side-effect is that deadlock is not uncommon. Which will happen when the STA thread stays busy and doesn't pump the message loop. The marshaling is done by COM plumbing code that relies on the message loop to dispatch the calls. This condition is pretty easy to debug, you'd use Debug + Break All, Debug + Windows + Threads and check what the STA (or Main) thread is busy with.

Also beware that attempting this kind of threading is probably 90% of the reason you get this interop exception in the first place. Trying to get code that's fundamentally thread-unsafe to do more than one thing at the same time just doesn't work well. You'd avoid the "is busy" exception from Excel by interlocking your own code, marking an operation that puts Excel in that 'busy' state so you back-off other threads. Painful to do of course.

浮华 2024-11-08 08:18:03

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

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

You have to initialize COM in each thread you wish to use your COM library. From the documentation of CoInitializeEx "CoInitializeEx must be called at least once, and is usually called only once, for each thread that uses the COM library.".

Instead of trying to implement your own threading perhaps you should check .NET's Task Parallel Library. Check this question on using COM objects from TPL. Essentially, you just create Task objects and submit them to an StaTaskScheduler for execution. The scheduler manages the creation and disposal of threads, raising exceptions etc.

亽野灬性zι浪 2024-11-08 08:18:03

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

I'm not really knowledgeable about C#, but my guess is that you still have to initialize COM in some way when starting a new thread in order to have a message box which can be used to signal your thread when the operation completed.

妞丶爷亲个 2024-11-08 08:18:03

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

我认为你只是死锁在某个地方。启动调试器,当它挂起时,中断并为每个可以锁定的对象(actionsToRun>结果和其他)。

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

It's usually not necessary to initialize COM in .NET -- unless you've done some native things such as P/Invoking. Using IDispatch does not require explicit initialization of COM.

I think you're just dead locking somewhere. Fire up your debugger, when it hangs, break in, and type Monitor.TryEnter(...) for each of the object that you can lock (actionsToRun, results and others).

BTW, you should really consider restructuring your application to not use Excel concurrently -- even if you do some kind of "serialization". Even if you get it work, it will come back and bite you forever. It's officially discouraged, and I speak from experience.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文