0

I have a third-party API which requires thread affinity. I am using WCF in my service application to handle requests from a client which are then delegated to this API. Since WCF uses a thread pool to handle requests, I attempted to work around this with the following code (using SynchronizationContext class):

using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Threading;

namespace MyAPIService
{
  [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
  public class MyService: IService
  {
      ThirdPartyAPI m_api;
      SynchronizationContext m_context;

      MyService()
      {
        m_api = new ThirdPartyAPI();
      }

      public bool Connect(string ipaddress, int port)
      {
        m_context = (SynchronizationContext.Current == null) ? new SynchronizationContext() : SynchronizationContext.Current;
        return m_api.Connect(ipaddress, port);
      }

      public bool Disconnect()
      {
        throw new NotImplementedException();
      }

      public bool IsConnected()
      {
        return Send(() => m_api.IsConnected());
      }

      public TResult Send<TResult>(Func<TResult> func)
      {
        TResult retval = default(TResult);
        m_context.Send(new SendOrPostCallback((x) =>
        {
          retval = func();
        })
        , null);
        return retval;
      }

  }
 }
}

I thought this was would allow me to execute the IsConnected method on the same exact thread that Connect was called on but from testing this is not the case. IsConnected still executes on any thread in the pool. What am I doing wrong?

Any help would be greatly appreciated. Thank you very much.

markf78
  • 597
  • 2
  • 7
  • 25
  • You need [MCVE] of client part too and also make sure to debug code to understand whether you are getting new instances of MyService more often than you expect or your managing of synchronization context does not work. – Alexei Levenkov Jul 11 '17 at 03:31
  • While a SynchronizationContext is the right tool for the job, it isn't magic either. If you create a new default synchronization context (`new SynchronizationContext()`), it'll post work to the threadpool (which is not what you want). Does your external API (I suppose it's a COM object) have some kind of callback method? If so, it may be possible to capture a synchronization context inside of that callback then reuse it elsewhere. Otherwise, I'm afraid you'll have to write your own – Kevin Gosse Jul 11 '17 at 06:48
  • @KevinGosse the third party API is actually not a com object. When you say "write your own", do you mean my own synchronization context? – markf78 Jul 11 '17 at 13:27
  • @markf78 Yes. Writing a synchronization context to manage a single thread shouldn't be too difficult. In fact, there's probably a few implementations available here and there – Kevin Gosse Jul 11 '17 at 18:14

1 Answers1

3

The default synchronization context executes your work on the threadpool (therefore, you have no thread affinity).

If you want to make sure the work is always posted to the same thread, you need to write your own synchronization context. For instance:

public class SingleThreadSynchronizationContext : SynchronizationContext
{
    private readonly BlockingCollection<(SendOrPostCallback callback, object state)> _queue;
    private readonly Thread _processingThread;

    public SingleThreadSynchronizationContext()
    {
        _queue = new BlockingCollection<(SendOrPostCallback, object)>();
        _processingThread = new Thread(Process) { IsBackground = true };
        _processingThread.Start();
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        using (var mutex = new ManualResetEventSlim())
        {
            var callback = new SendOrPostCallback(s =>
            {
                d(s);
                mutex.Set();
            });

            _queue.Add((callback, state));
            mutex.Wait();
        }
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        _queue.Add((d, state));
    }

    public override SynchronizationContext CreateCopy()
    {
        return this;
    }

    private void Process()
    {
        SetSynchronizationContext(this);

        foreach (var item in _queue.GetConsumingEnumerable())
        {
            item.callback(item.state);
        }
    }
}

Note that this synchronization context assumes that an uncaught exception in a callback will crash the process. If that's not the case (for instance, because you have a global exception handler) then you should add some error handling (in Process and in Send).

Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • I had to change "_queue.Add((d, state));" to "_queue.Add(new WorkItem(d, state));". Is this example using a new feature of C# 7.0? – markf78 Jul 12 '17 at 02:34
  • It's using ValueTuple yes, you need to add the nuget package to use them. If you don't want to, you can use "ordinary" tuples: `Tuple.Create(s, state)` – Kevin Gosse Jul 12 '17 at 06:26