Why SynchronizationContext does not work properly?
Asked Answered
S

3

3

I have following code:

[TestMethod]
public void StartWorkInFirstThread()
{
    if (SynchronizationContext.Current == null)
        SynchronizationContext.SetSynchronizationContext(
            new SynchronizationContext());

    var syncContext = SynchronizationContext.Current;

    Console.WriteLine("Start work in the first thread ({0})", 
        Thread.CurrentThread.ManagedThreadId);

    var action = ((Action) DoSomethingInSecondThread);
    action.BeginInvoke(CallbackInSecondThread, syncContext);

    // Continue its own work
}

private static void DoSomethingInSecondThread()
{
    Console.WriteLine("Do something in the second thread ({0})", 
        Thread.CurrentThread.ManagedThreadId);   
}

private void CallbackInSecondThread(IAsyncResult ar)
{
    Console.WriteLine("Callback in the second thread ({0})", 
        Thread.CurrentThread.ManagedThreadId);
    var syncContext = (SynchronizationContext) ar.AsyncState;
    syncContext.Post(CallbackInFirstThread, null);
}

private void CallbackInFirstThread(object obj)
{
    Console.WriteLine("Callback in the first thread ({0})",
        Thread.CurrentThread.ManagedThreadId);
}

I expect last method to be executed in the first thread, i.e. initial thread where SynchronizationContext is taken from, because I call Post() method of this context. I.e. something like this:

Start work in the first thread (28)
Do something in the second thread (17)
Callback in the second thread (17)
Callback in the first thread (28)

Isn't it the meaning of SynchronizationContext? But actually I have following output:

Start work in the first thread (28)
Do something in the second thread (17)
Callback in the second thread (17)
Callback in the first thread (7)

What is the problem? Does something go wrong with SynchronizationContext or I have some misunderstanding?

Update: I call this method as a unit test using Resharper test runner.

Smallish answered 8/10, 2010 at 16:24 Comment(2)
You haven't shown us how these are being called, or what's running the synchronization context (e.g. a WinForms event loop). Please update your question with a short but complete example.Caulescent
This is unit test (MS Test Framework), I use Resharper unit test runner.Smallish
B
8

See http://www.codeproject.com/KB/threads/SynchronizationContext.aspx

There is the answer you need. You must override SynchronizationContext to make it properly handling your operations.

Read starting from:

Notice that DoWork is executed on thread 11, the same thread as Run1. Not much of a SynchronizationContext into the main thread. Why? What's going on? Well... This is the part when you realize that nothing is for free in life. Threads can't just switch contexts between them, they must have an infrastructure built-in into them in order to do so. The UI thread, for example, uses a message pump, and within its SynchronizationContext, it leverages the message pump to sync into the UI thread.

Bayer answered 8/10, 2010 at 17:5 Comment(3)
Send simply calls the delegate on the calling thread (no thread switching of any kind), and Post does the same thing, but simply uses the ThreadPool to do it in an async fashion. In my opinion, this class should be abstract. The default implementation of this class is confusing and useless.Smallish
If SynchronizationContext was abstract, what would you create to assign to SynchronizationContext.Current in unit tests? It's better than nothing.Idun
@Rory, there could be a NoSynchronizationContext class for that.Candlepin
R
5

Default implementation of SynchronizationContext just executes passed delegate in the calling thread (in the thread that invokes Send/Post method not the thread that captures context). If you need some particular behavior, like thread affinity for some operations, you should implement this manually. BCL contains few out-of-box implementations for simplification of UI interoperability, like WindowsFormsSynchronizationContext or DispatcherSynchronizationContext.

Rabbinical answered 8/10, 2010 at 17:1 Comment(2)
So I need "just execute passed delegate in the calling thread"Smallish
Send invokes the delegate on the calling thread, Post queues it to the thread pool.Idun
I
3

Your expectation is wrong because there's no general way to "inject" a delegate into a running thread. Your "first thread" was started in the test runner, will execute one or more tests, and will then stop - there's no way to interrupt it and tell it to run CallbackInFirstThread. The SynchronizationContext class runs Post-ed delegates in the thread pool because that's about the only option it has.

Derived classes like WindowsFormsSynchronizationContext make use of the message loop in WinForms applications to pass the Post-ed delegate to the UI thread, but there's no equivalent in a test runner.

If you want to check which SynchronizationContext the code you're testing is using, you could create your own derived class that sets a flag you can check in your test. Here's an example:

public class TestSynchronizationContext : SynchronizationContext
{
    [ThreadStatic]
    private static object _CurrentPostToken;
    /// <summary>
    /// Gets the context's token, if the current thread is executing a delegate that
    /// was posted to this context; otherwise, null.
    /// </summary>
    public static object CurrentPostToken
    {
        get
        {
            return _CurrentPostToken;
        }
    }

    public object Token { get; private set; }

    /// <summary>
    /// Gets a WaitHandle that is set after the context executes a posted delegate.
    /// </summary>
    public AutoResetEvent PostHandle { get; private set; }

    public TestSynchronizationContext(object token)
    {
        Token = token;
        PostHandle = new AutoResetEvent(false);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        try
        {
            _CurrentPostToken = Token;
            // Execute the callback on this thread, so that we can reset the context
            // when it's finished.
            d(state);
        }
        finally
        {
            _CurrentPostToken = null;
        }

        // The test method will wait on this handle so that it doesn't exit before
        // the synchronization context is called.
        PostHandle.Set();
    }
}

In StartWorkInFirstThread, set the context to an instance of TestSynchronizationContext:

SynchronizationContext.SetSynchronizationContext(
        new TestSynchronizationContext(new object()));

After you call BeginInvoke, you need to wait for the Post to happen before you exit the test, so call:

((TestSynchronizationContext)SynchronizationContext.Current).PostHandle.WaitOne(1000);

In CallbackInFirstThread you can check what context is being used with something like:

Assert.IsNotNull(TestSynchronizationContext.CurrentPostToken);

The point is that there's no easy way to actually post back to the first thread, but you can check that the right context is being used so that, when your code runs in a real application, the callback will be running in the UI thread.

Idun answered 3/1, 2013 at 17:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.