Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is await supposed to restore Thread.CurrentContext?

Tags:

c#

async-await

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 ContextBoundObjects 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.

like image 569
Matt Smith Avatar asked Mar 14 '14 21:03

Matt Smith


People also ask

Does await free the thread?

We know now that await doesn't block - it frees up the calling thread.

Does await start a new 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.

Does await stop the main thread?

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.

What does await Task Run do?

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.


1 Answers

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);
        }
    }
}
like image 85
noseratio Avatar answered Sep 21 '22 21:09

noseratio