Note: this is under Unity3D
I need to run an inner task using a custom synchronization context, like the following:
This is because some objects in inner task can only be created on a specific thread that the custom synchronization context will post to, on the other hand, I want the outer task to be run normally, i.e. ThreadPool
or whatever Task.Run
uses.
Following advice from this question:
How to run a Task on a custom TaskScheduler using await?
Unfortunately, that doesn't work, the synchronization context is still the default one:
OUTER JOB: 'null'
0
INNER JOB: 'null' <------------- this context should not be the default one
0, 1, 2, 3, 4,
1
INNER JOB: 'null'
0, 1, 2, 3, 4,
2
INNER JOB: 'null'
0, 1, 2, 3, 4,
Done, press any key to exit
Code:
using System;
using System.Threading;
using System.Threading.Tasks;
internal static class Program
{
private static TaskFactory CustomFactory { get; set; }
private static async Task Main(string[] args)
{
// create a custom task factory with a custom synchronization context,
// and make sure to restore initial context after it's been created
var initial = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(new CustomSynchronizationContext());
CustomFactory = new TaskFactory(
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext()
);
SynchronizationContext.SetSynchronizationContext(initial);
// now do some work on initial context
await Task.Run(async () =>
{
Console.WriteLine($"OUTER JOB: {GetCurrentContext()}");
for (var i = 0; i < 3; i++)
{
Console.WriteLine(i);
await Task.Delay(100);
// now do some work on custom context
await RunOnCustomScheduler(DoSpecialWork);
}
});
Console.WriteLine("Done, press any key to exit");
Console.ReadKey();
}
private static void DoSpecialWork()
{
Console.WriteLine($"\tINNER JOB: {GetCurrentContext()}"); // TODO wrong context
for (var i = 0; i < 5; i++)
{
Thread.Sleep(250);
Console.Write($"\t{i}, ");
}
Console.WriteLine();
}
private static Task RunOnCustomScheduler(Action action)
{
return CustomFactory.StartNew(action);
}
private static string GetCurrentContext()
{
return SynchronizationContext.Current?.ToString() ?? "'null'";
}
}
public class CustomSynchronizationContext : SynchronizationContext
{
}
When I debug, I can see that the custom factory indeed has the custom scheduler with a custom context, but in practice it doesn't work however.
Question:
How can I get my custom scheduler to use the custom context it's been created from?
Final answer:
I was awaiting a method inside thread I wanted to avoid working in, wrapping that stuff in await Task.Run
(plus some details left out for brevity) fixed it. Now I can run my long task outside Unity thread but still make Unity calls inside it using custom factory approach in question above.
i.e. it was already working but I wasn't using it correctly
Your CustomSynchronizationContext
is indeed used for running the DoSpecialWork
method. You can confirm this by overriding the Post
method of the base class:
public class CustomSynchronizationContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
Console.WriteLine($"@Post");
base.Post(d, state);
}
}
Then run your program again and you'll see three @Post appearing in the console.
The reason that the SynchronizationContext.Current
is null
is because this property is associated with the current thread. Here is the source code:
// Get the current SynchronizationContext on the current thread
public static SynchronizationContext Current
{
get
{
return Thread.CurrentThread.GetExecutionContextReader().SynchronizationContext ?? GetThreadLocalContext();
}
}
You could install your CustomSynchronizationContext
in each thread of the ThreadPool
, but this is not recommended. It's not required either. In the absence of a SynchronizationContext.Current
the async continuations are running in the current TaskScheduler
, and the current TaskScheduler
inside the DoSpecialWorkAsync
method is the one that you have created yourself with the TaskScheduler.FromCurrentSynchronizationContext()
method, which is associated with your CustomSynchronizationContext
instance.