Folks,
Lets say I store three new instances of object Car using CallContext.SetData() from thread 10, 11, 12. Those threads finish executing. Then I perform another multi-threaded operation (perhaps different operation than the first) which uses threads 10, 11, 12. Will GetData() retrieve the same three objects I stored? Or is the context somehow different now and those objects go away?
My particular use-case is the Task Parallel Library. I'm using TPL to parallelize some operations and I want to understand what happens to data stored via CallContext.SetData() inbetween TPL calls.
EDIT
Per @wageoghe suggestion I tried ThreadLocal and it worked!
updated code to prove it out:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TlsTest
{
public class Program
{
public static void Main()
{
Console.WriteLine( "-------using threadpool---------" );
UseThreadPool();
Console.WriteLine( "-------using tasks---------" );
UseTasks();
Console.WriteLine( "-------using parallel for---------" );
UseParallelFor();
Console.ReadKey();
}
public static void UseThreadPool()
{
var finish = new CountdownEvent( TotalThreads );
for ( int i = 0 ; i < TotalThreads ; i++ )
{
ThreadPool.QueueUserWorkItem( x =>
{
int id = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep( SleepMilliseconds );
if ( ThreadId.IsValueCreated )
{
Console.WriteLine( "thread [{0}], tls.thread [{1}] - value already in Tls" , id , ThreadId.Value );
}
else
{
Console.WriteLine( "thread [{0}] - no Tls value" , id );
ThreadId.Value = id;
}
Thread.Sleep( SleepMilliseconds );
finish.Signal();
} );
}
finish.Wait();
}
public static void UseTasks()
{
const TaskCreationOptions taskCreationOpt = TaskCreationOptions.None;
var allTasks = new Task[ TotalThreads ];
for ( int i = 0 ; i < TotalThreads ; i++ )
{
Task task = Task.Factory.StartNew( () =>
{
int id = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep( SleepMilliseconds );
if ( ThreadId.IsValueCreated )
{
Console.WriteLine( "thread [{0}], tls.thread [{1}] - value already in Tls" , id , ThreadId.Value );
}
else
{
Console.WriteLine( "thread [{0}] - no Tls value" , id );
ThreadId.Value = id;
}
Thread.Sleep( SleepMilliseconds );
} , taskCreationOpt );
allTasks[ i ] = task;
}
Task.WaitAll( allTasks );
}
public static void UseParallelFor()
{
var options = new ParallelOptions();
options.MaxDegreeOfParallelism = 8;
Parallel.For( 0 , TotalThreads , options , i =>
{
int id = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep( SleepMilliseconds );
if ( ThreadId.IsValueCreated )
{
Console.WriteLine( "thread [{0}], tls.thread [{1}] - value already in Tls" , id , ThreadId.Value );
}
else
{
Console.WriteLine( "thread [{0}] - no Tls value" , id );
ThreadId.Value = id;
}
Thread.Sleep( SleepMilliseconds );
} );
}
private static readonly ThreadLocal<int> ThreadId = new ThreadLocal<int>();
private const int TotalThreads = 100;
private const int SleepMilliseconds = 500;
}
}
[UPDATE]
Actually, my orignal answer (at bottom of this post) seems to be partly wrong!
I wrote a little test program that tests a scenario of storing data in the CallContext from threads and from Tasks, ThreadPool threads, and from threads in Parallel.For. In both the Tasks test and the ThreadPool test, the data stored in the CallContext was not seen again when the same thread (as determined by the ManagedThreadId) was reused. However, in the case of Parallel.For, the data stored in the CallContext WAS seen again when the same thread (as determined by ManagedThreadId) was reused. I found that very interesting. I'm not sure of those results are expected or if there is something wrong with my program.
To try each case, simply uncomment the desired test function.
You will see that Tasks and ThreadPool threads never encounter the CallContext data in subsequent reuses of a thread while Parallel.For threads DO encounter the CallContext data.
The behavior of Parallel.For seems to be inconsistent. When I run the Parallel.For case I can see that a given thread does not necessarily ALWAYS find the CallContext data when that thread is reused. For example, here is the output from one run of the program (with UseParallelFor uncommented):
thread [9] - no CallContext value
thread [10] - no CallContext value
thread [11] - no CallContext value
thread [12] - no CallContext value
thread [9], cc.thread [9] - value already in CallContext <-- this is expected as this is the main thread
thread [10] - no CallContext value
thread [13] - no CallContext value
thread [11] - no CallContext value
thread [12] - no CallContext value
thread [14] - no CallContext value
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [11], cc.thread [11] - value already in CallContext
thread [13] - no CallContext value
thread [15] - no CallContext value
thread [12], cc.thread [12] - value already in CallContext
thread [16] - no CallContext value
thread [14] - no CallContext value
thread [9], cc.thread [9] - value already in CallContext
thread [10] - no CallContext value
thread [17] - no CallContext value
thread [13], cc.thread [13] - value already in CallContext
thread [15] - no CallContext value
thread [11] - no CallContext value
thread [12] - no CallContext value
thread [14], cc.thread [14] - value already in CallContext
thread [18] - no CallContext value
thread [16] - no CallContext value
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [13] - no CallContext value
thread [15], cc.thread [15] - value already in CallContext
thread [11], cc.thread [11] - value already in CallContext
thread [17] - no CallContext value
thread [19] - no CallContext value
thread [18] - no CallContext value
thread [16], cc.thread [16] - value already in CallContext
thread [14] - no CallContext value
thread [20] - no CallContext value
thread [12], cc.thread [12] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [21] - no CallContext value
thread [15] - no CallContext value
thread [11], cc.thread [11] - value already in CallContext
thread [17], cc.thread [17] - value already in CallContext
thread [13], cc.thread [13] - value already in CallContext
thread [19] - no CallContext value
thread [22] - no CallContext value
thread [18], cc.thread [18] - value already in CallContext
thread [16] - no CallContext value
thread [20] - no CallContext value
thread [14], cc.thread [14] - value already in CallContext
thread [12], cc.thread [12] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [23] - no CallContext value
thread [15], cc.thread [15] - value already in CallContext
thread [21] - no CallContext value
thread [11], cc.thread [11] - value already in CallContext
thread [17] - no CallContext value
thread [13], cc.thread [13] - value already in CallContext
thread [19], cc.thread [19] - value already in CallContext
thread [22] - no CallContext value
thread [16], cc.thread [16] - value already in CallContext
thread [18] - no CallContext value
thread [24] - no CallContext value
thread [20], cc.thread [20] - value already in CallContext
thread [14], cc.thread [14] - value already in CallContext
thread [12], cc.thread [12] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10] - no CallContext value
thread [15], cc.thread [15] - value already in CallContext
thread [21], cc.thread [21] - value already in CallContext
thread [17], cc.thread [17] - value already in CallContext
thread [13], cc.thread [13] - value already in CallContext
thread [22], cc.thread [22] - value already in CallContext
thread [18], cc.thread [18] - value already in CallContext
thread [16], cc.thread [16] - value already in CallContext
thread [14], cc.thread [14] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [15], cc.thread [15] - value already in CallContext
thread [17], cc.thread [17] - value already in CallContext
thread [18], cc.thread [18] - value already in CallContext
thread [16], cc.thread [16] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [17], cc.thread [17] - value already in CallContext
thread [18], cc.thread [18] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
thread [9], cc.thread [9] - value already in CallContext
thread [10], cc.thread [10] - value already in CallContext
As you can see, it is NOT the case that once a value has been found in a reused that it remains in the CallContext forever. There are some cases where for a couple of iterations of a given thread that the value is not found in the CallContext so it is added. Then an iteration will report that the value is found. Then, maybe, the next iteration will say that the value was not found.
The results tell me that you should not rely on data remaining intact in the CallContext for a given thread being cleaned out between reuses of the thread. They also tell me that you should not rely on the CallContext being cleaned out between reuses of the same thread in the case of Parallel.For.
Here is my test program:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.Remoting.Messaging;
namespace CallContextTest
{
class Program
{
static void Main(string[] args)
{
//UseTasks();
//UseThreadPool();
UseParallelFor();
Console.ReadKey();
}
public static void UseThreadPool()
{
int totalThreads = 100;
CountdownEvent finish = new CountdownEvent(totalThreads);
for (int i = 0; i < totalThreads; i++)
{
int ii = i;
ThreadPool.QueueUserWorkItem(x =>
{
int id = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep(1000);
object o = CallContext.GetData("threadid");
if (o == null)
{
//Always gets here.
Console.WriteLine("thread [{0}] - no CallContext value", id);
CallContext.SetData("threadid", id);
}
else
{
//Never gets here.
Console.WriteLine("thread [{0}], cc.thread [{1}] - value already in CallContext", o, id);
}
Thread.Sleep(1000);
finish.Signal();
});
}
finish.Wait();
}
public static void UseTasks()
{
int totalThreads = 100;
TaskCreationOptions taskCreationOpt = TaskCreationOptions.None;
Task task = null;
Task[] allTasks = new Task[totalThreads];
for (int i = 0; i < totalThreads; i++)
{
int ii = i;
task = Task.Factory.StartNew(() =>
{
int id = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep(1000);
object o = CallContext.GetData("threadid");
if (o == null)
{
//Always gets here.
Console.WriteLine("thread [{0}] - no CallContext value", id);
CallContext.SetData("threadid", id);
}
else
{
//Never gets here.
Console.WriteLine("thread [{0}], cc.thread [{1}] - value already in CallContext", o, id);
}
Thread.Sleep(1000);
}, taskCreationOpt);
allTasks[i] = task;
}
Task.WaitAll(allTasks);
}
public static void UseParallelFor()
{
int totalThreads = 100;
Parallel.For(0, totalThreads, i =>
{
int ii = i;
int id = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep(1000);
object o = CallContext.GetData("threadid");
if (o == null)
{
//Sometimes gets here.
Console.WriteLine("thread [{0}] - no CallContext value", id);
CallContext.SetData("threadid", id);
}
else
{
//Sometimes gets here as threads are reused.
Console.WriteLine("thread [{0}], cc.thread [{1}] - value already in CallContext", o, id);
}
Thread.Sleep(1000);
});
}
}
}
Note, in light of the above test program and my new comments, at the top of this answer, my original discussion seems to be wrong. From my test, it appears that data stored in the CallContext is not available in subsequent reuses of the same thread id in the case of Tasks and ThreadPool threads. However, it appears that the data stored in the CallContext IS available in reuses of the same thread in the case of Parallel.For.
Disregard everything after End Update.
[End Update]
I am certainly no TPL expert, but I have been looking at CallContext.SetData (and LogicalSetData) a lot recently, so I have some idea of how the CallContext works. There are better people here on SO to describe what may or may not happen with CallContext data in the "context" of TPL.
My understanding of how CallContext.SetData works is that the "data" will be cleared when the thread goes away. So, if you create a new thread and, while executing on that thread, you store some data with CallContext.SetData, then the data will disappear when the thread dies. If you are using a ThreadPool thread, the thread never dies (well, maybe never is too strong), so the data stored via CallContext.SetData will still be there the next time some code executes in the (reused) thread.
My understanding is also that the Task Parallel Library uses the ThreadPool internally, so any data stored via CallContext.SetData will probably still be there when the underlying ThreadPool thread is used again.
It should be easy enough to write one or more small tests to see what happens when you put data in the CallContext and then check to see if it is there in subsequent uses of the same thread.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With