Apparently, Thread.CurrentContext
doesn't get flowed. It's interesting to see what actually gets flowed as part of ExecutionContext
, here in .NET reference sources. Especially interesting how synchronization context gets flowed explicitly via ExecutionContext.Run
, but not implicitly with Task.Run
.
I'm not sure about customized synchronization contexts (e.g. AspNetSynchronizationContext
), which may flow more thread's properties than ExecutionContext
does by default.
Here is a great read, related: "ExecutionContext vs SynchronizationContext".
Updated, it doesn't appear that Thread.CurrentContext
could be flowed at all, even if you wanted to do it manually (with something like Stephen Toub's WithCurrentCulture
). Check the implementation of System.Runtime.Remoting.Contexts.Context
, apparently it is not designed to be copied to another thread (unlike SynchronizationContext
or ExecutionContext
).
I'm not an expert in .NET remoting, but I think ContextBoundObject
-derived objects require thread affinity. I.e., they get created, accessed and destroyed on the same thread, for their lifetime. I believe this is a part of ContextBoundObject
design requirements.
Updated, based on @MattSmith's update.
Matt, your're absolutely right, there is no thread affinity for ContextBoundObject
-based objects when it's called from a different domain. The access to the whole object across different threads or contexts gets serialized if [Synchronization]
is specified on the class.
There is also no logical connection between threads and contexts, as far as I can tell. A context is something associated with an object. There can be multiple contexts running on the same thread (unlike with COM apartments), and multiple threads sharing the same context (similar to COM apartments).
Using Context.DoCallback
, it is indeed possible to continue on the same context after await
, either with a custom awaiter (as done in the code below), or with a custom synchronization context, as you mentioned in your question.
The code I played with:
using System;
using System.Runtime.Remoting.Contexts;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
public class Program
{
[Synchronization]
public class MyController: ContextBoundObject
{
/// All access to objects of this type will be intercepted
/// and a check will be performed that no other threads
/// are currently in this object's synchronization domain.
int i = 0;
public void Test()
{
Console.WriteLine(String.Format("
enter Test, i: {0}, context: {1}, thread: {2}, domain: {3}",
this.i,
Thread.CurrentContext.ContextID,
Thread.CurrentThread.ManagedThreadId,
System.AppDomain.CurrentDomain.FriendlyName));
Console.WriteLine("Testing context...");
Program.TestContext();
Thread.Sleep(1000);
Console.WriteLine("exit Test");
this.i++;
}
public async Task TaskAsync()
{
var context = Thread.CurrentContext;
var contextAwaiter = new ContextAwaiter();
Console.WriteLine(String.Format("TaskAsync, context: {0}, same context: {1}, thread: {2}",
Thread.CurrentContext.ContextID,
Thread.CurrentContext == context,
Thread.CurrentThread.ManagedThreadId));
await Task.Delay(1000);
Console.WriteLine(String.Format("after Task.Delay, context: {0}, same context: {1}, thread: {2}",
Thread.CurrentContext.ContextID,
Thread.CurrentContext == context,
Thread.CurrentThread.ManagedThreadId));
await contextAwaiter;
Console.WriteLine(String.Format("after await contextAwaiter, context: {0}, same context: {1}, thread: {2}",
Thread.CurrentContext.ContextID,
Thread.CurrentContext == context,
Thread.CurrentThread.ManagedThreadId));
}
}
// ContextAwaiter
public class ContextAwaiter :
System.Runtime.CompilerServices.INotifyCompletion
{
Context _context;
public ContextAwaiter()
{
_context = Thread.CurrentContext;
}
public ContextAwaiter GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get { return false; }
}
public void GetResult()
{
}
// INotifyCompletion
public void OnCompleted(Action continuation)
{
_context.DoCallBack(() => continuation());
}
}
// Main
public static void Main(string[] args)
{
var ob = new MyController();
Action<string> newDomainAction = (name) =>
{
System.AppDomain domain = System.AppDomain.CreateDomain(name);
domain.SetData("ob", ob);
domain.DoCallBack(DomainCallback);
};
Console.WriteLine("
Press Enter to test domains...");
Console.ReadLine();
var task1 = Task.Run(() => newDomainAction("domain1"));
var task2 = Task.Run(() => newDomainAction("domain2"));
Task.WaitAll(task1, task2);
Console.WriteLine("
Press Enter to test ob.Test...");
Console.ReadLine();
ob.Test();
Console.WriteLine("
Press Enter to test ob2.TestAsync...");
Console.ReadLine();
var ob2 = new MyController();
ob2.TaskAsync().Wait();
Console.WriteLine("
Press Enter to test TestContext...");
Console.ReadLine();
TestContext();
Console.WriteLine("
Press Enter to exit...");
Console.ReadLine();
}
static void DomainCallback()
{
Console.WriteLine(String.Format("
DomainCallback, context: {0}, thread: {1}, domain: {2}",
Thread.CurrentContext.ContextID,
Thread.CurrentThread.ManagedThreadId,
System.AppDomain.CurrentDomain.FriendlyName));
var ob = (MyController)System.AppDomain.CurrentDomain.GetData("ob");
ob.Test();
Thread.Sleep(1000);
}
public static void TestContext()
{
var context = Thread.CurrentContext;
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine(String.Format("QueueUserWorkItem, context: {0}, same context: {1}, thread: {2}",
Thread.CurrentContext.ContextID,
Thread.CurrentContext == context,
Thread.CurrentThread.ManagedThreadId));
}, null);
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
Console.WriteLine(String.Format("UnsafeQueueUserWorkItem, context: {0}, same context: {1}, thread: {2}",
Thread.CurrentContext.ContextID,
Thread.CurrentContext == context,
Thread.CurrentThread.ManagedThreadId));
}, null);
}
}
}