SynchronizationContext.Send to main thread fails?

I was using the SynchronizationContext.Current to send a function from a background thread to the main thread e.g. to create a gameobject.
See this code sample:

        // Context from main thread:
        var syncContext = SynchronizationContext.Current;

        // runs on main thread - works:
        var sphere0 = GameObject.CreatePrimitive(PrimitiveType.Sphere);

        // create GameObject from background task:
        await Task.Run(
            () =>
                {
                    syncContext.Post(
                        s =>
                            {
                                // works:
                                var sphere1 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                            },
                        null);
                });

        await Task.Run(
            () =>
                {
                    syncContext.Send(
                        s =>
                            {
                                // does NOT work:
                                var sphere2 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                            },
                        null);
                });

The Post method (fire-and-forget) works, but the Send fails with

CreatePrimitive can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don’t use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
UnityEngine.GameObject:CreatePrimitive(PrimitiveType)

How come?
Thanks for any help.

Send() is implemented as

public override void Send(SendOrPostCallback callback, object state)
{
    callback(state);
}

It feels useless. Throwing NotImplementedException or NotSupportedException would be more fair.

I think, the proper implementation should look more like this:

public override void Send(SendOrPostCallback callback, object state)
{
    if (SynchronizationContext.Current == this)
    {
        callback(state);
    }
    else
    {
        var waitHandle = new ManualResetEvent(false);

        lock (m_AsyncWorkQueue)
        {
            m_AsyncWorkQueue.Enqueue(new WorkRequest(callback, state));
            m_AsyncWorkQueue.Enqueue(new WorkRequest(_=> waitHandle.Set(), null));
        }

        waitHandle.WaitOne();
    }
}
1 Like

I’m surprised the Post even works in this case to be honest! Why would

ThreadPool.QueueUserWorkItem (new WaitCallback (d), state);

put you back on the main thread?

SynchronizationContext.Post is expected to execute the delegate within the corresponding context.
ThreadPool.QueueUserWorkItem schedules the execution in one of the thread pool’s threads.

Right, so I guess the above code just got lucky? The SynchronizationContext “Post” method just queues the action onto one of the threadpool threads. Is the main thread one of those? I don’t understand why that would work like OP claims.

This is what the base implementation of SynchronizationContext does. Unity has the custom UnitySynchronizationContext, which is assigned to the main thread and schedules the jobs to this thread just like WindowsFormsSynchronizationContext or DispatcherSynchronizationContext do.

Oh ok that makes sense then, and it’s good to know that it is there!

Thanks for all reactions.

Anyone around from Unity, to confirm the Send() method of UnitySynchronizationContext will be fixed?

We’re looking into it now. Can you submit a bug report so that we can properly track this issue?

I’ve gone ahead and created the bug. You can track the fix here
https://issuetracker.unity3d.com/product/unity/issues/guid/934819/