Introduction
This article is the last part of the three part series on SynchronizationContext
. SynchronizationContext
is a class introduced by .NET 2.0 with little documentation or explanation of how to use it. I have tried to explain in part one how to use this class, and in part two, how to create your own SynchronizationContext
. In part two, I showed how to build a SynchronizationContext
that will marshal code from any .NET thread into a STA thread. I have done this so I can execute COM code that needs to run on the STA thread. The next step is to create a WCF service that will execute all its service operations on the STA thread (using the SynchronizationContext
I provided in part two). In this article, I will show you how to configure WCF to provide a custom STA SynchronizationContext
so each method on a WCF service will execute on the same STA thread. This will allow me to provide a simple programming model that will be fully compatible with COM, and I will not need to worry about thread safety because all my code will run within the same thread.
WCF - The Power
I will not even try to give a full explanation of WCF here. WCF is vast in functionality, and does much more than just "Remoting" or web services. I am starting to believe that WCF should be treated as a runtime rather than a communication framework. WCF provides us the following programming models "out of the box":
- built-in support for error handling
- built-in support for concurrency
- built-in support for security
- built-in support for transactions
- built-in support for data encryption
- built-in support for durable services
- built-in support for method interception and inspection (AOP)
- built-in support for one way communication, and callbacks
- and much more...
Just listen to this ARCast, where Juval Lowy believes every class should be a WCF class: Every Class a WCF Service, with Juval Lowy. At first, I said to myself, this is too much, every class to be a WCF class is just insane. But, the more I looked at WCF and what it had to offer, the more it made sense to me. WCF is a very extensible framework, allowing the developer to do a lot of custom configuration. For example, providing a SynchronizationContext
for your service is something you will not be able to do within native .NET classes, but only in WCF (and that's just one feature among many). Although, I must say I did not go as far as making each class a WCF class. I have decided to make each component a WCF service. Even if I don't plan to run my component remotely, I still believe the benefits of the WCF runtime are worth coding my components as WCF services. Bottom line, WCF is more than web services or Remoting services, it provides a solid programming model that applies in every type of development. If you don't know WCF, I strongly recommend you learn it, it is by far one of the better frameworks Microsoft has released. For this article, I assume you have a basic knowledge of WCF services.
A Simple WCF Service
Let me just say this right away, I have coded a WCF service for this article just for testing. I do not recommend you code services the way that I have. To be more specific, in my code, the service implementation and the service contact are within the same assembly - this is not recommended. However, because I am dealing with testing mySynchronizationContext
within a prototype/testing project, I wanted to keep the number of assemblies and code to a minimum. Normally, when I code a WCF service, I have three projects:
- Project containing the service contact (interface) and possibly any data contacts (DTOs)
- Project containing the service implementation
- Project hosting the service (optional)
So please, no bashing, I am stating it here that the code is just for testing and not for production. Now that we got that out of the way, let me show the service contact and the implementation.
[ServiceContract]
public interface IStaService
{
[OperationContract]
string DoWorkOnSTAThread(string state);
}
There is really not much to say about this service, it is very simple. Let me show you the implementation of this service:
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
return "processed by " + aptState.ToString() + " Thread id: " + id.ToString();
}
}
Notes:
DoWorkOnSTAThread
will be the method I plan to execute on the STA thread, so I coded some tracking code to make sure I am running this method on the STA thread. I check theApartmentState
and make sure it is anApartmentState.STA
.- I also log the thread ID; considering I want all my code to run on the same STA thread, the ID should always be the same.
- However, I did not write any code to indicate using the STA Sync Context, so for now, STA thread marshaling is not used.
Testing our Service
We will be running our service a few times, so let's learn how to test it. My service is hosted using webdev, and it runs as a web service. To test it, I have used the WCF test client applicaiton. It can normally be found at "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\WcfTestClient.exe". By calling DoWorkOnSTAThread
multiple times, I get the following output:
WCF current thread: 12
WCF current thread: 12
WCF current thread: 11
WCF current thread: 11
WCF current thread: 11
WCF current thread: 13
WCF current thread: 10
WCF current thread: 11
WCF current thread: 10
WCF current thread: 10
WCF current thread: 13
To find out the service behavior, you can view the ServiceDescription
within a custom ServiceHost (I will show the custom service host code later in the article).
Notice that I am running the same method on multiple threads. This is because, by default, WCF created my service using a PerSession
InstanceContextMode
and a ConcurrencyMode
of Single
(see image above). This means that the method will be executed one at a time, but each time, it is executed by a thread assigned by the thread pool. First, you should avoid using PerSession
for your services. PerSession
simply does not scale, I consider it evil for coding scalable services, so, I am going to change the service behavior.
[ServiceBehavior(UseSynchronizationContext=true,
ConcurrencyMode=ConcurrencyMode.Multiple,
InstanceContextMode=InstanceContextMode.PerCall)]
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
//throw new Exception("boom");
return "processed by " + aptState.ToString() + " Thread id: " + id.ToString();
}
}
Using the WCF service behavior attribute [ServiceBehavior(UseSynchronizationContext=true, ConcurrencyMode=ConcurrencyMode.Multiple, InstanceContextMode=InstanceContextMode.PerCall)]
, I ask WCF to create this service using InstanceContextMode.PerCall
. This means, every method call will create an instance of the service, and as soon as the method completes, the instance is destroyed. Using ConcurrencyMode.Multiple
allows for clients to execute methods within this service concurrently, allowing multiple threads to execute the same method at the same time. UseSynchronizationContext=true
means that I ask the service to use the SynchronizationContext
attached to the host's thread. Using this new service behavior, let's see our output by executing the method three times:
WCF current thread: 12
WCF current thread: 9
WCF current thread: 6
Same results more or less... Our service can scale better, but still have no control on which thread the code is executed on.
Providing your Service a SynchronizationContext
You noticed that irrespective of whether the service is using single or multiple concurrency mode, in both, the invocation is controlled by WCF using different threads from the thread pool. In order to provide your own SynchronizationContext
, you have two choices:
- Set the
SynchronizationContext
on the hosting thread before opening the host - Create your own
ServiceBehavior
and override theSynchronizationContext
on the service endpoint
I will explore the second option simply because we don't always create our own hosting program, and in many cases, the hosting is done in WAS or IIS. Let me show you the ServiceBehvior
attribute I created to use STA thread synchronization.
public class StaServiceBehaviorAttribute : Attribute, IContractBehavior, IServiceBehavior
{
StaSynchronizationContext mStaContext;
public StaServiceBehaviorAttribute()
{
mStaContext = new StaSynchronizationContext();
}
#region IContractBehavior Members
void IContractBehavior.AddBindingParameters(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
}
void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)
{
dispatchRuntime.SynchronizationContext = mStaContext;
}
void IContractBehavior.Validate(ContractDescription contractDescription,
ServiceEndpoint endpoint)
{
}
#endregion
#region IServiceBehavior Members
void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase,
System.Collections.ObjectModel.Collection endpoints,
System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription,
System.ServiceModel.
ServiceHostBase serviceHostBase)
{
}
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
serviceHostBase.Closed += delegate
{
mStaContext.Dispose();
};
}
#endregion
}
By creating my own custom ServiceBehvior
attribute, I can set certain settings that are otherwise not available to me.
Notice that I am creating the StaSynchronizationContext
at the constructor:
public StaServiceBehaviorAttribute()
{
mStaContext = new StaSynchronizationContext();
}
The other most important part is implementing the ApplydispatchBehvior
method. This method will be executed once on each endpoint within your service. It allows me to override the default SynchronizationContext
with my own custom one.
void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)
{
dispatchRuntime.SynchronizationContext = mStaContext;
}
To make sure my STA thread ends correctly, I have also modified the IServiceBehavior.Validate
and provided an event handler for closing the host.
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
serviceHostBase.Closed += delegate
{
mStaContext.Dispose();
};
}
Using the STA Synchronization Context
Let's take another look at our service, now that we have created our own Service Behavior to apply a custom Synchronization Context.
[StaServiceBehaviorAttribute]
[ServiceBehavior(UseSynchronizationContext=true, ConcurrencyMode=ConcurrencyMode.Multiple,
InstanceContextMode=InstanceContextMode.PerCall)]
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
//throw new Exception("boom");
return "processed by " + aptState.ToString() + " Thread id: " + id.ToString();
}
}
- Notice that I kept the WCF service behavior to use
PerCall
andConcurrencyMode.Multiple
. Considering we are planning to marshal our code to an STA thread, usingConcurrencyMode.Single
will have the same behavior asConcurrencyMode.Multiple
. - Notice, I have placed my
StaServiceBehaiorAttribue
on the service. This will create the STA thread, and apply an STA synchronization context on all the endpoints my service exposes (in the examplen I only have one endpoint).
Very, well, let's try it. But using the WCF client tool, I have send multiple requests to my service, and here are the results:
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
- Notice that all the calls to the WCF method are executed on thread 9
- Notice that thread 9 is an STA thread as we expected
Now, I am able to control on which thread the WCF service is executing on.
A Word About Hosting
Notice that using this method, you can control the synchronization context of your service no matter what hosting method you choose. If you are hosting using a console application, you could set the synchronization context to STA (using the SynchronizationContext.SetSynchronizationContext
). Before opening the host, it will have the same effect. I tried to use a custom ServiceHostFactory
and set the synchronization context there, but it did not work. Let me show you the custom ServiceHostFactory
and the custom ServiceHost
.
public class StaCustomHostFactory : ServiceHostFactory
{
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
return new StaCustomHost(serviceType, baseAddresses);
}
}
public class StaCustomHost : ServiceHost
{
public StaCustomHost(Type serviceType, params Uri[] baseAddresses) :
base(serviceType, baseAddresses)
{
}
protected override void InitializeRuntime()
{
int id = Thread.CurrentThread.ManagedThreadId;
base.InitializeRuntime();
}
protected override void ApplyConfiguration()
{
int id = Thread.CurrentThread.ManagedThreadId;
// get the configuration from the app.config first
base.ApplyConfiguration();
// create the STA Thread Sync object and attach it to this hosting
// thread.
StaSynchronizationContext staContext = new StaSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(staContext);
}
}
<%@ ServiceHost
Language="C#" Debug="true"
Factory="StaServiceHost.StaCustomHostFactory"
Service="TestStaService.StaService"
CodeBehind="StaService.svc.cs" %>
Notice, I set the SynchronizationContext
within the ApplyConfiguration
method. WCF will take whatever SynchronizationContext
is set on the host's thread and use it for each invocation. However, it did not work at all. I don't know exactly why, but the only explanation I can come up with is that the code within ApplyConfiguration
does not run within the same thread as ServiceHost.Open
. Considering that the host is opened by webdev or IIS, I really don't know how to set the synchronization context on these threads. Therefore, I recommend you stick to using custom service behavior as I have shown before for overriding the synchronization context of your service's endpoints.
One little comment about closing the host. As you can see in part II, the STA thread is created as soon as the STA SynchronizationContext
object is created. In this case, it is created within the service behavior. It is important that this thread ends when the host closes. That's the reason I have coded this event handler:
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
serviceHostBase.Closed += delegate
{
mStaContext.Dispose();
};
}
I have tested this code by stopping the webdev process and placing a break-point on the event handler. I validated that the STA thread is exiting when webdev is closing. Thanks for the small WCF miracles.
Is WCF using Send or Post?
You might be wondering if the Send
or the Post
of our sync-context is used. I really didn't know, so I set a breakpoint on both the Send
and Post
methods, and found out that WCF always uses the Post
method. I have changed the service concurrency-mode to ConcurrencyMode.Single
and still Post
was used. So, when using Single
or Multiple
concurrency, in both cases, the Post
method is used to marshal code into the STA thread. What about exceptions? Notice, I have modified my service to throw an exception:
[StaServiceBehaviorAttribute]
[ServiceBehavior(UseSynchronizationContext=true,
ConcurrencyMode=ConcurrencyMode.Multiple,
InstanceContextMode=InstanceContextMode.PerCall)]
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
throw new Exception("boom");
//return "processed by " + aptState.ToString() +
// " Thread id: " + id.ToString();
}
}
Initially, I believed that this might cause the STA thread to terminate. It is an unhandled exception running on the STA thread, but WCF does handle the exception for you. So, the STA thread does not end even if you throw exceptions within the STA thread. WCF catches any unhandled exceptions, and converts them to FaultException
on the client thread. This saves our STA thread from ending, so it keeps running regardless if exceptions are thrown. Thank God for another WCF miracle.
Conclusion
In this article, I got into the inner workings of WCF, and grabbed control on "where" a service method is executed on. I am able to "tell" WCF to marshal all the method calls on an STA thread, allowing us not to worry about calling COM objects that are designed to work on the STA thread. This will also allow you to pop a UI within your service method, but I really don't recommend it. By marshaling the code yourself, you can add additional logic to log and validate each invocation. You might even add security at this level, and refuse marshaling a call if it does not fit a certain criteria. However, do not use this as a wild card; service side method interception can also do the same thing. Still, now that you know about this feature, you might find a good use for it within your projects.
Thank you for reading, and happy .NETting.
3 comments:
In your replies to comments to Parts I and II, you said you'd include a VS2k8 Sample Project with Part III.
source code can be found here
http://www.codeproject.com/KB/threads/SynchronizationContext3.aspx
Hi
i use an active x in my wcf service.
when i run my service , the exception
has rise for cus active x need a single thread and wcf service isn't a single thread apartemant.
plz help!
Post a Comment