如何正确释放ThreadLocal<IDisposable>中持有的元素?

31
当您使用 ThreadLocal<T>,且 T 实现了 IDisposable 接口时,您应该如何处理 ThreadLocal 中持有的成员的处理?
根据 ILSpy 的分析,ThreadLocal 的 Dispose() 和 Dispose(bool) 方法如下:
public void Dispose()
{
    this.Dispose(true);
    GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
    int currentInstanceIndex = this.m_currentInstanceIndex;
    if (currentInstanceIndex > -1 && Interlocked.CompareExchange(ref this.m_currentInstanceIndex, -1, currentInstanceIndex) == currentInstanceIndex)
    {
        ThreadLocal<T>.s_availableIndices.Push(currentInstanceIndex);
    }
    this.m_holder = null;
}

ThreadLocal似乎不会尝试调用其子成员的Dispose。我无法确定如何引用它内部分配的每个线程,以便我可以处理它。


我使用以下代码运行了一个测试,该类从未被销毁。

static class Sandbox
{
    static void Main()
    {
        ThreadLocal<TestClass> test = new ThreadLocal<TestClass>();
        test.Value = new TestClass();

        test.Dispose();
        Console.Read();
    }
}

class TestClass : IDisposable
{
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected void Dispose(bool Disposing)
    {
        Console.Write("I was disposed!");
    }
}

Dispose方法就像普通方法一样。一旦在你的示例中调用test.Dispose(),它必须执行TestClass中的Dispose方法。如果没有执行,那就很奇怪了。另外,IDisposable实现不正确。请看这里http://msdn.microsoft.com/en-us/library/b1yfkh5e.aspx。 - CharithJ
1
请注意,只有在您的类具有非托管资源时才需要使用该模式。否则,请不要添加 finalizers,因为它们会增加类创建和最终化的开销。 - Just another metaprogrammer
在 .Net 4.5 中,您可以告诉 ThreadLocal 跟踪所有 Value,这使得手动处理它们变得容易:.NET 4.5 中的新功能:ThreadLocal.Values - Sphinxxx
6个回答

14
我查看了ThreadLocal<T>中的代码以了解当前的Dispose是做什么的,它似乎有很多神秘的操作。显然是在处理与线程相关的内容。
但是,如果T本身是可释放的,它不会处理这些值的释放。
现在,我有一个解决方案-一个ThreadLocalDisposables<T>类,但在给出完整定义之前,值得思考一下如果你编写了以下代码应该发生什么:
var tl = new ThreadLocalDisposables<IExpensiveDisposableResource>();
tl.Value = myEdr1;
tl.Value = myEdr2;
tl.Dispose();

需要释放myEdr1myEdr2吗?还是只需要释放myEdr2?或者当myEdr2被分配时,是否应该释放myEdr1

对我来说语义并不清晰。

但是很明显,如果我写了这段代码:

var tl = new ThreadLocalDisposables<IExpensiveDisposableResource>(
    () => new ExpensiveDisposableResource());
tl.Value.DoSomething();
tl.Dispose();

我希望每个线程创建的资源都能被处理。

因此,我不允许直接分配可处理值给ThreadLocalDisposables,只允许使用工厂构造函数。

这是ThreadLocalDisposables

public class ThreadLocalDisposables<T> : IDisposable
    where T : IDisposable
{
    private ThreadLocal<T> _threadLocal = null;
    private ConcurrentBag<T> _values = new ConcurrentBag<T>();

    public ThreadLocalDisposables(Func<T> valueFactory)
    {
        _threadLocal = new ThreadLocal<T>(() =>
        {
            var value = valueFactory();
            _values.Add(value);
            return value;
        });
    }

    public void Dispose()
    {
        _threadLocal.Dispose();
        Array.ForEach(_values.ToArray(), t => t.Dispose());
    }

    public override string ToString()
    {
        return _threadLocal.ToString();
    }

    public bool IsValueCreated
    {
        get { return _threadLocal.IsValueCreated; }
    }

    public T Value
    {
        get { return _threadLocal.Value; }
    }
}

这个有帮助吗?


是的,这正是我想到的,我知道我需要这样做。我只是想等一下再发表自己的答案。 - Scott Chamberlain

9
在.NET 4.5中,ThreadLocal<>添加了Values属性来处理手动管理ThreadLocal对象的生命周期的问题。它返回绑定到该ThreadLocal变量的所有当前实例的列表。
一个使用Parallel.For循环访问ThreadLocal数据库连接池的示例在this MSDN article中提供。相关的代码片段如下。
var threadDbConn = new ThreadLocal<MyDbConnection>(() => MyDbConnection.Open(), true);
try
{
    Parallel.For(0, 10000, i =>
    {
        var inputData = threadDbConn.Value.GetData(i);
        ...
    });
}
finally
{
    foreach(var dbConn in threadDbConn.Values)
    {
        dbConn.Close();
    }
}

2
通常情况下,当您没有明确处理保存非托管资源的类时,垃圾回收器将最终运行并处理它。为此,该类必须具有一个处理其资源的终结器。您的示例类没有终结器。
现在,为了处理存储在ThreadLocal<T>中的类,其中T是IDisposable,您还需要自己处理。 ThreadLocal<T>只是一个包装器,它不会尝试猜测在处理它本身时其包装引用的正确行为。例如,该类可能在其线程本地存储中幸存下来。

1
当使用ThreadLocal时,对象实现IDisposeable而且它不像我的例子那么简单,仅仅让终结器来处理,该怎么办呢?这对我来说似乎是错误的。此外,如果我有一种容易跟踪对象生命周期的方法,可以手动调用Dispose,我就不会使用ThreadLocal了,我会直接跟踪它。 - Scott Chamberlain
2
@ScottChamberlain - 让终结器处理它是个坏主意。不能保证终结器会被执行。您必须在代码中明确释放对象。你可能需要包装ThreadLocal<T>以满足你的需求。 - Enigmativity
我认为这个答案不够清晰。如果一个类实现了 IDisposable 接口,那么在编写正确的代码时,我肯定认为它应该被处理;我们不应该依赖 finalizer。如果我们必须依赖于某个类的 finalizer,那么这个类的创建者不应该暴露出一个 Disposabe 方法。 - mortb
终结器是一种后备机制,因为通常不能依赖消费者编写“正确”的代码。但只有在实际持有非托管资源时才需要使用它。 - Jordão
(编辑文本以澄清只有在需要处理非托管资源时才需要使用终结器...) - Jordão

1

MSDN参考文档指出ThreadLocal的值应该在使用它们的线程完成后进行处理。但是在某些情况下,例如事件线程池中使用线程时,一个线程可能会使用该值并离开去做其他事情,然后返回该值N次。

具体示例是我想要一个Entity Framework DBContext在一系列服务总线工作线程的生命周期内持久存在。

我编写了以下类,在这些情况下使用它:

DisposeThreadCompletedValues可以由另一个线程定期手动调用,也可以激活内部监视器线程。

希望这可以帮助?

using System.Threading;
public class DisposableThreadLocal<T> : IDisposable
    where T : IDisposable
{
    public DisposableThreadLocal(Func<T> _ValueFactory)
    {
        Initialize(_ValueFactory, false, 1);
    }
    public DisposableThreadLocal(Func<T> _ValueFactory, bool CreateLocalWatcherThread, int _CheckEverySeconds)
    {
        Initialize(_ValueFactory, CreateLocalWatcherThread, _CheckEverySeconds);
    }

    private void Initialize(Func<T> _ValueFactory, bool CreateLocalWatcherThread, int _CheckEverySeconds)
    {
        m_ValueFactory = _ValueFactory;
        m_CheckEverySeconds = _CheckEverySeconds * 1000;
        if (CreateLocalWatcherThread)
        {
            System.Threading.ThreadStart WatcherThreadStart;
            WatcherThreadStart = new ThreadStart(InternalMonitor);
            WatcherThread = new Thread(WatcherThreadStart);
            WatcherThread.Start();
        }
    }

    private object SyncRoot = new object();

    private Func<T> m_ValueFactory;
    public Func<T> ValueFactory
    {
        get
        {
            return m_ValueFactory;
        }
    }

    private Dictionary<Thread, T> m_InternalDict = new Dictionary<Thread, T>();
    private Dictionary<Thread, T> InternalDict
    {
        get
        {
            return m_InternalDict;
        }
    }

    public T Value
    {
        get
        {
            T Result;
            lock(SyncRoot)
            {
                if (!InternalDict.TryGetValue(Thread.CurrentThread,out Result))
                {
                    Result = ValueFactory.Invoke();
                    InternalDict.Add(Thread.CurrentThread, Result);
                }
            }
            return Result;
        }
        set
        {
            lock (SyncRoot)
            {
                if (InternalDict.ContainsKey(Thread.CurrentThread))
                {
                    InternalDict[Thread.CurrentThread] = value;
                }
                else
                {
                    InternalDict.Add(Thread.CurrentThread, value);
                }
            }
        }
    }

    public bool IsValueCreated
    {
        get
        {
            lock (SyncRoot)
            {
                return InternalDict.ContainsKey(Thread.CurrentThread);
            }
        }
    }

    public void DisposeThreadCompletedValues()
    {
        lock (SyncRoot)
        {
            List<Thread> CompletedThreads;
            CompletedThreads = new List<Thread>();
            foreach (Thread ThreadInstance in InternalDict.Keys)
            {
                if (!ThreadInstance.IsAlive)
                {
                    CompletedThreads.Add(ThreadInstance);
                }
            }
            foreach (Thread ThreadInstance in CompletedThreads)
            {
                InternalDict[ThreadInstance].Dispose();
                InternalDict.Remove(ThreadInstance);
            }
        }
    }

    private int m_CheckEverySeconds;
    private int CheckEverySeconds
    {
        get
        {
            return m_CheckEverySeconds;
        }
    }

    private Thread WatcherThread;

    private void InternalMonitor()
    {
        while (!IsDisposed)
        {
            System.Threading.Thread.Sleep(CheckEverySeconds);
            DisposeThreadCompletedValues();
        }
    }

    private bool IsDisposed = false;
    public void Dispose()
    {
        if (!IsDisposed)
        {
            IsDisposed = true;
            DoDispose();
        }
    }
    private void DoDispose()
    {
        if (WatcherThread != null)
        {
            WatcherThread.Abort();
        }
        //InternalDict.Values.ToList().ForEach(Value => Value.Dispose());
        foreach (T Value in InternalDict.Values)
        {
            Value.Dispose();
        }
        InternalDict.Clear();
        m_InternalDict = null;
        m_ValueFactory = null;
        GC.SuppressFinalize(this);
    }
}

如果类没有终结器,为什么要调用GC.SuppressFinalize(this) - Кое Кто

1

这与 ThreadLocal<> 和内存泄漏 有关。

我猜测是因为在 T 上没有 IDisposable 约束,所以假定使用 ThreadLocal<T> 的用户会在适当的时候处理本地对象的释放。


因为否则您无法声明ThreadLocal<int>,所以它不能在T上具有IDisposable约束。 - UserControl
没错。ThreadLocal<> 被设计成可用于任何类型,因此将 IDisposable 约束放入其中是没有意义的。 - Igor Pashchuk

1

ThreadLocal.Dispose方法本身是如何被调用的?我认为它最有可能在类似于“using”块的东西中被调用。建议将ThreadLocal的“using”块与要存储的资源的“using”块包装在一起。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接