await是否应该还原Thread.CurrentContext?

9

这个问题相关,

await是否应该恢复ContextBoundObject表示的上下文(特别是由Thread.CurrentContext表示的上下文)?请考虑以下内容:

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
    }
}
  • async/await情况下,上下文不会被恢复,因此await后的剩余代码将在不同的上下文中执行。

  • .ContinueWith情况下,上下文不会被tpl恢复,而是由于lambda最终被转换为类成员方法而导致上下文得到恢复。如果lambda没有使用成员变量,则在那种情况下上下文也不会得到恢复。

由于此原因,使用async/await或具有ContextBoundObject的连续处理将导致意外结果。例如,考虑如果我们在使用async/await的类上使用了[Synchronization]属性 (MSDN doc)。同步保证将不适用于第一个await后的代码。

回应 @Noseratio

ContextBoundObjects不一定(默认情况下)需要线程亲和性。在我给出上下文相同的示例中,您不会处于相同的线程上(除非您很幸运)。您可以使用Context.DoCallBack(...)在上下文中进行工作。这不会将您放回原始线程(除非Context为您执行此操作)。这里是一个修改了的Class1示例:

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

如果await恢复上下文,则我的期望不是将上下文“复制”到新线程,而是类似于如何恢复SynchronizationContext。基本上,您需要在await处捕获当前上下文,然后通过调用capturedContext.DoCallback(afterAwaitWork)来执行await后面的部分。 DoCallback完成了恢复上下文的工作。恢复上下文的确切工作取决于特定上下文。
基于这一点,似乎可以通过创建自定义SynchronizationContext来获取此行为,该SynchronizationContext在发布任何工作时都会包装在对DoCallback的调用中。
1个回答

5
显然,Thread.CurrentContext没有被传输。有趣的是,可以看到实际上作为ExecutionContext一部分被流动的内容,在.NET参考源这里。尤其有趣的是,同步上下文通过ExecutionContext.Run明确地传递,但不是通过Task.Run隐式地传递。
我不确定自定义同步上下文(例如AspNetSynchronizationContext)是否比默认情况下ExecutionContext流动了更多的线程属性。
这是一个相关的好阅读材料:"ExecutionContext vs SynchronizationContext"
更新一下,即使您想手动进行流传(例如使用Stephen Toub的WithCurrentCulture),Thread.CurrentContext似乎根本无法流传。检查System.Runtime.Remoting.Context的实现,显然它不是设计为复制到另一个线程(与SynchronizationContextExecutionContext不同)。
我不是.NET重定向专家,但是我认为派生自ContextBoundObject的对象需要线程关联。也就是说,它们在其生命周期内在同一线程上创建、访问和销毁。我相信这是ContextBoundObject设计要求的一部分。
基于@MattSmith的更新,你是绝对正确的,当从不同的域调用时,没有ContextBoundObject基础对象的线程关联。如果在类上指定了[Synchronization],则可以序列化跨不同线程或上下文对整个对象的访问。
据我所知,线程和上下文之间也没有逻辑连接。上下文是与对象相关联的东西。可以在同一线程上运行多个上下文(与COM组件不同),也可以在多个线程共享同一个上下文(类似于COM组件)。
使用Context.DoCallback,确实可以在await之后继续在相同的上下文中进行,既可以使用自定义awaiter(如下面的代码中所做的那样),也可以使用自定义同步上下文,正如你在你的问题中提到的那样。
我试玩的代码:
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);
        }
    }
}

针对您的更新:Context并不(必须)意味着特定线程(而且ContextBoundObject不需要线程亲和性)。虽然可以设置需要线程亲和性的Context。请参见我上面的编辑。 - Matt Smith
@MattSmith,太棒了。我现在已经更多地尝试了一下,你的更新绝对正确,感谢你。事实上,你已经回答了自己的问题,如果你把它作为自己的答案发布,我会点赞的。你可能还想更新链接的答案。 - noseratio - open to work
1
感谢提供额外的信息/测试。我喜欢ContextAwaiter。我希望在某个时候发布一个更新,尝试使用ContextSynchronizationContext方法。如果我这样做了,我会联系你并请你查看。再次感谢。 - Matt Smith
@MattSmith,也谢谢你,我从你的帖子和这个小研究中学到了一些很棒的东西。 - noseratio - open to work
1
我已经在另一个问题(https://dev59.com/v33aa4cB1Zd3GeqPfJXR#22798377)中添加了一个尝试使用ContextSynchronizationContext的答案。它包括了一个“pass through”同步上下文的尝试(我想知道是否有其他方法可以做到这一点)。我很感谢您提供任何评论。 - Matt Smith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接