All about Async/Await, System.Threading.Tasks, System.Collections.Concurrent, System.Linq, and more…
Available since .NET 4, ThreadLocal<T> is a container that holds a separate value for every thread. In practice, ThreadLocal<T> is often convenient for storing per-thread counters, resources, or partial results.
As mentioned earlier on this blog, we have been thinking about adding a Values property to enumerate over the values from all threads that ever stored a value into the ThreadLocal<T> instance. After getting helpful feedback from this blog and other sources, we ended up implementing the capability in .NET 4.5.
To use the Values property, you have to initialize the ThreadLocal<T> instance by setting the constructor argument trackAllValues= true. The sample below demonstrates this usage:
var localResult = new ThreadLocal<int>(() => 0, trackAllValues: true);
Parallel.For(0, 10000, i =>
localResult.Value += Compute(i);
int result = localResult.Values.Sum();
If you don’t set the trackAllValues constructor argument to true, accessing the Values property will throw an InvalidOperationException. The Values property requires additional bookkeeping, and so ThreadLocal<T> requires that you declare upfront if you plan to access Values.
Most importantly, trackAllValues=true changes the lifetime of the values stored into a ThreadLocal<T>. By default (i.e., trackAllValues=false), a value stored into a ThreadLocal<T> is cleared when the ThreadLocal<T> instance is disposed/finalized OR when the corresponding thread exits, whichever happens first. This is the same behavior that ThreadLocal<T> had in .NET 4, and can be especially important when T is a large reference type.
However, removing a value when its corresponding thread exits would make the Values property poorly behaved. For example, in the code sample shown earlier in this blog post, the final summation should clearly process all partial results. It would be unacceptable to omit one of the partial results, say because the ThreadPool has decided to retire the thread that computed it.
So, if you set trackAllValues=true, the ThreadLocal instance will keep track of all values that have been stored into it, even if the corresponding threads are now gone. Then, the values can be returned via the Values property.
In addition to aggregation of partial results, the Values property can be useful for resource cleanup. The example below executes a Parallel.For loop, where each thread participating in the loop opens its own connection to the database. Once the loop completes, we can use the Values property to release all connections that have been opened:
var threadDbConn = new ThreadLocal<MyDbConnection>(() => MyDbConnection.Open(), true);
Parallel.For(0, 10000, i =>
var inputData = threadDbConn.Value.GetData(i);
foreach(var dbConn in threadDbConn.Values)
Additionally, readers of this blog suggested in comments that the Values property would be useful to simplify existing parallel loops, to aggregate data parallel code that does not use parallel loops, to implement an optimized object pool, and to gather high-performance statistics.
If you find this new property useful in your code, please let us know in the comments!
This is pretty awesome.
Now you guys just need to work on IIS being able to run parallel tasks without eating ANY worker threads from IIS. Virtual threads, separate thread pool etc, either way I should be able to parallelize task execution in IIS hosted applications without consuming ITS threads.
Two questions pop to mind:
1. You wrote "By default (i.e., trackAllValues=false), a value stored into a ThreadLocal<T> is cleared when the ThreadLocal<T> instance is disposed/finalized OR when the corresponding thread exits, whichever happens first."
If the ThreadLocal field is explicitly set to null and there are no other references to its values, will all values (Ts) stored in it be garbage collected for all existing threads?
The use case I’m thinking about is when the ThreadLocal<T> type is used as the field in a caching class and when T is a large reference type. If that caching class has a Flush() method, how should it clear the values? Set the field to null or go over the Values and clear each?
2. Will the additional bookkeeping required by using trackAllValues=true incur a performance penalty?