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 :/

Goz
  • 61,365
  • 24
  • 124
  • 204
  • 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. – groverboy Jul 16 '14 at 12:20
  • 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. – Daniel Kelley Jul 16 '14 at 12:20
  • 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? – Simon Mourier Jul 16 '14 at 12:47
  • 1
    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](http://stackoverflow.com/a/21684059/17034) that you can keep running. – Hans Passant Jul 16 '14 at 13:34
  • @HansPassant: So if the thread it is created on is kept alive then COM will be able to marshall it to a different thread? – Goz Jul 16 '14 at 14:04
  • 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. – Hans Passant Jul 16 '14 at 14:09
  • @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. – noseratio Jul 16 '14 at 14:38
  • @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? – Goz Jul 16 '14 at 14:42
  • Don't mess with it. Writing thread-safe code requires a lot more than changing a registry key. The bugs you get are undebuggable. – Hans Passant Jul 16 '14 at 14:52
  • @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? – Goz Jul 16 '14 at 14:54
  • 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. – Hans Passant Jul 16 '14 at 14:58
  • 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 ... – Goz Jul 16 '14 at 15:05

1 Answers1

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:

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Id rather be able to, somehow, pass the COM objects between threads. Something like the old CoMarshallInterthreadInterfaceInStream ...? – Goz Jul 16 '14 at 14:10
  • I've updated my question with a small amount more info as well! – Goz Jul 16 '14 at 14:10
  • @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. – noseratio Jul 16 '14 at 14:24
  • 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: http://support.microsoft.com/kb/150777 – noseratio Jul 16 '14 at 14:27
  • @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: http://stackoverflow.com/a/24121119/1768303 – noseratio Jul 16 '14 at 14:42