2

A have a need to force certain functions of my code to run on the main thread. We are talking to a single thread black box and if I run from a worker thread my performance is unacceptable. I had been working around this for months by saving the current synchronization context and then using Post to call a function that would then execute in the main thread. I know this is meant for GUIs and I'm not sure why it had been working to date but it has now been inconsistent when we go to older versions of our base software which I must be compatible with.

I have written this basic example that shows my issue. I run a task that runs in a new thread and at a certain point I want that thread to be able to ask the main thread to perform some actions and then I want the worker thread to be able to continue along.

This is the current output:

Main (ThreadId = 1)
RunTask (ThreadId = 3)
CallBack (ThreadId = 4)  << I want this to be ThreadId 1

Any help would be great and even better if it is something close to the current solution because we were days from release and I'm worried a major rewrite could cause more problems. Thanks!

public class Test
{
    internal static SynchronizationContext _context;    
    internal static bool _busy = false;

    static void Main(string[] args)
    {
        Console.WriteLine("Main (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");

        SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());

        _context = SynchronizationContext.Current;

        Task.Run(() => RunTask());

        Console.Read();
    }

    public static Task RunTask()
    {
        Console.WriteLine("RunTask (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");

        _busy = true;

        _context.Post(new SendOrPostCallback((o) =>
        {
            CallBack(null,
                     EventArgs.Empty);
        }),
                       null);

        while (_busy == true)
        {

        }

        return null;
    }

    public static void CallBack(object sender, EventArgs e)
    {
        Console.WriteLine("CallBack (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");
    }
}
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Rich F
  • 31
  • 4
  • Is your real app, a console app, winforms, or wpf ? – TheGeneral Sep 04 '18 at 00:52
  • In your sample, isn't the main thread blocked on the `Console.Read()` call? I think that might be why the callback has a different thread ID - whatever the "main" thread is needs to still be pumping messages in order to process a callback. I'm assuming the actual source has some sort of message processing mechanism? – E. Moffat Sep 04 '18 at 01:01
  • 1
    Welcome Rich. Probably not your fault as nearly all the MSDN examples do the same thing, but spawning a thread and either doing nothing or waiting for it to complete negates the usefulness of threads. Maybe just put the operation in the main thread? –  Sep 04 '18 at 01:57
  • You could utilize a continuation to communicate between threads, but a more robust method might be to create an Rx pattern, such as using a subject that could be observed by all interested threads, and signalling (and delivery payload) changes. Another approach might be to throw down a payload (containing your signal/context) into a shared, thread-safe queue, which could be periodically checked by interested threads. Sort of like how a producer-consumer queue works. Don't forget to check Joe Albahari's excellent website on threading! – code4life Sep 04 '18 at 03:27
  • This is not my actual code. In my actual application (class library) I am instantiated by the main program (from Excel) and I launch a worker thread to listen for TCP/IP connections. When someone connects and requests data I need to retrieve it from the main thread. On TCP/IP command can result in me asking the main thread for 20+ things. When I do this one by one it is slow so I then the main thread to run the command and set a static variable with the result. The sample program was my attempt to simplify it. I can't be in the main thread because it would lock up the main program. – Rich F Sep 04 '18 at 04:47
  • @RichF - Please don't post sample code that doesn't really show what you are trying to do. It make it hard for us to suggest a good solution. Can you please try to write a good sample that is as close to the real thing as possible? – Enigmativity Sep 04 '18 at 10:15
  • I apologize I was trying to create a simpler case because our environment includes Excel, VBA, C++, C# and Python. I did find a solution that I will post below – Rich F Sep 04 '18 at 18:37

2 Answers2

1

You can try to pass a delegate to main thread through event:

public class Test
{
    public static BlockingCollection<Action> Handlers = new BlockingCollection<Action>();

    static void Main(string[] args)
    {
        Console.WriteLine("Main (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");
        var task = new TaskWrapper();
        task.CallBack += OnCallBack;
        task.Run();

        while (true)
        {
            var action = Handlers.Take();
            action();
        }
    }

    public static void OnCallBack(object sender, Action a)
    {
        Handlers.Add(a);
    }
}

public class TaskWrapper
{
    public event EventHandler<Action> CallBack;

    public TaskWrapper()
    {
        CallBack += (sender, args) => { };
    }

    public void Run()
    {
        Task.Run(() =>
        {
            Console.WriteLine("RunTask (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");

            CallBack(this, () => Console.WriteLine("CallBack (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")"));

            while (true)
            {

            }
        });
    }
}

Also take a look on this question and this article about implementation of synchronization context in console application.

KozhevnikovDmitry
  • 1,660
  • 12
  • 27
  • Thank you for the response. This does work but I can not loop in the main thread waiting for Take. I will a look at the other question – Rich F Sep 04 '18 at 04:54
1

Thank you all for the suggestions. I eventually found this:

https://stackoverflow.com/a/20071498/10312402

and a similar solution fixes my issue. Here is my sample program now:

public static class DispatcherHelper
{
    public static void DoEvents()
    {
        DispatcherFrame frame = new DispatcherFrame();
        Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
        Dispatcher.PushFrame(frame);
    }

    private static object ExitFrame(object frame)
    {
        ((DispatcherFrame)frame).Continue = false;
        return null;
    }
}

public class Test
{
    internal static bool _busy = false;

    internal static Dispatcher _dispatcher;

    static void Main(string[] args)
    {
        Console.WriteLine("Main (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");

        _dispatcher = Dispatcher.CurrentDispatcher;

        Task.Run(() => RunTask());

        DispatcherHelper.DoEvents();
        DispatcherHelper.DoEvents();
    }

    public static Task RunTask()
    {
        Console.WriteLine("RunTask (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");

        _busy = true;

        _dispatcher.Invoke(new Action(CallBack));

        while (_busy == true)
        {

        }

        return null;
    }

    public static void CallBack()
    {
        Console.WriteLine("CallBack (ThreadId = " + Thread.CurrentThread.ManagedThreadId + ")");

        _busy = false;
    }
}

With that code I get my desired output:

Main (ThreadId = 1)
RunTask (ThreadId = 3)
CallBack (ThreadId = 1)

Plugging it back into my full program also works.

Rich F
  • 31
  • 4