The following article is excerpted from chapter 5 of the book Practical .NET2 and C#2.
Contents
You can greatly simplify the synchronization of acces to your resources by using the notion of affinity between threads and resources. The idea is to always access a resource using the same thread. Hence, you can remove the need to protect the resource from concurrent access since it is never shared. The .NET framework presents several mechanisms to implement this notion of affinity.
By default, a static field is shared by all the threads of a process. This behavior forces the developer to synchronize all accesses to such a field. By applying the System.ThreadStaticAttribute
attribute on a static field, you can constrain the CLR to create an instance of this static field for each thread of the process. Hence, the use of this mechanism is a good way to implement the notion of affinity between threads and resources.
It is better to avoid directly initializing such a static field during its declaration. In fact, in this case, only the thread which loads the class will complete the initialization of its own version of the field. This behavior is illustrated by the following program:
Example1.cs
using System.Threading;
class Program {
[System.ThreadStatic]
static string str = "Initial value ";
static void DisplayStr() {
System.Console.WriteLine("Thread#{0} Str={1}",
Thread.CurrentThread.ManagedThreadId , str);
}
static void ThreadProc() {
DisplayStr();
str = "ThreadProc value";
DisplayStr();
}
static void Main() {
DisplayStr();
Thread thread = new Thread( ThreadProc );
thread.Start();
thread.Join();
DisplayStr();
}
}
This program displays the following:
Thread#1 Str=Initial value
Thread#2 Str=
Thread#2 Str=ThreadProc value
Thread#1 Str=Initial value
The notion of affinity between threads and resources can also be implemented with the concept of thread local storage (often called TLS). This concept is not new, and exists at the level of Win32. In fact, the .NET framework internally uses this implementation.
The concept of TLS uses the notion of a data slot. A data slot is an instance of the System.LocalDataStoreSlot
class. A data slot can also be seen as an array of objects. The size of this array is always equal to the number of threads in the current process. Hence, each thread has its own slot in the data slot. This slot is invisible to other threads. It can be used to reference an object. For each data slot, the CLR takes care of establishing a correspondence between the threads and their objects. The Thread
class provides the two following methods to allow read and write access to an object stored in a data slot:
static public object GetData( LocalDataStoreSlot slot );
static public void SetData( LocalDataStoreSlot slot, object obj );
You have the possibility of naming a data slot in order to identify it. The Thread
class provides the following methods in order to create, obtain, or destroy a named data slot:
static public LocalDataStoreSlot AllocateNamedDataSlot( string slotName );
static public LocalDataStoreSlot GetNamedDataSlot( string slotName );
static public void FreeNamedDataSlot( string slotName );
The garbage collector does not destroy named data slots. This is the responsibility of the developer.
The following program uses a named data slot in order to provide a counter to each thread of the process. This counter is incremented for each call to the fServer()
method. This program takes advantage of the TLS in the sense where the fServer()
method does not take a reference to the counter as a parameter. Another advantage is that the developer does not need to maintain a counter himself for each thread.
Example2.cs
using System;
using System.Threading;
class Program {
static readonly int NTHREAD = 3;
static readonly int MAXCALL = 2;
static readonly int PERIOD = 1000;
static bool fServer() {
LocalDataStoreSlot dSlot = Thread.GetNamedDataSlot( "Counter" );
int counter = (int) Thread.GetData( dSlot );
counter++;
Thread.SetData( dSlot, counter );
return !( counter == MAXCALL );
}
static void ThreadProc() {
LocalDataStoreSlot dSlot = Thread.GetNamedDataSlot( "Counter" );
Thread.SetData( dSlot, (int) 0 );
do{
Thread.Sleep( PERIOD );
Console.WriteLine(
"Thread#{0} I’ve called fServer(), Counter = {1}",
Thread.CurrentThread.ManagedThreadId ,
(int)Thread.GetData(dSlot));
} while ( fServer() );
Console.WriteLine("Thread#{0} bye",
Thread.CurrentThread.ManagedThreadId );
}
static void Main() {
Console.WriteLine( "Thread#{0} I’m the main thread, hello world",
Thread.CurrentThread.ManagedThreadId );
Thread.AllocateNamedDataSlot( "Counter" );
Thread thread;
for ( int i = 0; i < NTHREAD; i++ ) {
thread = new Thread( ThreadProc );
thread.Start();
}
Thread.Sleep( PERIOD * (MAXCALL + 1) );
Thread.FreeNamedDataSlot( "Counter" );
Console.WriteLine( "Thread#{0} I’m the main thread, bye.",
Thread.CurrentThread.ManagedThreadId );
}
}
This program displays the following:
Thread#1 I’m the main thread, hello world
Thread#3 I’ve called fServer(), Counter = 0
Thread#4 I’ve called fServer(), Counter = 0
Thread#5 I’ve called fServer(), Counter = 0
Thread#3 I’ve called fServer(), Counter = 1
Thread#3 bye
Thread#4 I’ve called fServer(), Counter = 1
Thread#4 bye
Thread#5 I’ve called fServer(), Counter = 1
Thread#5 bye
Thread#1 I’m the main thread, bye.
You can call the AllocateDataSlot()
static method of the Thread
class to create an anonymous data slot. You are not responsible for the destruction of an anonymous data slot. However, you must make it so that an instance of the LocalDataStoreSlot
class be visible by all the threads. Let’s rewrite the previous program using the notion of an anonymous data slot:
Example3.cs
using System;
using System.Threading;
class Program {
static readonly int NTHREAD = 3;
static readonly int MAXCALL = 2;
static readonly int PERIOD = 1000;
static LocalDataStoreSlot dSlot;
static bool fServer() {
int counter = (int) Thread.GetData( dSlot );
counter++;
Thread.SetData( dSlot, counter );
return !( counter == MAXCALL );
}
static void ThreadProc() {
Thread.SetData( dSlot, (int) 0 );
do{
Thread.Sleep(PERIOD);
Console.WriteLine(
"Thread#{0} I’ve called fServer(), Counter = {1}",
Thread.CurrentThread.ManagedThreadId ,
(int)Thread.GetData(dSlot));
} while ( fServer() );
Console.WriteLine( "Thread#{0} bye",
Thread.CurrentThread.ManagedThreadId );
}
static void Main() {
Console.WriteLine( "Thread#{0} I’m the main thread, hello world",
Thread.CurrentThread.ManagedThreadId );
dSlot = Thread.AllocateDataSlot();
for ( int i = 0; i < NTHREAD; i++ ) {
Thread thread = new Thread( ThreadProc );
thread.Start();
}
Thread.Sleep( PERIOD * (MAXCALL + 1) );
Console.WriteLine( "Thread#{0} I’m the main thread, bye.",
Thread.CurrentThread.ManagedThreadId );
}
}
The System.ComponentModel.ISynchronizeInvoke
interface is defined as follows:
public object System.ComponentModel.ISynchronizeInvoke {
public object Invoke( Delegate method, object[] args );
public IAsyncResult BeginInvoke( Delegate method, object[] args );
public object EndInvoke( IAsyncResult result );
public bool InvokeRequired{ get; }
}
An implementation of this interface can make sure that certain methods are always executed by the same thread, in a synchronous or asynchronous way:
- In the synchronous scenario, a thread
T1
calls a method M()
on an object OBJ
. In fact, T1
calls the ISynchronizeInvoke.Invoke()
method by specifying a delegate object which references OBJ.M()
and an array containing its arguments. Another thread T2
executes the OBJ.M()
method. T1
waits for the end of the call, and retrieves the information after the return of the call.
- The asynchronous scenario is different than the synchronous scenario by the fact that
T1
calls the ISynchronizeInvoke.BeginInvoke()
method. T1
does not remain blocked while T2
executes the OBJ.M()
method. When T1
needs the information from the return of the call, it will call the ISynchronizeInvoke.EndInvoke()
method which will provide this information if T2
completed the execution of OBJ.M()
.
The ISynchronizeInvoke
interface is mainly used by the framework to force the Windows Forms technology to execute the methods of a form using the thread dedicated to the form. This constraint comes from the fact that the Windows Forms technology is built on top of the Windows messages plumbing. The same kind of problem is also addressed by the System.ComponentModel.BackgroundWorker
class.
You can develop your own implementations of the ISynchronizeInvoke
interface by inspiring yourself from the Implementing ISynchronizeInvoke example provided by Juval Lowy.