以上下文(Context)的形式创建一个共享数据的容器
在很多情况下我们具有这样的需求:为一组相关的操作创建一个执行上下文并提供一个共享的数据容器,而不是简单地定义一个全局变量,或者将数据通过参数传来传去。这样的上下文一般具有其生命周期,它们在目标操作开始执行的时候被激活,在执行完成之后被回收。该上下文一般不能跨越多个线程,以避免多个线程操作相同的数据容器造成数据的不一致。针对这个需求,我们写了一个非常简单的例子,有兴趣的朋友可以看看。[源代码从这里下载]
目录
一、ExecutionContext的基本编程方式
二、异步调用的问题
三、ExecutionContext
四、DependentExecutionContext
五、ExecutionContextScope
一、ExecutionContext的基本编程方式
我将这个作为数据容器的上下文命名为ExecutionContext,我完全借鉴了TransactionScope的编程方式来设计这个ExecutionContext。如下的代码片段体现了ExecutionContext最基本的编程方式:我们通过ExecutionContextScope 来创建当前ExecutionContext,并且控制它的生命周期。当前ExecutionContext通过静态属性Current获取。我们分别调用GetValue和SaveValue进行上下文数据项的获取和设置。
using (ExecutionContextScope contextScope = new ExecutionContextScope()) { //Set ExecutionContext.Current.SetValue(“ActivityID”, “A001”); //Get string activityId = ExecutionContext.Current.GetValue<string>(“ActivityID”) }
和TransactionScope一样,ExecutionContextScope 也支持嵌套。具体来说,当我们采用嵌套的ExecutionContextScope 时,有对应着如下三种不同的上下文共享行为:
- Required: 外层的ExecutionContext直接被内层使用;
- RequiresNew:内层创建一个全新的ExecutionContext;
- Suppress:外层的ExecutionContext在内层中使被屏蔽掉,内层的当前ExecutionContext不存在。
如下的代码片段反映了嵌套使用ExecutionContextScope 的编程方式,上述的三种行为通过作为ExecutionContextScope构造函数参数的ExecutionContextOption枚举来控制。
using (ExecutionContextScope contextScope1 = new ExecutionContextScope()) { //... using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.Required)) { //... } using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.RequiresNew)) { //... } using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.Suppress)) { //... } }
ExecutionContext基本的编程方式,以及三种ExecutionContextScope 嵌套所体现的ExecutionContext创建/共享机制可以通过如下的Unit Test代码来体现:
[TestMethod] public void SetAndGetContexts1() { string name = Guid.NewGuid().ToString(); string value1 = Guid.NewGuid().ToString(); string value2 = Guid.NewGuid().ToString(); //1. Outside of ApplicationContextScope: ApplicationContext.Current = null Assert.IsNull(ExecutionContext.Current); //2. Current ApplicationContext is avilable in the ApplicationContextScope. using (ExecutionContextScope contextScope = new ExecutionContextScope()) { ExecutionContext.Current.SetValue(name, value1); Assert.AreEqual<string>(value1, ExecutionContext.Current.GetValue<string>(name)); } //3. Nested ApplicationContextScope: ApplicationContextOption.Required using (ExecutionContextScope contextScope1 = new ExecutionContextScope()) { ExecutionContext.Current.SetValue(name, value1); using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.Required)) { Assert.AreEqual<string>(value1, ExecutionContext.Current.GetValue<string>(name)); ExecutionContext.Current.SetValue(name, value2); Assert.AreEqual<string>(value2, ExecutionContext.Current.GetValue<string>(name)); } Assert.AreEqual<string>(value2, ExecutionContext.Current.GetValue<string>(name)); } //4. Nested ApplicationContextScope: ApplicationContextOption.RequiresNew using (ExecutionContextScope contextScope1 = new ExecutionContextScope()) { ExecutionContext.Current.SetValue(name, value1); using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.RequiresNew)) { Assert.IsNotNull(ExecutionContext.Current); Assert.IsNull(ExecutionContext.Current.GetValue<string>(name)); ExecutionContext.Current.SetValue(name, value2); Assert.AreEqual<string>(value2, ExecutionContext.Current.GetValue<string>(name)); } Assert.AreEqual<string>(value1, ExecutionContext.Current.GetValue<string>(name)); } //5. Nested ApplicationContextScope: ApplicationContextOption.Supress using (ExecutionContextScope contextScope1 = new ExecutionContextScope()) { ExecutionContext.Current.SetValue(name, value1); using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.Suppress)) { Assert.IsNull(ExecutionContext.Current); } Assert.AreEqual<string>(value1, ExecutionContext.Current.GetValue<string>(name)); } }
二、异步调用的问题
如果具有当前ExecutionContext的程序以异步的方式执行相应的操作,我们希望当前操作和异步操作使用不同的数据容器,否则就会出现并发问题;但是我们又希望在异步操作开始执行的时候,当前的上下文数据能够自动地拷贝过去。为此我们依然借鉴TransactionScope的方式,定义了一个DependentContext(对应着DependentTransaction)。在异步操作开始执行之前,我们根据当前ExecutionContext创建一个DependentContext,此时当前ExecutionContext相应数据项会拷贝到DependentContext中。在异步操作代码中,我们根据DependentContext创建ExecutionContextScope ,那么通过Current属性返回的实际上就是这么一个DependentContext。由于DependentContext和当前ExecutionContext各自具有自己的数据容器,针对它们的操作互不影响。如下所示的相应的编程方式:
using (ExecutionContextScope contextScope1 = new ExecutionContextScope()) { ExecutionContext.Current.SetValue(name, value1); DependentContext depedencyContext = ExecutionContext.Current.DepedentClone(); ExecutionContext.Current.SetValue(name, value2); Task.Factory.StartNew(() => { using (ExecutionContextScope contextScope2 = new ExecutionContextScope(depedencyContext)) { string value1 = ExecutionContext.Current.GetValue<string>(name); } }); }
相应的编程方式,已经异步线程和当前线程上下文的独立性也可以通过如下所示的Unit Test代码来体现。
[TestMethod] public void SetAndGetContexts2() { string name = Guid.NewGuid().ToString(); string value1 = Guid.NewGuid().ToString(); string value2 = Guid.NewGuid().ToString(); //1. Change current ApplicationContext will never affect the DependentContext. using (ExecutionContextScope contextScope1 = new ExecutionContextScope()) { ExecutionContext.Current.SetValue(name, value1); DependentContext depedencyContext = ExecutionContext.Current.DepedentClone(); ExecutionContext.Current.SetValue(name, value2); Task<string> task = Task.Factory.StartNew<string>(() => { using (ExecutionContextScope contextScope2 = new ExecutionContextScope(depedencyContext)) { return ExecutionContext.Current.GetValue<string>(name); } }); Assert.AreEqual<string>(value1, task.Result); Assert.AreEqual<string>(value2, ExecutionContext.Current.GetValue<string>(name)); } //2. Change DependentContext will never affect the current ApplicationContext. using (ExecutionContextScope contextScope1 = new ExecutionContextScope()) { ExecutionContext.Current.SetValue(name, value1); DependentContext depedencyContext = ExecutionContext.Current.DepedentClone(); Task<string> task = Task.Factory.StartNew<string>(() => { using (ExecutionContextScope contextScope2 = new ExecutionContextScope(depedencyContext)) { ExecutionContext.Current.SetValue(name, value2); return ExecutionContext.Current.GetValue<string>(name); } }); Assert.AreEqual<string>(value2, task.Result); Assert.AreEqual<string>(value1, ExecutionContext.Current.GetValue<string>(name)); } }
三、ExecutionContext
现在我们来讨论具体的设计和实现,先来看看表示当前执行上下文的ExecutionContext的定义。如下面的代码片段所示,ExecutionContext实际上是利用了通过Items属性表示的字典对象作为保存数据的容器,GetValue和SetValue实际上就是针对该字典的操作。表示当前ExecutionContext的静态属性Current实际上是返回一个应用了ThreadStaticAttribute特性的静态字段current,意味着ExecutionContext是基于某个线程的,每个线程的当前ExecutionContext是不同的。方法DepedentClone用于创建DependentContext 以实现当前上下文数据向异步线程的传递。
[Serializable] public class ExecutionContext { [ThreadStatic] private static ExecutionContext current; public IDictionary<string, object> Items { get; internal set; } internal ExecutionContext() { this.Items = new Dictionary<string, object>(); } public T GetValue<T>(string name, T defaultValue = default(T)) { object value; if (this.Items.TryGetValue(name, out value)) { return (T)value; } return defaultValue; } public void SetValue(string name, object value) { this.Items[name] = value; } public static ExecutionContext Current { get { return current; } internal set { current = value; } } public DependentContext DepedentClone() { return new DependentContext(this); } }
四、DependentExecutionContext
如下所示的DependentContext的定义,它是ExecutionContext的子类。我们我们根据指定的ExecutionContext 对象创建一个DependentContext对象的时候,它的上下文数据项会自动拷贝到创建的DependentContext之中。
[Serializable] public class DependentContext: ExecutionContext { public Thread OriginalThread { get; private set; } public DependentContext(ExecutionContext context) { this.OriginalThread = Thread.CurrentThread;
this.Items = new Dictionary<string, object>(context.Items); } }
五、ExecutionContextScope
如下所示的是ExecutionContextScope的定义,它实现了IDisposable接口。在ExecutionContextScope被创建之前,当前ExecutionContext 被保存下来。第一个构造函数根据指定的ExecutionContextOption来对当前ExecutionContext 进行相应的设置;第二个构造函数则直接将指定的DependentContext 作为当前的ExecutionContext 。
public class ExecutionContextScope:IDisposable { private ExecutionContext originalContext = ExecutionContext.Current; public ExecutionContextScope(ExecutionContextOption contextOption = ExecutionContextOption.Required) { switch (contextOption) { case ExecutionContextOption.RequiresNew: { ExecutionContext.Current = new ExecutionContext(); break; } case ExecutionContextOption.Required: { ExecutionContext.Current = originalContext ?? new ExecutionContext(); break; } case ExecutionContextOption.Suppress: { ExecutionContext.Current = null; break; } } } public ExecutionContextScope(DependentContext dependentContext) { if (dependentContext.OriginalThread == Thread.CurrentThread) { throw new InvalidOperationException("The DependentContextScope cannot be created in the thread in which the DependentContext is created."); } ExecutionContext.Current = dependentContext; } public void Dispose() { ExecutionContext.Current = originalContext; } }