Ожидается ли ожидание восстановления Thread.CurrentContext?
Связанный с этим вопросом,
Является await
должен восстановить контекст (в частности, контекст, представленный Thread.CurrentContext
) для ContextBoundObject
? Рассмотрим ниже:
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
В этом случае контекст не восстанавливается, поэтому оставшийся код после ожидания заканчивается выполнением в другом контексте.в
.ContinueWith
В этом случае контекст не восстанавливается с помощью tpl, но вместо этого контекст заканчивается восстановлением из-за того, что лямбда в конечном итоге превращается в метод члена класса. Если бы лямбда не использовала переменную-член, контекст в этом случае также не был бы восстановлен.
Кажется, что из-за этого, используя async
/ await
или продолжения с ContextBoundObject
s приведет к неожиданному поведению. Например, рассмотрим, использовали ли мы [Synchronization]
атрибут ( документ MSDN) в классе, который использует async
/ await
, Гарантии синхронизации не будут применяться к коду после первого 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)
,
DoCallback
выполняет работу по восстановлению контекста. В чем именно заключается работа по восстановлению контекста, зависит от конкретного контекста.
Исходя из этого, кажется, что возможно получить такое поведение, создав SynchronizationContext
который оборачивает любую работу, опубликованную на нем в вызове DoCallback
,
1 ответ
По-видимому, Thread.CurrentContext
не течет. Интересно посмотреть, что на самом деле происходит как часть ExecutionContext
, вот в.NET справочные источники. Особенно интересно, как контекст синхронизации передается явно через ExecutionContext.Run
, но не безоговорочно с Task.Run
,
Я не уверен насчет пользовательских контекстов синхронизации (например, AspNetSynchronizationContext
), который может передавать больше свойств потока, чем ExecutionContext
делает по умолчанию.
Вот отличная статья: "ExecutionContext vs SynchronizationContext".
Обновлено, похоже, что Thread.CurrentContext
может течь вообще, даже если вы хотите сделать это вручную (с чем-то вроде Стивена Туба WithCurrentCulture
). Проверьте выполнение System.Runtime.Remoting.Contexts.Context
по-видимому, он не предназначен для копирования в другой поток (в отличие от SynchronizationContext
или же ExecutionContext
).
Я не эксперт в удаленном взаимодействии.NET, но я думаю ContextBoundObject
-приобретенные объекты требуют соответствия потоков. Т.е. они создаются, получают доступ и уничтожаются в одном потоке в течение своей жизни. Я считаю, что это часть ContextBoundObject
требования к дизайну.
Обновлено на основе обновления @MattSmith.
Мэтт, вы абсолютно правы 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);
}
}
}