C# Marshalling COM objects between threads
Asked Answered
T

1

5

I'm getting very confused about whether C# marshal's COM objects between threads. To this end I have an application which is loading a set of files in a task parallel fashion. I'm using the StaTaskScehduler to load the files using the COM object. Once the COM object is loaded I am storing the object in a central list.

I then later try to perform some processing on this data, again using the STATaskScheduler. However at this point I hit a problem. I receive an exception as follows:

An unhandled exception of type 'System.Runtime.InteropServices.InvalidComObjectException' occurred in MadCat.exe

Additional information: COM object that has been separated from its underlying RCW cannot be used

Now my understanding is that I receive this error because the object hasn't been marshalled into the new thread. I thought this was something C# does for you?

How can I create an apartment threaded COM object in one thread and then use it from another thread?

Am I barking up the wrong tree here? Should I not even be using the Sta apartment for my threads? I can guarantee that the object is never access from multiple threads simultaneously. Any thoughts much appreciated.

Edit: The COM object is defined as follows:

[
    coclass,
    threading( apartment ),
    vi_progid( [Namespace.Class] ),
    progid( [Namespace.Class].6 ),
    version( 6.0 ),
    uuid( GUID_C[Class] ),
    helpstring( [Class]" Class" )
]

So by my understanding this is an apartment threaded object, right? I've just tried using a modified task scheduler that doesn't set the apartment state (MTA by default?). This object then does seem to work when I create it in one thread and use it from another. Is this safe or will it come back to bite me some other way?

COM's threading model has always confused the hell out of me :/

Tessie answered 16/7, 2014 at 12:2 Comment(12)
I've only seen this error when code attempted to call into the COM object after it was released by Marshal.ReleaseComObject. But I've never tried to use an STA-threaded COM object on multiple threads, which to my knowledge will not work. That is, you can only use such a COM object on the thread that created it. Also, if it's documented as STA-threaded then it should be used on an STA thread. If you must use the object in this way I think you'll need to add thread synchronization: other threads must request the creator thread to call into the COM object.Acaulescent
The times I have seen this error are instances where Marshal.ReleaseComObject was called on an instance created from a COM callable wrapper followed by an attempt to call a method on that instance.Spawn
COM (not .NET) does marshaling for you if the COM objects are correctly declared in the registry with respect to how they handle multithreading (Both, Apartment, etc.). How are declared the objects you're using?Cadaverous
That StaTaskScheduler causes a whole lot more problems then it ever solves, it seems. Rock-hard rule is that the thread that owns the object must stay alive to keep the object alive. Running tasks on the UI thread with StaTaskScheduler is a rather pointless endeavor. Consider giving these COM objects a safe home with a dedicated thread that you can keep running.Maddy
@HansPassant: So if the thread it is created on is kept alive then COM will be able to marshall it to a different thread?Tessie
COM automatically marshals calls made on worker threads to the thread that hosts the COM object. Doesn't otherwise have anything to do with why your code crashed, the thread isn't running anymore so the underlying object got automatically destroyed. But the wrapper object in your code didn't. Trying to use it causes this exception.Maddy
@HansPassant, new StaTaskScheduler(numberOfThreads:1) does run a dedicated STA thread for the lifetime of StaTaskScheduler, until StaTaskScheduler.Dispose has been called. Which can be when the app shuts down. If used correctly, it's a perfect home for STA COM objects.Highness
@HansPassant: Is it worth me changing all the C++ COM Objects to have an apart model of "both" for better multi-threading performance? I do, afterall, have access to the COM library (even if it was written by a third party). Other than the threadsafe joys is there any other gotchas I need to worry about with doing this?Tessie
Don't mess with it. Writing thread-safe code requires a lot more than changing a registry key. The bugs you get are undebuggable.Maddy
@HansPassant: I've written a lot of multi-threaded non COM code in the past so I'm aware of the problems with doing that, sadly ... But assuming the classes ARE thread safe then it should be ok to have them as "both" right?Tessie
Hmm, COM programmers don't use the wrong ThreadingModel by accident. Thread-safety is designed, not acquired or hoped for. Of course nobody can talk you out of it if you already know all this.Maddy
Well the COM object in question is written by me and I'm pretty sure its thread-safe (bugs willing! ;)). You are probably right on the other classes in the library, however ...Tessie
H
4

It appears you're using Stephen Toub's StaTaskScheduler as a part of some "stateful" logic, where your COM objects live across StartNew boundaries. If that's the case, make sure you create and use these objects on the same StaTaskScheduler STA thread and nowhere outside it. Then you wouldn't have to worry about COM marshaling at all. Needless to say, you should create StaTaskScheduler with only one thread, i.e., numberOfThreads:1.

Here's what I mean:

var sta = new StaTaskScheduler(numberOfThreads:1);

var comObjects = new { Obj = (ComObject)null };

Task.Factory.StartNew(() =>
{
    // create COM object
    comObjects.Obj = (ComObject)Activator.CreateInstance(
        Type.GetTypeFromProgID("Client.ProgID"));
}, CancellationToken.None, TaskCreationOptions.None, sta);

//...

for(int i=0; i<10; i++)
{
    var result = await Task.Factory.StartNew(() =>
    {
        // use COM object
        return comObjects.Obj.Method();    
    }, CancellationToken.None, TaskCreationOptions.None, sta);
}

If Obj.Method() returns another COM objects, you should keep the result in the same StaTaskScheduler's "apartment" and access it from there, too:

var comObjects = new { Obj = (ComObject)null, Obj2 = (AnotherComObject)null };
//...
await Task.Factory.StartNew(() =>
{
    // use COM object
    comObjects.Obj2 = comObjects.Obj.Method();    
}, CancellationToken.None, TaskCreationOptions.None, sta);

If you also need to handle events sourced by Obj, check this:

Highness answered 16/7, 2014 at 13:24 Comment(5)
Id rather be able to, somehow, pass the COM objects between threads. Something like the old CoMarshallInterthreadInterfaceInStream ...?Tessie
I've updated my question with a small amount more info as well!Tessie
@Goz, then maybe you don't need StaTaskScheduler at all. If you create an STA COM object on an MTA thread, COM will create an implicit STA apartment (for that and other STA objects like that) and will marshal a proxy to the STA object to your MTA client. This is why it works as described in your update.Highness
It may work, but also may easily lead to deadlocks, especially if the COM object sources some events or other callbacks to MTA clients. Here is a brief yet comprehensive crash course: support.microsoft.com/kb/150777Highness
@Goz, if you prefer to use COM marshaling, consider using Global Interface Table (GIT) rather than CoMarshallInterthreadInterfaceInStream, it's just easier. I have a code sample here: https://mcmap.net/q/1166737/-cannot-pass-a-gchandle-across-appdomains-solution-without-delegatesHighness

© 2022 - 2024 — McMap. All rights reserved.