Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
248 views
in Technique[技术] by (71.8m points)

c# - Is await supposed to restore Thread.CurrentContext?

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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);
        }
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...