Related to this question,
Is await
supposed to restore the context (specifically the context represented by Thread.CurrentContext
) for a ContextBoundObject
? Consider the below:
class Program
{
static void Main(string[] args)
{
var c1 = new Class1();
Console.WriteLine("Method1");
var t = c1.Method1();
t.Wait();
Console.WriteLine("Method2");
var t2 = c1.Method2();
t2.Wait();
Console.ReadKey();
}
}
public class MyAttribute : ContextAttribute
{
public MyAttribute() : base("My") { }
}
[My]
public class Class1 : ContextBoundObject
{
private string s { get { return "Context: {0}"; } } // using a property here, since using a field causes things to blow-up.
public async Task Method1()
{
Console.WriteLine(s, Thread.CurrentContext.ContextID); // Context1
await Task.Delay(50);
Console.WriteLine(s, Thread.CurrentContext.ContextID); // Context0
}
public Task Method2()
{
Console.WriteLine(s, Thread.CurrentContext.ContextID); // Context1
return Task.Delay(50).ContinueWith(t => Console.WriteLine(s, Thread.CurrentContext.ContextID)); // Context1
}
}
In the async
/await
case, the context isn't restored, so the remaining code after the await ends up executing in a different context.
In the .ContinueWith
case, the context isn't restored by the tpl, but instead the context ends up getting restored due to the fact that the lambda ends up being turned in to class member method. Had the lambda not used a member variable, the context wouldn't be restored in that case either.
It seems that due to this, using async
/await
or continuations with ContextBoundObject
s will result in unexpected beahvior. For example, consider if we had used the [Synchronization]
attribute (MSDN doc) on a class that uses async
/await
. The Synchronization guarantees would not apply to code after the first await
.
In Response to @Noseratio
ContextBoundObjects
don't (necessarily or by default) require thread affinity. In the example, I gave where the context ends up being the same, you don't end up being on the same thread (unless you are lucky). You can use Context.DoCallBack(...)
to get work within a context. This won't get you on to the original thread (unless the Context
does that for you). Here's a modification of Class1
demonstrating that:
public async Task Method1()
{
var currCtx = Thread.CurrentContext;
Console.WriteLine(s, currCtx.ContextID); // Context1
Console.WriteLine("Thread Id: {0}", Thread.CurrentThread.ManagedThreadId);
await Task.Delay(50);
currCtx.DoCallBack(Callback);
}
static void Callback()
{
Console.WriteLine("Context: {0}", Thread.CurrentContext.ContextID); // Context1
Console.WriteLine("Thread Id: {0}", Thread.CurrentThread.ManagedThreadId);
}
If await
were to restore the Context, my expectation would not be that the Context would be "copied" to the new thread, but rather it would be similar to how the SynchronizationContext
is restored. Basically, you would want the current Context to be captured at the await
, and then you would want the part after the await to be executed by calling capturedContext.DoCallback(afterAwaitWork)
.
The DoCallback
does the work of restoring the context. Exactly what the work of the restoring the context is is dependent on the specific context.
Based on that, it seems that it might be possible to get this behavior by creating a custom SynchronizationContext
which wraps any work posted to it in a call to DoCallback
.
We know now that await doesn't block - it frees up the calling thread.
The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active.
Because await is only valid inside async functions and modules, which themselves are asynchronous and return promises, the await expression never blocks the main thread and only defers execution of code that actually depends on the result, i.e. anything after the await expression.
As you probably recall, await captures information about the current thread when used with Task. Run . It does that so execution can continue on the original thread when it is done processing on the other thread.
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("\nenter 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("\nPress Enter to test domains...");
Console.ReadLine();
var task1 = Task.Run(() => newDomainAction("domain1"));
var task2 = Task.Run(() => newDomainAction("domain2"));
Task.WaitAll(task1, task2);
Console.WriteLine("\nPress Enter to test ob.Test...");
Console.ReadLine();
ob.Test();
Console.WriteLine("\nPress Enter to test ob2.TestAsync...");
Console.ReadLine();
var ob2 = new MyController();
ob2.TaskAsync().Wait();
Console.WriteLine("\nPress Enter to test TestContext...");
Console.ReadLine();
TestContext();
Console.WriteLine("\nPress Enter to exit...");
Console.ReadLine();
}
static void DomainCallback()
{
Console.WriteLine(String.Format("\nDomainCallback, 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);
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With