Multithreading concurrency with Interop

M

Mark Sisson

Hey all.
I've got a legacy COM dll that I'd like to use in my multithreaded C#
app. Problem though is when I create several instances of the interop
object and start them on their own threads they always seem to run
synchronously.

I've boiled down the code to a simple "lab" experiment below. In this
console app you'll see that instance one comes back after roughly 4
seconds and then instance two comes back in 8 seconds, twice the time.
This indicates that it is wait on the first thread to finish. Why
won't they run concurrently?

I've tried my best to make sure that the thread types are MTAThread
before calling the COM objects. Also, in VB I made sure the project
threading model was "Apartment Threaded" and the Instancing set to
GlobalUseMulti.

Any ideas???
Thanks



******************* .NET C# CONSOLE TEST APP ********************
using System;
using System.Threading;

namespace ComThreadTest
{
class MyConsoleApp
{
[MTAThread]
static void Main(string[] args)
{
TestClass test1 = new TestClass("1");
TestClass test2 = new TestClass("2");

Thread th = new Thread(new ThreadStart(test1.Go));
th.ApartmentState = ApartmentState.MTA;
th.Start();

th = new Thread(new ThreadStart(test2.Go));
th.ApartmentState = ApartmentState.MTA;
th.Start();
}
}

public class TestClass
{
public DotNetSleeperClass dotnet;
public MyCom.SleeperClass com;
public string mName;

public TestClass(string name)
{
mName = name;
dotnet = new DotNetSleeperClass();
com = new MyCom.SleeperClass();
}

[MTAThread]
public void Go()
{
DateTime start = DateTime.Now;
Console.WriteLine(mName + " OUT at " +
start.ToString("hh:mm:ss:fff"));

//THIS WORK FINE BECAUSE IT'S SIMPLE MANAGED CODE.
//dotnet.Go();

//THIS BLOCKS OTHER THREADS WHILE IT RUNS com.Go();

DateTime finish = DateTime.Now;
TimeSpan ts = finish - start;
Console.WriteLine(mName + " IN at " +
finish.ToString("hh:mm:ss:fff") + " ELAPSED=" +
ts.TotalMilliseconds.ToString() + " milliseconds.");
Console.WriteLine("Press any key to continue...");
Console.Read();
}
}

public class DotNetSleeperClass
{
public DotNetSleeperClass() {}

public void Go()
{
Thread.Sleep(3000);
}
}

}


******************* VB6 ACTIVEX DLL ********************
Option Explicit
Public Sub Go()
'On my box this runs in about 4 seconds to run.
Dim a, b, c
For a = 1 To 1000
For b = 1 To 1000
c = a Mod b
DoEvents
Next b
Next a
End Sub
 
J

Justin Rogers

If you are running this on a single-proc or sometimes even a dual-proc
machine you wouldn't see concurrency on these threads. The processor is
allowing the first thread to finish before it does a context switch to a new
thread. The only real way to see multi-threading in action on a single-proc
machine is to have the thread jump into the wait state while it isn't doing
anything. Then you'll see your other threads running.


--
Justin Rogers
DigiTec Web Consultants, LLC.

Mark Sisson said:
Hey all.
I've got a legacy COM dll that I'd like to use in my multithreaded C#
app. Problem though is when I create several instances of the interop
object and start them on their own threads they always seem to run
synchronously.

I've boiled down the code to a simple "lab" experiment below. In this
console app you'll see that instance one comes back after roughly 4
seconds and then instance two comes back in 8 seconds, twice the time.
This indicates that it is wait on the first thread to finish. Why
won't they run concurrently?

I've tried my best to make sure that the thread types are MTAThread
before calling the COM objects. Also, in VB I made sure the project
threading model was "Apartment Threaded" and the Instancing set to
GlobalUseMulti.

Any ideas???
Thanks



******************* .NET C# CONSOLE TEST APP ********************
using System;
using System.Threading;

namespace ComThreadTest
{
class MyConsoleApp
{
[MTAThread]
static void Main(string[] args)
{
TestClass test1 = new TestClass("1");
TestClass test2 = new TestClass("2");

Thread th = new Thread(new ThreadStart(test1.Go));
th.ApartmentState = ApartmentState.MTA;
th.Start();

th = new Thread(new ThreadStart(test2.Go));
th.ApartmentState = ApartmentState.MTA;
th.Start();
}
}

public class TestClass
{
public DotNetSleeperClass dotnet;
public MyCom.SleeperClass com;
public string mName;

public TestClass(string name)
{
mName = name;
dotnet = new DotNetSleeperClass();
com = new MyCom.SleeperClass();
}

[MTAThread]
public void Go()
{
DateTime start = DateTime.Now;
Console.WriteLine(mName + " OUT at " +
start.ToString("hh:mm:ss:fff"));

//THIS WORK FINE BECAUSE IT'S SIMPLE MANAGED CODE.
//dotnet.Go();

//THIS BLOCKS OTHER THREADS WHILE IT RUNS com.Go();

DateTime finish = DateTime.Now;
TimeSpan ts = finish - start;
Console.WriteLine(mName + " IN at " +
finish.ToString("hh:mm:ss:fff") + " ELAPSED=" +
ts.TotalMilliseconds.ToString() + " milliseconds.");
Console.WriteLine("Press any key to continue...");
Console.Read();
}
}

public class DotNetSleeperClass
{
public DotNetSleeperClass() {}

public void Go()
{
Thread.Sleep(3000);
}
}

}


******************* VB6 ACTIVEX DLL ********************
Option Explicit
Public Sub Go()
'On my box this runs in about 4 seconds to run.
Dim a, b, c
For a = 1 To 1000
For b = 1 To 1000
c = a Mod b
DoEvents
Next b
Next a
End Sub
 
W

Willy Denoyette [MVP]

VB6 COM objects are STA objects, that means they must run on an STA thread.
You did create two instances of the object from two MTA threads, but the object itself will run on a single (COM (OLE) created) STA
thread, and access from the two MTA threads will be marshaled and synchronized.
So what you should do is, initialize the threads as STA so that each objects runs on his own STA thread without marshaling and you
will be fine.

Willy.


Mark Sisson said:
Hey all.
I've got a legacy COM dll that I'd like to use in my multithreaded C#
app. Problem though is when I create several instances of the interop
object and start them on their own threads they always seem to run
synchronously.

I've boiled down the code to a simple "lab" experiment below. In this
console app you'll see that instance one comes back after roughly 4
seconds and then instance two comes back in 8 seconds, twice the time.
This indicates that it is wait on the first thread to finish. Why
won't they run concurrently?

I've tried my best to make sure that the thread types are MTAThread
before calling the COM objects. Also, in VB I made sure the project
threading model was "Apartment Threaded" and the Instancing set to
GlobalUseMulti.

Any ideas???
Thanks



******************* .NET C# CONSOLE TEST APP ********************
using System;
using System.Threading;

namespace ComThreadTest
{
class MyConsoleApp
{
[MTAThread]
static void Main(string[] args)
{
TestClass test1 = new TestClass("1");
TestClass test2 = new TestClass("2");

Thread th = new Thread(new ThreadStart(test1.Go));
th.ApartmentState = ApartmentState.MTA;
th.Start();

th = new Thread(new ThreadStart(test2.Go));
th.ApartmentState = ApartmentState.MTA;
th.Start();
}
}

public class TestClass
{
public DotNetSleeperClass dotnet;
public MyCom.SleeperClass com;
public string mName;

public TestClass(string name)
{
mName = name;
dotnet = new DotNetSleeperClass();
com = new MyCom.SleeperClass();
}

[MTAThread]
public void Go()
{
DateTime start = DateTime.Now;
Console.WriteLine(mName + " OUT at " +
start.ToString("hh:mm:ss:fff"));

//THIS WORK FINE BECAUSE IT'S SIMPLE MANAGED CODE.
//dotnet.Go();

//THIS BLOCKS OTHER THREADS WHILE IT RUNS com.Go();

DateTime finish = DateTime.Now;
TimeSpan ts = finish - start;
Console.WriteLine(mName + " IN at " +
finish.ToString("hh:mm:ss:fff") + " ELAPSED=" +
ts.TotalMilliseconds.ToString() + " milliseconds.");
Console.WriteLine("Press any key to continue...");
Console.Read();
}
}

public class DotNetSleeperClass
{
public DotNetSleeperClass() {}

public void Go()
{
Thread.Sleep(3000);
}
}

}


******************* VB6 ACTIVEX DLL ********************
Option Explicit
Public Sub Go()
'On my box this runs in about 4 seconds to run.
Dim a, b, c
For a = 1 To 1000
For b = 1 To 1000
c = a Mod b
DoEvents
Next b
Next a
End Sub
 
M

Mark Sisson

Thank Willy and Justin. You comments are both helpful but I'm still
not much closer to a solution. I found a few more posts on
context-switching I'm starting to get the feeling that when you're
running on a single processor system that you can't be guaranteed of
any context switching. The latest COM code and timing results are
below. Note that my only weak attempt at forcing a context-switch is
with Sleep. Are there any more potent WinAPI's that I could enlist to
force the context-thread?

Now let's step out of the lab for a second. In the real world my
production COM component doesn't sit in a loop doing math
calculations. Instead it executes an HTTP POST and stores the results
into SQL. Since the POST is what will take 95% of processing time
will the CPU-time-slice-manager see this as a context-switching
opportunity (I hope)? Or am I doomed to think that I will ever be
able to get my multi-threaded COM objects to be sending 4 concurrent
HTTP POSTs simultaneously? It's seems ludicrous that while my CPU is
doing nothing but waiting for the HTTP results (which take between 5
and 25 seconds) that all other threads could not at least send their
HTTP POSTs in the meantime. That's the whole point of my architecture
being multi-threaded.

Thanks again.
mark



=============== COM CODE THAT NEVER SWITCHES CONTEXT ===============
Option Explicit
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As
Long)

Public Function Go() As String
Dim rslt As String
Dim a As Double
Dim b As Double
Dim c As Double
Dim CR As String

CR = Chr(13) + Chr(10)
rslt = CR + " 00%=" + FormatDateTime(Now, vbLongTime) + CR
For a = 1 To 1000
If a Mod 100 = 0 Then
rslt = rslt + " " + CStr(a * 100 / 2000) + "%=" +
FormatDateTime(Now, vbLongTime) + CR
Sleep (100)
End If
For b = 1 To 1000
c = a Mod b
DoEvents
Next b
Next a
Go = rslt
End Function

==================== RESULTS SEEN IN .NET CALL APP ===============
Stats for #1
00%=7:07:39 AM
10%=7:07:40 AM
20%=7:07:41 AM
30%=7:07:41 AM
40%=7:07:42 AM
50%=7:07:43 AM
60%=7:07:43 AM
70%=7:07:44 AM
80%=7:07:45 AM
90%=7:07:45 AM
100%=7:07:46 AM

Stats for #2
00%=7:07:39 AM
10%=7:07:47 AM
20%=7:07:48 AM
30%=7:07:49 AM
40%=7:07:49 AM
50%=7:07:50 AM
60%=7:07:51 AM
70%=7:07:52 AM
80%=7:07:53 AM
90%=7:07:53 AM
100%=7:07:54 AM
 
D

Dick Grier

Hi,

The standard way to handle this is to create an ActiveX EXE (convert your
DLL to an EXE). Then, in the class module where you call the
"long-winded-process", disconnect the call by invoking a timer that starts
the actual process, and allow the call from your outside process (in this
case, your .NET code) to return immediately. When the "long-winded-process"
is finished, it can execute an event to notify the calling routine. There
still will be some bookkeeping required to keep identify (associated the
event with the call).

Since this still is COM based, creating a free thread in .NET hasn't added
anything to the equation. The only way to get the COM (STAThread) object to
act as you want is to create it as a separate process.

Dick

--
Richard Grier (Microsoft Visual Basic MVP)

See www.hardandsoftware.net for contact information.

Author of Visual Basic Programmer's Guide to Serial Communications, 3rd
Edition ISBN 1-890422-27-4 (391 pages) published February 2002.
 
S

Steffen Ramlow

Mark said:
Now let's step out of the lab for a second. In the real world my
production COM component doesn't sit in a loop doing math
calculations. Instead it executes an HTTP POST and stores the results
into SQL. Since the POST is what will take 95% of processing time
will the CPU-time-slice-manager see this as a context-switching
opportunity (I hope)? Or am I doomed to think that I will ever be
able to get my multi-threaded COM objects to be sending 4 concurrent
HTTP POSTs simultaneously? It's seems ludicrous that while my CPU is
doing nothing but waiting for the HTTP results (which take between 5
and 25 seconds) that all other threads could not at least send their
HTTP POSTs in the meantime. That's the whole point of my architecture
being multi-threaded.

Your VB-Dll must be compiled with setting "Apartment threaded" and not
"single threaded". If you compile it "single threaded", all objects "in" the
dll will run in the main STA of the process, thus all calls get serialized.
 
S

Steffen Ramlow

Dick said:
Since this still is COM based, creating a free thread in .NET hasn't
added anything to the equation. The only way to get the COM
(STAThread) object to act as you want is to create it as a separate
process.

What kind of weird tip. Multiprocessing instead of Multithreading.
 
M

Mark Sisson

Ok everyone, there's been a lot of input but we still have no
solution.

Dick - you proposed that I go Active.exe. That seems a little extreme
but maybe it really is the only way to go. My problem is that I'm
dealing with a legacy dll that is very involved and is used by other
apps. I'm not exactly sure how I'm going to pull that off. What's
the best-practices on that? Create an ActiveX.exe wrapper?

Steffen - you barked at the multi-processing tip but have you actually
tried to make this work? Like me, I think you believe that this SHOULD
work in theory but haven't actually tried it. Multi-threading just
doesn't seem to work. The context-switching doesn't happen and they
won't run in parallel like you'd expect. I've already setup everything
with MTA (see my first post) and IT DOESN'T WORK! TRY IT!

Justin - you seem like you've been around the block before on this one
and point out that it's just not going to switch contexts on a
single-processor box. BUT HOW DO IT GET THE THREAD TO JUMP INTO A WAIT
STATE LIKE YOU RECOMMEND? The WinAPI Sleep didn't work. Tons of
DoEvents didn't work. Any other API?

All - you all have good advice but if we're going to get to the bottom
of this may I recommend that back your recommendation with a working
code sample? There's a lot of "theory" floating around but it just
theory until you can prove it works. I'm willing to work my ass on to
try and make this work but I've tried all of your recommendations and
it's just not happening. The code I posted originally should save you
most of the typing and I'D REALLY REALLY REALLY APPRECIATE IT!!

Thanks again for all your help guys.
mark
 
S

Steffen Ramlow

Mark said:
Steffen - you barked at the multi-processing tip but have you actually
tried to make this work? Like me, I think you believe that this SHOULD
work in theory but haven't actually tried it. Multi-threading just
doesn't seem to work. The context-switching doesn't happen and they
won't run in parallel like you'd expect. I've already setup everything
with MTA (see my first post) and IT DOESN'T WORK! TRY IT!

You did not anwser my question! Is the VB-dll build "apartment threaded" or
"single threaded"?
Apartment threaded objects work well in a multithread environment, e.g. each
ASP is a STA object, tons of com+ and iis-apps are build with apartment
threaded vb-dlls.
And you have ignored the hint (forgotten the poster) that you should not mix
apartment types (MTA vs. STA).
 
M

Mark Sisson

You did not anwser my question! Is the VB-dll build "apartment threaded" or
"single threaded"?
Apartment threaded objects work well in a multithread environment, e.g. each
ASP is a STA object, tons of com+ and iis-apps are build with apartment
threaded vb-dlls.
And you have ignored the hint (forgotten the poster) that you should not mix
apartment types (MTA vs. STA).

Steffen,
Ummmmm... in my very first post in explained that my COM object was
compiled with threading model set to "Apartment Threaded" and the
Instancing set to GlobalUseMulti. I also said that in .NET I'm using
the attribute [MTAThread] for all of my class methods and I'm setting
the Thread object's ApartmentState property to ApartmentState.MTA
before calling it's Start method (that invokes the COM object). Isn't
this what you're asking for? If it is, which is where I started from
in the first place, it doesn't work. I've already proven that. If it
isn't, my apologies I'll make any modifications you think are
necessary and try it. If it is, would you mind compiling it and
seeing the results for yourself? MTA threading doesn't guarantee
context-switching! Justin's explanation seemed to agree but I just
don't know where to go from here.

Thanks for your help. I'm amazed that I can't get this to work!!!!!!!
 
S

Steffen Ramlow

Mark said:
Ummmmm... in my very first post in explained that my COM object was
compiled with threading model set to "Apartment Threaded" and the
Instancing set to GlobalUseMulti.

Ok, i missed that. GlobalMultiUse might be a prob, try MultiUse.

I also said that in .NET I'm using
the attribute [MTAThread] for all of my class methods

And someone here said, that this is not a good idea when calling VB-objects.
Use STA instead.
 
W

Willy Denoyette [MVP]

I hope Dick meant create a separate thread ... :)
Anyway, VB style COM objects are always STA. Now in order to prevent apartment marshaling and thread switching you need to create
instances in STA initialized apartments.
Note also that when you set the [MTAThread] attribute on Main, you effectively initialize the main thread as MTA, when you create
instances of STA objects from MTA threads COM will create a separate (unmanaged) thread and initialize it as STA (this is called the
default STA), all calls to STA objects from MTA threads will be marshaled (and incur thread switches), in some cases Idispatch calls
will fail due to IP marshaling failures.
So the advise is use STA (and therefore VB6) objects from compatible apartments only.

Willy.
 
D

Dick Grier

Hi,

I meant a separate process (the COM ActiveX EXE is a simulation of a
separate thread by creating a separate process). ActiveX objects that are
created in VB do not inherently support free threading. As you note, there
can (and will, in my experience) be marshalling errors if you call an STA
object from an MTA thread.

Now, there are tricky workarounds for this, but they require the use of a
helper DLL that was created for the purpose in C, to do the threading work.
I haven't tried free threading in VB6 (other than the use of an ActiveX EXE
substitute), because it is begging to create a debugging nightmare.

Dick

--
Richard Grier (Microsoft Visual Basic MVP)

See www.hardandsoftware.net for contact information.

Author of Visual Basic Programmer's Guide to Serial Communications, 3rd
Edition ISBN 1-890422-27-4 (391 pages) published February 2002.
 
D

Dick Grier

Hi,

Yes, I suggest creating an ActiveX EXE wrapper. This will be only a few
(perhaps as many as 20?) lines of extra code. The complexity depends, at
the end of the day, on details that I don't know.

--
Richard Grier (Microsoft Visual Basic MVP)

See www.hardandsoftware.net for contact information.

Author of Visual Basic Programmer's Guide to Serial Communications, 3rd
Edition ISBN 1-890422-27-4 (391 pages) published February 2002.
 
S

Steffen Ramlow

I hope Dick meant create a separate thread ... :)

No, unfortunately not...
Anyway, VB style COM objects are always STA. Now in order to prevent
apartment marshaling and thread switching you need to create
instances in STA initialized apartments.
Note also that when you set the [MTAThread] attribute on Main, you
effectively initialize the main thread as MTA, when you create
instances of STA objects from MTA threads COM will create a separate
(unmanaged) thread and initialize it as STA (this is called the
default STA), all calls to STA objects from MTA threads will be
marshaled (and incur thread switches), in some cases Idispatch calls
will fail due to IP marshaling failures.
So the advise is use STA (and therefore VB6) objects from compatible
apartments only.


Argh! Willy gave the proper answer already in his first posting! Since i
read this from the COM+ / Component Services group, i thought we talk about
COM+ (Nice x-post, Mark...). Here you have a STA-Thread Pool and your
VB-objects will run on any thread in this pool. But with a "normal" client
all your vb objects will served by the main STA thread.

So Mark, make your client threads STA and all will be fine.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top