正在等待应该恢复Thread.CurrentContext

本文关键字:Thread CurrentContext 恢复 在等待 | 更新日期: 2023-09-27 17:50:24

与这个问题有关,

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 s 的延续会导致意想不到的 beahvial。 例如,考虑我们是否在使用 async/await 的类上使用过 [Synchronization] 属性(MSDN 文档(。 同步保证不适用于第一个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获得此行为,该该将发布到它的任何工作包装在调用DoCallback中。

正在等待应该恢复Thread.CurrentContext

显然,Thread.CurrentContext不会流动。有趣的是,在.NET引用源中,实际作为ExecutionContext的一部分流动的内容。特别有趣的是,同步上下文如何通过ExecutionContext.Run显式流动,但不隐式地通过Task.Run

我不确定自定义的同步上下文(例如 AspNetSynchronizationContext (,默认情况下,线程属性的流动可能比ExecutionContext多。

这里有一个很好的阅读,相关:"ExecutionContext vs SynchronizationContext"。

更新后,即使您想手动执行此操作(使用类似斯蒂芬·图布的WithCurrentCulture(,似乎根本无法Thread.CurrentContext流动。检查System.Runtime.Remoting.Contexts.Context的实现,显然它不是为复制到另一个线程而设计的(不像SynchronizationContextExecutionContext(。

我不是 .NET 远程处理方面的专家,但我认为ContextBoundObject派生对象需要线程亲和力。 也就是说,它们在其生命周期内在同一线程上创建、访问和销毁。我相信这是ContextBoundObject设计要求的一部分。

根据@MattSmith的更新进行了更新。

Matt,你是绝对正确的,当从不同的域调用基于 ContextBoundObject 的对象时,它没有线程亲和力。如果在类上指定了[Synchronization]则跨不同线程或上下文对整个对象的访问将被序列化。

据我所知,线程和上下文之间也没有逻辑联系。上下文是与对象相关联的东西。可以有多个上下文在同一线程上运行(与 COM 单元不同(,多个线程共享同一上下文(类似于 COM 单元(。

使用

Context.DoCallback ,确实可以在 await 之后继续使用相同的上下文,无论是使用自定义等待者(如下面的代码中所做的那样(,还是使用自定义同步上下文,如您在问题中提到的。

我玩的代码:

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