我在静态类中有一个ThreadStatic
成员。静态类在多线程环境中使用。我想确保当线程返回到线程池(或重用)时,成员被释放(或重新初始化),因此特定线程的任何后续使用都会获得变量的新副本。成员必须保持静态,因此实例成员不会真正提供帮助。
我尝试使用ThreadLocal
,AsyncLocal
和CallContext
但这些都没有真正的帮助。(CallContext
主要用于概念验证,它是一个.NET标准应用程序,因此CallContext无论如何都不起作用)。
这只是我编写的示例代码,用于重现我的问题,具有ThreadStatic
,ThreadLocal
,AsyncLocal
和CallContext
进行测试。
class Program
{
static void Main(string[] args)
{
var act = new List<Action<int>>()
{
v=> ThreadClass.Write(v),
v=> ThreadClass.Write(v),
};
Parallel.ForEach(act, new ParallelOptions { MaxDegreeOfParallelism = 1 }, (val, _, index) => val((int)index));
Console.WriteLine($"Main: ThreadId: {Thread.CurrentThread.ManagedThreadId} ThreadStatic = {ThreadClass.ThreadStatic} ThreadLocal = {ThreadClass.ThreadLocal.Value} AsyncLocal = {ThreadClass.AsyncLocal.Value} CallContext: {ThreadClass.CallContextData}");
Console.ReadKey();
}
}
public static class ThreadClass
{
static object _lock = new object();
[ThreadStatic]
public static string ThreadStatic;
public static ThreadLocal<string> ThreadLocal = new ThreadLocal<string>(() => "default");
public static readonly AsyncLocal<string> AsyncLocal = new AsyncLocal<string>();
public static string CallContextData
{
get => CallContext.LogicalGetData("value") as string;
set => CallContext.LogicalSetData("value", value);
}
static ThreadClass()
{
AsyncLocal.Value = "default";
}
public static void Write(int id)
{
lock (_lock)
{
Console.WriteLine($"{id} Init: ThreadId: {Thread.CurrentThread.ManagedThreadId} ThreadStatic = {ThreadStatic} ThreadLocal = {ThreadLocal.Value} AsyncLocal = {AsyncLocal.Value} CallContext: {ThreadClass.CallContextData}");
ThreadStatic = $"Static({id})";
ThreadLocal.Value = $"Local({id})";
AsyncLocal.Value = $"Async({id})";
CallContextData = $"Call({id})";
Console.WriteLine($"{id} Chng: ThreadId: {Thread.CurrentThread.ManagedThreadId} ThreadStatic = {ThreadStatic} ThreadLocal = {ThreadLocal.Value} AsyncLocal = {AsyncLocal.Value} CallContext: {ThreadClass.CallContextData}");
}
}
}
上面的代码在单个线程中运行,因此可以重用该线程。
0 Init: ThreadId: 1 ThreadStatic = ThreadLocal = default AsyncLocal = default CallContext:
0 Chng: ThreadId: 1 ThreadStatic = Static(0) ThreadLocal = Local(0) AsyncLocal = Async(0) CallContext: Call(0)
--------------------
1 Init: ThreadId: 1 ThreadStatic = Static(0) ThreadLocal = Local(0) AsyncLocal = Async(0) CallContext: Call(0)
1 Chng: ThreadId: 1 ThreadStatic = Static(1) ThreadLocal = Local(1) AsyncLocal = Async(1) CallContext: Call(1)
--------------------
Main: ThreadId: 1 ThreadStatic = Static(1) ThreadLocal = Local(1) AsyncLocal = CallContext:
但是,如输出所示,当进行第二次调用并重用线程 1 时,它仍然具有线程 0 设置的值。
重新使用线程时,有没有办法ThreadStatic
变量重置为默认值或 null?
TL;博士
如果不希望变量在多线程应用程序中被多个线程重用,则没有理由将其设置为静态。
如果我们不希望一个变量被同一个线程重用,那么我们为什么要故意使用[ThreadStatic]
是值得怀疑的,因为这是它允许我们做的。
我专注于这方面的ThreadStatic
方面,因为它似乎是问题的焦点。
因此,特定线程的任何后续使用都会获得变量的新副本。
线程的使用不需要自己的变量副本 -使用该变量的方法可能需要也可能不需要自己的变量副本。这听起来像是一件令人毛骨悚然的事情,但线程本身不需要任何变量的副本。它可能正在执行与此静态类和此变量无关的操作。
当我们使用变量时,我们关心它是否是"新鲜副本"。也就是说,当我们调用使用该变量的方法时。
如果我们使用静态变量(在方法外部声明)时,我们想要的是确保它在使用它之前是新实例化的,并在我们完成处理后进行处理,那么我们可以在使用它的方法中完成它。如果需要,我们可以实例化它,处置它,甚至将其设置为null
。然而,当我们这样做时,显而易见的是,它通常消除了在使用它的方法之外声明变量的任何需要。
如果我们这样做:
public static class HasDisposableThreadStaticThing
{
[ThreadStatic]
public static DisposableThing Foo;
public static void UseDisposableThing()
{
try
{
using (Foo = new DisposableThing())
{
Foo.DoSomething();
}
}
finally
{
Foo = null;
}
}
}
我们已经实现了目标。
重新使用线程时,有没有办法将线程静态变量重置为默认值或空值?
做。每次同一线程进入方法("重新使用线程")时,它都是 null。
但是,如果这就是我们想要的,那么为什么不这样做呢?
public static class HasDisposableThreadStaticThing
{
public static void UseDisposableThing()
{
using (var foo = new DisposableThing())
{
foo.DoSomething();
}
}
}
结果完全相同。每个线程都以DisposableThing
的新实例开始,因为当它执行该方法时,它会声明变量并创建一个新实例。而不是将其设置为null
引用超出范围。
两者之间的唯一区别是,在第一个例子中,DisposableThing
在类之外公开公开。这意味着其他线程可以使用它而不是声明自己的变量,这很奇怪。既然他们还需要确保在使用它之前将其实例化,为什么不也像第二个示例一样创建自己的实例呢?
确保每次在静态方法中需要变量时都对其进行初始化和释放的最简单和最正常的方法是在静态方法中本地声明该变量并创建一个新实例。然后,无论有多少线程同时调用它,它们都将各自使用一个单独的实例。
不幸的是,ThreadPool 没有提供 API 来侦听重池事件来普遍执行此操作。但是,如果您可以控制队列工作到 ThreadPool 的每个位置,则可以编写一个简单的包装器来执行所需的操作。
public struct DisposableThreadStatic<T> : IDisposable where T : class, IDisposable
{
[ThreadStatic]
private static T ts_value;
private bool _shouldDispose;
public T Value => ts_value;
public static DisposableThreadStatic<T> GetOrCreate(Func<T> creator)
{
if (ts_value == null)
{
ts_value = creator();
return new DisposableThreadStatic<T>() { _shouldDispose = true };
}
return default;
}
public void Dispose()
{
if (_shouldDispose && ts_value != null)
{
ts_value.Dispose();
ts_value = null;
}
}
}
有了这个,你可以用这个来包装你的线程函数。
ThreadPool.QueueUserWorkItem(_ =>
{
using var dts = DisposableThreadStatic<MyDisposable>.GetOrCreate(() => new MyDisposable());
// Use value, call any other functions, etc.
dts.Value.Func();
});
在调用堆栈中更深的位置使用相同的GetOrCreate
调用将只返回缓存的值,并且只有最顶层的调用(工作完成时)才会释放它。