I would like to show some code according to my understanding, hopefully it can help somebody.
As aepot, dymanoid and Hans Passant (thanks to them) said, using the default SynchronizationContext
will do nothing more than Post
ing the rest of the code after await
ing to the SynchronizationContext
.
I created a very very basic and NOT optimal SynchronizationContext
to demonstrate how the basic implementation should look like. My implementation will create a new Thread
and run some Task
s in a specific context inside the same newly created Thread
.
Better implementation (but much complex) may be found here in Stephen Cleary's GitHub repository.
My implementation looks basically like following (from my GitHub repository, the code in the repository may look different in the future):
/// <summary>
/// This <see cref="SynchronizationContext"/> will call all posted callbacks in a single new thread.
/// </summary>
public class SingleNewThreadSynchronizationContext : SynchronizationContext
{
readonly Thread _workerThread;
readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _actionStatePairs = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
/// <summary>
/// Returns the Id of the worker <see cref="Thread"/> created by this <see cref="SynchronizationContext"/>.
/// </summary>
public int ManagedThreadId => _workerThread.ManagedThreadId;
public SingleNewThreadSynchronizationContext()
{
// Creates a new thread to run the posted calls.
_workerThread = new Thread(() =>
{
try
{
while (true)
{
var actionStatePair = _actionStatePairs.Take();
SetSynchronizationContext(this);
actionStatePair.Key?.Invoke(actionStatePair.Value);
}
}
catch (ThreadAbortException)
{
Console.WriteLine($"The thread {_workerThread.ManagedThreadId} of {nameof(SingleNewThreadSynchronizationContext)} was aborted.");
}
});
_workerThread.IsBackground = true;
_workerThread.Start();
}
public override void Post(SendOrPostCallback d, object state)
{
// Queues the posted callbacks to be called in this SynchronizationContext.
_actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
}
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException();
}
public override void OperationCompleted()
{
_actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(new SendOrPostCallback(_ => _workerThread.Abort()), null));
_actionStatePairs.CompleteAdding();
}
}
and here is a Demo to use it:
static void SingleNewThreadSynchronizationContextDemo()
{
var synchronizationContext = new SingleNewThreadSynchronizationContext();
// Creates some tasks to test that the whole calls in the tasks (before and after awaiting) will be called in the same thread.
for (int i = 0; i < 20; i++)
Task.Run(async () =>
{
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
// Before yielding, the task will be started in some thread-pool thread.
var threadIdBeforeYield = Thread.CurrentThread.ManagedThreadId;
// We yield to post the rest of the task after await to the SynchronizationContext.
// Other possiblity here is maybe to start the whole Task using a different TaskScheduler.
await Task.Yield();
var threadIdBeforeAwait1 = Thread.CurrentThread.ManagedThreadId;
await Task.Delay(100);
var threadIdBeforeAwait2 = Thread.CurrentThread.ManagedThreadId;
await Task.Delay(100);
Console.WriteLine($"SynchronizationContext: thread Id '{synchronizationContext.ManagedThreadId}' | type '{SynchronizationContext.Current?.GetType()}.'");
Console.WriteLine($"Thread Ids: Before yield '{threadIdBeforeYield}' | Before await1 '{threadIdBeforeAwait1}' | Before await2 '{threadIdBeforeAwait2}' | After last await '{Thread.CurrentThread.ManagedThreadId}'.{Environment.NewLine}");
});
}
static void Main(string[] args)
{
Console.WriteLine($"Entry thread {Thread.CurrentThread.ManagedThreadId}");
SingleNewThreadSynchronizationContextDemo();
Console.WriteLine($"Exit thread {Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();
}
Output:
Entry thread 1
Exit thread 1
SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'
Thread Ids: Before yield '11' | Before await1 '5' | Before await2 '5' | After last await '5'.
SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'
Thread Ids: Before yield '4' | Before await1 '5' | Before await2 '5' | After last await '5'.
SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'
Thread Ids: Before yield '12' | Before await1 '5' | Before await2 '5' | After last await '5'.
SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'
Thread Ids: Before yield '6' | Before await1 '5' | Before await2 '5' | After last await '5'.
SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'
Thread Ids: Before yield '10' | Before await1 '5' | Before await2 '5' | After last await '5'.
SynchronizationContext: thread Id '5' | type 'SynchronizationContexts.SingleNewThreadSynchronizationContext.'
Thread Ids: Before yield '7' | Before await1 '5' | Before await2 '5' | After last await '5'.