从单线程进行COM调用挂起线程 [英] Making COM calls from single thread hangs the thread

查看:156
本文介绍了从单线程进行COM调用挂起线程的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个应用程序,通过自动添加进行一些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);


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


// run
foreach(copy pair中的var pair)
{
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();不幸的是,你在做什么没有意义,因为我们不知道你在做什么,这已经在做。 Office互操作基于进程外COM。像许多 COM接口,Excel接口在注册表中标记为公寓线程。



COM会自动处理不支持线程的组件,它会自动调度对worker的调用线程到创建COM对象的线程。其中应该是一个线程的STA,像任何程序的主线程有一个用户界面。如果需要,它将自动创建STA线程。这个编组的一个副作用是,工作线程所做的调用被自动序列化。毕竟,STA线程每次只能调度一个调用。



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



还要注意,尝试这种线程可能是90%的原因,你得到这个互操作异常首先。试图获得基本上线程不安全的代码,同时做多个事情只是不能很好地工作。您可以通过互锁您的自己的代码,标记一个将Excel设置为忙状态的操作,以避免其他线程退出,从Excel中避免正忙异常。当然是痛苦的。


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

解决方案

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.

这篇关于从单线程进行COM调用挂起线程的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆