閱讀633 返回首頁    go 阿裏雲 go 技術社區[雲棲]


以上下文(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.))
    {
        //...
    }
    using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.))
    {
        //...
    }
    using (ExecutionContextScope contextScope2 = new ExecutionContextScope(ExecutionContextOption.))
    {
        //...
    }
}

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);
    
    ExecutionContext.Current.SetValue(name, value2);

     Task.Factory.StartNew(() =>
        {
            using (ExecutionContextScope contextScope2 = new ExecutionContextScope())
            {
                 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
{
    
    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;
    }
}

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 16:03:54

  上一篇:go  yield在WCF中的錯誤使用——99%的開發人員都有可能犯的錯誤[下篇]
  下一篇:go  Web前端初學者,需用了解的7大HTML知識點