Introduction
This article illustrates working with Duplex mode in WCF with concurrency set to Reentrant. In general, Duplex is one of the three message exchange patterns (MEPs) which are:
- One-way
- Request-Response (synchronous and asynchronous)
- Duplex
On the other hand, concurrency in WCF services deals with how many concurrent threads can access these services at one time.
A full discussion of MEPs and Concurrency in WCF is not the intent of this article; as a matter of fact, that would be the job of a full book. As such, this article assumes familiarity (but necessarily experience) of the following:
- WCF programming
- MEPs in WCF
- Concurrency modes in WCF
- Basic concepts of threads
Scenario
The working example of this article simulates a scenario where a client application sends a request for a service to register for notifications about low temperature drops. The service later responds to the client in Duplex mode, meaning that the client won’t wait for the response, rather the service will inform the client of the temperature drop via a callback.
Building the Duplex Service
The service is self-hosted inside a Windows application. This is a duplex service using wsDualHttpBinding
. The service code is shown below:
[ServiceContract(CallbackContract=typeof(IClientCallback))]
public interface ITemperature
{
[OperationContract(IsOneWay=true)]
void RegisterForTempDrops();
}
public interface IClientCallback
{
[OperationContract(IsOneWay = true)]
void TempUpdate(double temp);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
ConcurrencyMode = ConcurrencyMode.Single)]
public class Temperature : ITemperature
{
public void RegisterForTempDrops()
{
OperationContext ctxt = OperationContext.Current;
IClientCallback callBack = ctxt.GetCallbackChannel<iclientcallback>();
Thread.Sleep(3000);
callBack.TempUpdate(10);
}
}
Let’s examine the code:
- The “
ITemperature
” is the service contract and the “IClientCallback
” is the callback interface for the client. The operation “RegisterForTempDrops
” will be consumed by clients wanting to know about drops in temperatures; these clients, on the other hand, will have to implement the callback interface in order for the service to be able to call their “TempUpdate
” method for notification. Note how both “RegisterForTempDrops
” and “TempUpdate
” are one-way operations. - The
ServiceBehavior
attribute defines a PerSession
instance mode and a Single
concurrency mode. These are the default WCF settings any way, but it is always a better practice to explicitly write down the code. At a glance, the PerSession
instance mode means that each client will get its own service instance on the first call and that the same instance will keep serving the client until the proxy is explicitly closed or the session times out. Single
concurrency mode, on the other hand, means that one thread at a time will be able to access the service instance; so, if the client is trying to issue multiple concurrent calls to the service instance which has been allocated to it, the calls will be queued and served one at a time because multithreading is disabled at the service (ConcurrencyMode.Single
). - The “
Temperature
” class implements the “RegisterForTempDrops
” method. This method calls back to the client and sends the notification via the “TempUpdate
” method in Duplex mode.
The Duplex configuration of the service is shown below:
Here, we are using wsDualHttpBinding
for duplex operations. Also exposed is the metadata exchange endpoint (MEX) which enables using the Add Service Reference option for the client via Visual Studio.
Finally, the code for starting the self-hosted service:
internal static ServiceHost myServiceHost = null;
private void button1_Click(object sender, EventArgs e)
{
myServiceHost = new ServiceHost(typeof(Temperature));
myServiceHost.Open();
MessageBox.Show("Service Started!");
}
private void button2_Click(object sender, EventArgs e)
{
if (myServiceHost.State != CommunicationState.Closed)
{
myServiceHost.Close();
MessageBox.Show("Service Stopped!");
}
}
Building the Duplex Client
The client is a console application which consumes the service and gets the result back by implementing the callback interface. The first step is to add a service reference to the WCF service. First, you have to start the service by running the Windows application and clicking on the “Start” button. Next, at the client, use VS to add a service reference as follows (notice the use of the base address specified in the service configuration; this works because of the MEX endpoint):
The client code is shown below:
public class CallBackHandler : ITemperatureCallback
{
static InstanceContext site = new InstanceContext(new CallBackHandler());
static TemperatureClient proxy = new TemperatureClient(site);
public void TempUpdate(double temp)
{
Console.WriteLine("Temp dropped to {0}", temp);
}
class Program
{
static void Main(string[] args)
{
proxy.RegisterForTempDrops();
Console.ReadLine();
}
}
}
Let’s examine the code:
- For the client to be able to participate in a Duplex conversation, it needs to have its own endpoint for the service to callback to. This code is automatically generated using the Add Service Reference option.
- Note how the client class “
CallBackHandler
” implements the “ITemperatureCallback
” which is the type defined at the service; this will enable Duplex communication. - The “
InstanceContext
” holds context information about a service instance; the client uses this context to create the proxy. This is different than when consuming non-duplex services where the “InstanceContext
” is not needed. In this case, the “InstanceContext
” holds references about the client’s channels automatically created in order for the client to participate in a Duplex operation. - Using the proxy, the client calls the “
RegisterForTempDrops
” service method. - The client will then get the callback from the service on the method “
TempUpdate
”.
Run the Sample
With the service application running, create a new instance of the client console application, and notice how after three seconds (the time which simulates the temperature dropping event) the client application will display a message that it was notified, as shown below:
Sample Architecture
Now, let’s see a simple sketch of the architecture of what just happened. The image below shows the Duplex communication between the service and the client:
- The client consumes the One-Way operation “
RegisterForTempDrops
” of the service; no response comes back for one-way operations. - The service processes the request and calls back to the client on the One-Way operation “
TempUpdate
”. The duplex communication has ended; it’s that simple!
However, recall that the service behavior was defined as Single concurrency mode; meaning that only one thread can access the service instance at a time. Ok, so now can you imagine what will happen if the method “TempUpdate” was not One-Way? So let’s say that the “TempUpdate” method should return a Boolean value to the service to indicate that it has successfully been notified. Let’s see the architecture diagram updated for this scenario:
- Again, the client calls for the one-way “
RegisterForTempDrops
” operation (thread A). - Also, the service processes the request and calls for the method “
TempUpdate
” on the client. - The big difference now is that “
TempUpdate
” is not a one-way operation anymore; instead, it returns a boolean value to the service. So now, the client is trying to call the service to deliver the return of the “TempUpdate
” method. This will happen on another thread B. But, recall that thread A is still “hanging on” to the service instance which has its concurrency mode set to Single
, so it cannot serve more than a single thread at a time. As a result, thread B will be stuck, and you will have a dead lock case where thread B is striving for a resource (the service instance); nonetheless, that same resource is being captured by thread A who is refusing to let go simply because in thread A’s perspective, the communication cycle is not finished yet.
Want to see this in action? Keep reading.
Modifying the Sample
In order to see the above scenario in action, change the callback operation and set its return type to bool
as opposed to void
and remove the One-Way mark. The service code now is shown below:
[ServiceContract(CallbackContract=typeof(IClientCallback))]
public interface ITemperature
{
[OperationContract(IsOneWay=true)]
void RegisterForTempDrops();
}
public interface IClientCallback
{
[OperationContract]
bool TempUpdate(double temp);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
ConcurrencyMode = ConcurrencyMode.Single)]
public class Temperature : ITemperature
{
public void RegisterForTempDrops()
{
OperationContext ctxt = OperationContext.Current;
IClientCallback callBack = ctxt.GetCallbackChannel<IClientCallback>();
Thread.Sleep(3000);
callBack.TempUpdate(10);
}
}
Start the service and update the service reference at the client. The only change is to return a boolean value from the “TempUpdate
” method. The client code is shown below:
public class CallBackHandler : ITemperatureCallback
{
static InstanceContext site = new InstanceContext(new CallBackHandler());
static TemperatureClient proxy = new TemperatureClient(site);
public bool TempUpdate(double temp)
{
Console.WriteLine("Temp dropped to {0}", temp);
return true;
}
class Program
{
static void Main(string[] args)
{
proxy.RegisterForTempDrops();
Console.ReadLine();
}
}
}
Now, run the client and notice that you won’t get any response back. You will actually enter a deadlock state. So, how do we solve this problem?
Reentrant Concurrency
The solution is actually ultra easy: just set the ConcurrencyMode
value of the ServiceBehavior
attribute to Reentrant
instead of Single
.
In order to understand the Reentrant
mode, consider the image below:
When the client calls the service, the call thread (A) is marked with a marker which we will call “M”. Now, the service responds back to the client which processes the request, and sends the boolean result back to the service on thread (B), which is also marked with marker “M”. WCF checks if the thread coming to the service has the same marker of the first thread, then that means that the call coming in is in response to a call that went out, and as a result, the call is accepted.
So in summary, we are still doing single threading but with the additional feature of allowing this particular call to come back; otherwise, we will have a deadlock. That’s single threaded with Reentrant feature!
In order to see that in action, just change the ConcurrencyMode
of the ServiceBehavior
to Reentrant
instead of Single
. This is shown below:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
ConcurrencyMode = ConcurrencyMode.Reentrant)]
This time, the client invocation succeeds and the full cycle is done without any deadlocks.
Sample Program
You can download the service and client applications at the start of the article.