【问题标题】:Does C# have a "ThreadLocal" analog (for data members) to the "ThreadStatic" attribute?C# 是否对“ThreadStatic”属性有“ThreadLocal”模拟(对于数据成员)?
【发布时间】:2010-01-29 00:08:50
【问题描述】:

我最近发现 "ThreadStatic" 属性非常有用,但现在让我想要一个 "ThreadLocal" 类型属性 让我拥有基于每个线程的非静态数据成员。

现在我知道这会产生一些重要的影响,但是:

C#/.net 中是否已经内置了这样的东西? 或者由于目前看来答案是否定的(对于 .net

我可以自己想出一种合理的方法来实现它,但如果可用的话,我会使用已经存在的东西。

稻草人示例,如果它尚不存在,它将实现我正在寻找的东西:

class Foo
{
    [ThreadStatic] 
    static Dictionary<Object,int> threadLocalValues = new Dictionary<Object,int>();
    int defaultValue = 0;

    int ThreadLocalMember
    {
         get 
         { 
              int value = defaultValue;
              if( ! threadLocalValues.TryGetValue(this, out value) )
              {
                 threadLocalValues[this] = value;
              }
              return value; 
         }
         set { threadLocalValues[this] = value; }
    }
}

请原谅任何 C# 的无知。我是一名 C++ 开发人员,最近才开始接触 C# 和 .net 更有趣的功能

我仅限于 .net 3.0,也许是 3.5(项目已经/即将迁移到 3.5)。

特定用例是线程特定的回调列表(使用虚构的 [ThreadLocal] 属性) a la:

class NonSingletonSharedThing
{
     [ThreadLocal] List<Callback> callbacks;

     public void ThreadLocalRegisterCallback( Callback somecallback )
     {    
         callbacks.Add(somecallback);    
     }

     public void ThreadLocalDoCallbacks();
     {    
         foreach( var callback in callbacks )  
            callback.invoke();  
     }
}

【问题讨论】:

  • 我看不出区别。如果数据是每个线程的,为什么 ThreadStatic 不是您需要的?换句话说,本地到什么?
  • 这就是我提供示例的原因。我正在为一个类寻找每个胎面的非静态数据成员。 ThreadStatic 仅适用于静态数据成员。
  • 我真的很好奇,这个用例是什么?如果对象绑定到局部变量,则它及其所有成员实际上都是线程局部的。如果对象绑定到全局变量,则全局上的 ThreadStatic 将使它(及其成员)成为线程本地的。需要共享对象才能使线程本地存储有用,它是如何共享的,以便使选定的成员有用地成为线程本地的?
  • 我仍然看不到实用程序:(。这是您的实际用例吗?NonSingletonSharedThing 如何在多个线程之间共享?您知道 ThreadLocalDoCallbacks() 不会调用其他线程的回调那些线程对吗?据我所知,这个例子相当于在每个线程中都有一个本地回调列表,没有什么可共享的,也不需要共享。
  • 本地人存在于堆栈中,因此根据定义它们是线程本地的

标签: c# .net multithreading attributes


【解决方案1】:

Enter .NET 4.0!

如果您卡在 3.5(或更早版本),您应该查看 some functions,例如 AllocateDataSlot,它应该可以满足您的需求。

【讨论】:

  • 肯定还在 3.0 或 3.5; 4.0 目前不是我的选择。否则,4.0 的东西看起来可能就是我正在寻找的东西。可悲的是,这可能意味着 .net
  • 您总是可以使用 Reflector 从 .NET4 对 ThreadLocal 实现进行逆向工程,将其包含在您的项目中,当您最终迁移到 .NET4 时,只需删除您的逆向工程实现(其他所有内容)将保持不变)。在从 .NET2 迁移到 .NET 3.5 时,我成功地将这种技术与 TimeZoneInfo 结合使用。
  • @Milan:这看起来是最站得住脚的解决方案,因为它看起来很清楚,我正在寻找的东西在 .net 3.x 中不存在
【解决方案2】:

您应该三思而后行。您实际上是在创建内存泄漏。线程创建的每个 对象都被引用并且不能被垃圾回收。直到线程结束。

【讨论】:

  • 好点,虽然它可以被弄清楚/照顾。如果存在内置功能,则更有理由使用它,因为它会处理这个问题。
【解决方案3】:

如果您希望在每个线程的基础上存储唯一数据,您可以使用 Thread.SetData。请务必阅读http://msdn.microsoft.com/en-us/library/6sby1byh.aspx 的优缺点,因为这会影响性能。

【讨论】:

  • 谢谢。这很有趣,但有点尴尬。在不幸使用之前,我可能会使用我的稻草人示例。
【解决方案4】:

考虑:

与其尝试为对象中的每个成员变量赋予线程特定的值,不如赋予每个线程自己的对象实例。 -- 将对象作为状态传递给 threadstart,或者使 threadstart 方法成为线程将“拥有”的对象的成员,并为您生成的每个线程创建一个新实例。

编辑 (回应卡斯库尔的言论。 这是一个封装结构的例子

public class TheStructWorkerClass { private StructData TheStruct; public TheStructWorkerClass(StructData yourStruct) { this.TheStruct = yourStruct; } public void ExecuteAsync() { System.Threading.ThreadPool.QueueUserWorkItem(this.TheWorkerMethod); } private void TheWorkerMethod(object state) { // your processing logic here // you can access your structure as this.TheStruct; // only this thread has access to the struct (as long as you don't pass the struct // to another worker class) } } // now hte code that launches the async process does this: var worker = new TheStructWorkerClass(yourStruct); worker.ExecuteAsync();

现在是选项 2(将结构作为状态传递)

{ // (from somewhere in your existing code System.Threading.Threadpool.QueueUserWorkItem(this.TheWorker, myStruct); } private void TheWorker(object state) { StructData yourStruct = (StructData)state; // now do stuff with your struct // works fine as long as you never pass the same instance of your struct to 2 different threads. }

【讨论】:

  • 我不拥有围绕它的结构,因此不能按照您的建议对其进行重组。即使我这样做了,对于我的具体情况,以这种方式重组它也不合适。
  • @Catskul:如果你不能重构那个结构,你可以封装它,或者将它作为状态传递给线程。如果您将其作为状态传递,那么您需要为每个线程创建 1 个结构(但您不必更改该结构的任何内容)。要封装,您将定义一个拥有您的结构并包含 threadstart 的类。该线程将为该类的实例运行并访问该类的结构的私有副本。我会用一个例子来编辑我的回复。
  • 我实际上并不是指结构中的结构,我的意思是我正在工作的更大系统是更大项目的一部分。我无权重组它。知道何时何地创建其他线程并对其进行维护或限制不是这可以/应该做的事情。
  • 我能看到的唯一缺点是您必须能够控制创建线程。但是,在某些情况下,运行时或其他组件已经创建了线程,但您仍然必须使用每个线程的本地存储。
  • @Ivaylo Slavov 是的。在这种情况下,您可能需要使用 TLS(DataSlots 或 .Net 中的 ThreadLocal)。
【解决方案5】:

我最终实现并测试了我最初建议的版本:

public class ThreadLocal<T>
{
    [ThreadStatic] private static Dictionary<object, T> _lookupTable;

    private Dictionary<object, T> LookupTable
    {
        get
        {
            if ( _lookupTable == null)
                _lookupTable = new Dictionary<object, T>();

            return _lookupTable;
        }
    }


    private object key = new object(); //lazy hash key creation handles replacement
    private T originalValue;

    public ThreadLocal( T value )
    {
        originalValue = value;
    }

    ~ThreadLocal()
    {
        LookupTable.Remove(key);
    }

    public void Set( T value)
    {
        LookupTable[key] = value;
    }

    public T Get()
    {
        T returnValue = default(T);
        if (!LookupTable.TryGetValue(key, out returnValue))
            Set(originalValue);

        return returnValue;
    }
}

【讨论】:

  • 我认为当 Set() 之前没有被调用(返回default(T) 而不是 originalValue?)时 Get() 中存在错误。此外,规范的 IDisposable 支持是否只需要声明 ~ThreadLocal 而不需要更多的工作?
【解决方案6】:

虽然我仍然不确定您的用例何时有意义(请参阅我对问题本身的评论),但我想提供一个我认为比线程本地存储更具可读性的工作示例(无论是静态或实例)。该示例使用 .NET 3.5:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Linq;

namespace SimulatedThreadLocal
{
    public sealed class Notifier
    {
        public void Register(Func<string> callback)
        {
            var id = Thread.CurrentThread.ManagedThreadId;
            lock (this._callbacks)
            {
                List<Func<string>> list;
                if (!this._callbacks.TryGetValue(id, out list))
                {
                    this._callbacks[id] = list = new List<Func<string>>();
                }
                list.Add(callback);
            }
        }

        public void Execute()
        {
            var id = Thread.CurrentThread.ManagedThreadId;
            IEnumerable<Func<string>> threadCallbacks;
            string status;
            lock (this._callbacks)
            {
                status = string.Format("Notifier has callbacks from {0} threads, total {1} callbacks{2}Executing on thread {3}",
                    this._callbacks.Count,
                    this._callbacks.SelectMany(d => d.Value).Count(),
                    Environment.NewLine,
                    Thread.CurrentThread.ManagedThreadId);
                threadCallbacks = this._callbacks[id]; // we can use the original collection, as only this thread can add to it and we're not going to be adding right now
            }

            var b = new StringBuilder();
            foreach (var callback in threadCallbacks)
            {
                b.AppendLine(callback());
            }

            Console.ForegroundColor = ConsoleColor.DarkYellow;
            Console.WriteLine(status);
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine(b.ToString());
        }

        private readonly Dictionary<int, List<Func<string>>> _callbacks = new Dictionary<int, List<Func<string>>>();
    }

    public static class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                var notifier = new Notifier();
                var syncMainThread = new ManualResetEvent(false);
                var syncWorkerThread = new ManualResetEvent(false);

                ThreadPool.QueueUserWorkItem(delegate // will create closure to see notifier and sync* events
                {
                    notifier.Register(() => string.Format("Worker thread callback A (thread ID = {0})", Thread.CurrentThread.ManagedThreadId));
                    syncMainThread.Set();
                    syncWorkerThread.WaitOne(); // wait for main thread to execute notifications in its context

                    syncWorkerThread.Reset();
                    notifier.Execute();
                    notifier.Register(() => string.Format("Worker thread callback B (thread ID = {0})", Thread.CurrentThread.ManagedThreadId));
                    syncMainThread.Set();
                    syncWorkerThread.WaitOne(); // wait for main thread to execute notifications in its context

                    syncWorkerThread.Reset();
                    notifier.Execute();
                    syncMainThread.Set();
                });

                notifier.Register(() => string.Format("Main thread callback A (thread ID = {0})", Thread.CurrentThread.ManagedThreadId));
                syncMainThread.WaitOne(); // wait for worker thread to add its notification

                syncMainThread.Reset();
                notifier.Execute();
                syncWorkerThread.Set();
                syncMainThread.WaitOne(); // wait for worker thread to execute notifications in its context

                syncMainThread.Reset();
                notifier.Register(() => string.Format("Main thread callback B (thread ID = {0})", Thread.CurrentThread.ManagedThreadId));
                notifier.Execute();
                syncWorkerThread.Set();
                syncMainThread.WaitOne(); // wait for worker thread to execute notifications in its context

                syncMainThread.Reset();
            }
            finally
            {
                Console.ResetColor();
            }
        }
    }
}

当您编译并运行上述程序时,您应该得到如下输出: alt text http://img695.imageshack.us/img695/991/threadlocal.png

根据您的用例,我认为这就是您想要实现的目标。该示例首先添加来自两个不同上下文(主线程和工作线程)的两个回调。然后该示例首先从主线程运行通知,然后从工作线程运行。执行的回调被当前线程 ID 有效过滤。为了显示事情按预期工作,该示例添加了两个回调(总共 4 个)并再次从主线程和工作线程的上下文运行通知。

请注意,Notifier 类是一个常规实例,可以具有状态、多个实例等(同样,根据您问题的用例)。该示例未使用静态、线程静态或线程本地。

如果您能查看代码并让我知道我是否误解了您想要实现的目标,或者这样的技术是否可以满足您的需求,我将不胜感激。

【讨论】:

  • 感谢您为此答案付出的努力。但是,如果不涉及大量细节,这对我没有帮助。我真的只对 .net 3.x 中是否提供每个线程的非静态数据成员感兴趣(以每个线程静态的方式)。
【解决方案7】:

我不确定您一开始是如何生成线程的,但有一些方法可以为每个线程提供自己的线程本地存储,而无需使用您在问题中发布的代码等骇人听闻的变通方法。

public void SpawnSomeThreads(int threads)
{
    for (int i = 0; i < threads; i++)
    {
        Thread t = new Thread(WorkerThread);

        WorkerThreadContext context = new WorkerThreadContext
        {
            // whatever data the thread needs passed into it
        };

        t.Start(context);
    }
}

private class WorkerThreadContext
{
    public string Data { get; set; }
    public int OtherData { get; set; }
}

private void WorkerThread(object parameter)
{
    WorkerThreadContext context = (WorkerThreadContext) parameter;

    // do work here
}

这显然忽略了等待线程完成工作,确保对任何共享状态的访问在所有工作线程中都是线程安全的,但你明白了。

【讨论】:

  • 我不担心给每个线程它自己的线程本地存储。 c# 很容易提供线程本地存储。我对赋予对象线程本地成员很感兴趣。
【解决方案8】:

虽然发布的解决方案看起来很优雅,但它会泄漏对象。终结器 - LookupTable.Remove(key) - 仅在 GC 线程的上下文中运行,因此可能只会在创建另一个查找表时产生更多垃圾。

您需要从访问过 ThreadLocal 的每个线程的查找表中删除对象。我能想到解决这个问题的唯一优雅方法是通过弱键字典 - c# 中奇怪缺乏的数据结构。

【讨论】:

  • 这可能会更好地用作对您所指的特定答案的评论,而不是作为答案本身。我是否正确假设您指的是我提出的实现?
  • 对不起,我还不能将 cmets 添加到答案中(低代表)。我指的是您提出的实施方案。您正在使用终结器删除 LookupTable 键,乍一看似乎很好,但请考虑 GC 在其自己的线程中运行的情况。它运行时所发生的只是创建一个新的 LookupTable,并尝试从中删除键,但什么也没做。它不泄漏对象的唯一情况是 GC 在曾经访问过 ThreadLocal 的唯一线程的上下文中运行 - 这是不太可能的情况。
猜你喜欢
  • 2013-08-22
  • 2016-08-28
  • 2020-11-10
  • 1970-01-01
  • 2019-02-16
  • 1970-01-01
  • 2012-02-29
  • 2020-10-14
  • 1970-01-01
相关资源
最近更新 更多