【问题标题】:How to store per thread contextual data in c#?如何在 C# 中存储每个线程的上下文数据?
【发布时间】:2015-06-10 16:24:42
【问题描述】:

我的具体需求是存储远程 IP,以便在记录时打印它,而无需在每个方法调用中显式传递它。一种每个线程的环境变量范围。

我想有一个每个线程的单例,以便记录器可以访问它。有没有更好/更安全的方法?

让它与异步一起工作是一个加号。

我理想的 API 应该是这样的:

using (new LogScope("key1", "value1"))
{
   Call1();
}

using (new LogScope("key1", "value1"))
{
    Call1();
}

void Call1()
{
    using (new LogScope("key2", "value2"))
    {
        Call2();     // key1:value1 key2:value2
        using (new LogScope("key1", "value3"))
        {
            Call2(); // key1:value3 key2:value2
        }
    }
    using (new LogScope("key1", "value3"))
    {
        Call2();     // key1:value3
    }
}

void Call2()
{
    foreach (var kv in LogScope.Context) Console.Write("'{0}':'{1}' ");
    Console.WriteLine();
}

【问题讨论】:

  • 您希望在foreach 循环中看到什么?仅内部 LogScope 的值(仅限 key2),或两者的组合 LogScopes(key1 + key2)。如果结合起来,LogScope 实例的目的是什么。当您退出 using 块时会处理什么?
  • 澄清了我的问题。
  • 感谢您的澄清。我想我明白了你的意图,但仍有一种情况没有完全涵盖:当你退出 using 块时。您是否希望键/值恢复到进入范围之前的状态?我认为是的,但我想确定一下。而且您的示例并没有完全涵盖这种情况。
  • 是的。我希望范围仅在范围内覆盖。
  • 那你可以看看我的回答。它应该完全按照您的描述工作。

标签: c# thread-local


【解决方案1】:

不是最优化的实现。但它应该做你正在寻找的东西。它至少应该展示一些您可以重复使用的原则。

public class LogScope : IDisposable
{
    private static readonly ThreadLocal<Stack<Dictionary<string, string>>> currentDictionary =
        new ThreadLocal<Stack<Dictionary<string, string>>>(() => new Stack<Dictionary<string, string>>());

    public LogScope(string key, string value)
    {
        var stack = currentDictionary.Value;
        Dictionary<string, string> newDictionary = null;
        if (stack.Count == 0)
        {
            newDictionary = new Dictionary<string, string>();
        }
        else
        {
            newDictionary = new Dictionary<string, string>(stack.Peek());
        }

        newDictionary[key] = value;

        stack.Push(newDictionary);
    }

    public void Dispose()
    {
        currentDictionary.Value.Pop();
    }

    public static IEnumerable<KeyValuePair<string, string>> Context
    {
        get
        {
            var stack = currentDictionary.Value;
            if (stack.Count == 0)
            {
                return Enumerable.Empty<KeyValuePair<string, string>>();
            }
            else
            {
                return stack.Peek();
            }
        }
    }
}

编辑:我应该指定在我的LogScope 实现中,LogScope 的实例只有在它被实例化并在同一个线程上处理时才能正常工作。如果您在一个线程中创建LogScope,然后在另一个线程中将其释放,则结果未定义。同样,当 LogScope 实例生效时,其键/值将仅在该线程中可见。

【讨论】:

  • 我想出了一个类似的实现。线程完成时 currentDictionary 会被释放吗?
  • 它最终会被处理掉。当线程完成时,不再有对该线程特定堆栈的引用,因此它会按预期进行垃圾回收。
  • 对于任何自定义范围:DI 容器应该为其提供开箱即用的单例实例。例如 Ninject:(参见自定义)github.com/ninject/ninject/wiki/Object-Scopes
【解决方案2】:

a) 如果你想使用每线程单例,你绝对不应该自己写,有开箱即用的解决方案。主要是所有现代 DI 框架都支持实例化选项,如单例和每个线程的单例。只是 Ninject 的一个例子:

kernel.Bind(typeof (IAnyThing)).To(typeof (AnyThing)).InThreadScope();

或者如果你不想使用/实现接口:

kernel.Bind(typeof (AnyThing)).To(typeof (AnyThing)).InThreadScope();

在绑定之后,无论您在哪里请求 DI 容器,您都会获得一个 AnyThing 实例,每个线程都有一个单例。 AnyThing 可以是任何东西:比如 POCO。

编辑: 如果您想要任何 自定义 范围,那么只需实现您的范围定义,留下容器的其余部分 look for Custom in this page

b) 你可以使用ThreadLocal&lt;T&gt;

编辑: 在建议 ThreadLocal 之前,请注意您在问题中提到的异步期望。是的,ThreadLocal 与异步不“兼容”。

但这不是问题的根源:线程亲和性 与大多数并发编程设计模式 (CDP) 不“兼容”,因为许多 CDP 使用 线程的概念“不相关性”,如池、序列化到消息队列、编组到其他线程等。因此,期望线程亲和力和期望异步工作似乎不是一个好主意,应该被丢弃或更换。请注意,这不是关于 async 关键字,而是关于异步控制流。 结束编辑

请参考sample code in msdn doc

    // Demonstrates: 
    //      ThreadLocal(T) constructor 
    //      ThreadLocal(T).Value 
    //      One usage of ThreadLocal(T) 
    static void Main()
    {
        // Thread-Local variable that yields a name for a thread
        ThreadLocal<string> ThreadName = new ThreadLocal<string>(() =>
        {
            return "Thread" + Thread.CurrentThread.ManagedThreadId;
        });

        // Action that prints out ThreadName for the current thread
        Action action = () =>
        {
            // If ThreadName.IsValueCreated is true, it means that we are not the 
            // first action to run on this thread. 
            bool repeat = ThreadName.IsValueCreated;

            Console.WriteLine("ThreadName = {0} {1}", ThreadName.Value, repeat ? "(repeat)" : "");
        };

        // Launch eight of them.  On 4 cores or less, you should see some repeat ThreadNames
        Parallel.Invoke(action, action, action, action, action, action, action, action);

        // Dispose when you are done
        ThreadName.Dispose();
    }

【讨论】:

  • ThreadLocal 不适用于异步。看来我需要 CallContext.LogicalGetData() 和 CallContext.LogicalSetData() 但它需要携带“名称”参数。
  • Gatis,你是对的,请在我的回答中查看我的编辑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-15
  • 1970-01-01
  • 1970-01-01
  • 2021-02-13
  • 1970-01-01
  • 2011-01-27
  • 1970-01-01
相关资源
最近更新 更多