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