mardi 12 octobre 2010

Sync C# - Thread Local Storage - Introduction

Description
TLS pour les intimes, Thread Local Storage est une méthode permettant de stocker des informations au niveau du thread (dans un slot) en s'assurant qu'il existe un slot par Thread.
Le slot peut être nommé (ou non). Il doit être déclaré hors du thread (avant qu'il ne démarre) mais doit impérativement être initialisé dans le thread (car chaque dispose de sa propre copie du slot).

Petite note pour signaler que si un TLS nommé est crée, il doit impérativement être libéré par le code.
Le Garbage Collector ne libère que les TLS anonymes.

Mais pourquoi utiliser des Thread Local Storage?
A priori, on pourrait ne pas trouver cela très intéressant.
En effet, la méthode exécutée dans le thread (le worker) peut déclarer des variables locales faciles à manipuler librement... l'avantage n'est pas forcément évident.
Dans la vie réelle, le worker pourrait implémenter un code et un quantité d'objets largement plus massif qu'un exemple. Dans ce cas, la tâche est découpée dans plusieurs méthodes... et c'est la qu'intervient TLS.
Il est bien entendu possible de passer des paramètres et références de méthodes en méthodes mais s'il y a beaucoup d'objets impliquées, cela pourrait vite devenir un véritable cauchemar... rendant la signature des différentes méthodes à peine lisible.
C'est la qu'intervient TLS car il permet de retrouver facilement un référence vers le Slot depuis une quelconque des sous routines.

Le pdf "pratique de .Net 2.0 et de C# 2.0" aborde entre autre ce sujet (rechercher le texte "LocalDataStoreSlot").
Un exemple plus basique mais forcément lisible est disponible sur la page "Use thread-local storage" (la seule erreur de l'auteur est de recréer un objet Random dans le thread, ce qui a pour résultat de reproduire la même série de nombre aléatoire).
L'exemple inclus dans cet article est plus élaboré mais aussi plus proche d'un cas d'utilisation réèl.

L'attribut ThreadStatic... une alternative intéressante
TLS n'est pas la seule méthode possible. Il existe en effet l'attribut [System.ThreadStatic]qui permet de déclaré une variable statique comme ayant une valeur distincte pour chaque thread.
Cette variable doit bien évidemment être initialisée dans le thread et non avant... car sa stockage sera dissociée au démarrage du Thread.
Juste une petite note pour signaler que l'utilisation de ThreadStatic peut être plus rapide que la méthode tls.

Exemple d'utilisation de TLS
L'exemple (repris ci-dessous) utilise un TLS de type anonyme.
La méthode Worker est appelée par le Thread et donc en charge d'initialiser son slot avec une valeur.
Le worker intialise le slot (tlsCounter) qui maintient la donnée d'un compteur de tentative d'essai.
Le worker fait également appel à la méthode WaitForAck() qui est censée vérifier la réception d'un packet d'acknowledgment.
WaitForAck retourne true pour sortir d'une boucle infinie si:
  • Le packet ACK est recu (non codé dans l'exemple)
  • Le nombre de tentative de réception (tlsCounter) à dépassé un certain seuil.
Pour les besoins de l'exemple, la gestion du compteur se fait via TLS.
Cela implique donc que WaitForAck() peut accéder et modifier ce compteur.
Note:
Pour compléter l'exemple, si WaitForAck() recevait le packet d'acknoledgment, il aurait pu en retourner la valeur en utilisant un autre slot TLS (ex: tlsAckPacket).

Source: Threading_ThreadLocalStorage.cs

Résultat:
Thread 4 will check for AckDataPacket. Counter=0
Thread 4 WaitForAck during 7109 ms.
Thread 3 will check for AckDataPacket. Counter=0
Thread 3 WaitForAck during 2554 ms.
Thread 3 will check for AckDataPacket. Counter=1
Thread 3 WaitForAck during 6268 ms.
Thread 4 will check for AckDataPacket. Counter=1
Thread 4 WaitForAck during 337 ms.
Thread 4 will check for AckDataPacket. Counter=2
Thread 3 will check for AckDataPacket. Counter=2
Threads exit
Press any key to continue...

Source:
using System;
using System.Collections.Generic;
using System.Threading;

public class MyClass
{
    static readonly int PERIOD = 3000 ; // 3 secondes entre chaque appel.
    static readonly int WAIT_ACK_MAX_COUNTER = 3 ; // Nbre de fois que l'on va attendre l'ack
    private static LocalDataStoreSlot tlsCounter = Thread.AllocateDataSlot();    
    private static Random rndWaitPeriod = new Random(); // Will be used to generate a random wait périod
    
    public static void RunSnippet()
    {
        Thread thread1  = new Thread( new ThreadStart( Worker ) );    
        thread1.Start();

        Thread thread2  = new Thread( new ThreadStart( Worker ) );    
        thread2.Start();
        
        thread1.Join();
        thread2.Join();
        Console.WriteLine("Threads exit");
    }
        
    private static void Worker() {
        // Intialize Slot and Counter        
        Thread.SetData( tlsCounter, (int)0 );
        // Mimic the reception of a data packet
        do {                
            Thread.Sleep( PERIOD );            
            Console.WriteLine( "Thread {0} will check for AckDataPacket. Counter={1}", Thread.CurrentThread.ManagedThreadId, (int)Thread.GetData(tlsCounter) );            
        } while( !WaitForAck() ); // WaitForAck will query the 
    }
    
    /// <summary>
    /// Wait for an aknowledgment packet.
    /// </summary>
    /// <returns>When WaitForAck should exit because too muck attempt.</returns>
    private static bool WaitForAck(){        
        int waitTime; 
        
        // Extract the COUNTER VALUE from tls
        int currentCounter = (int)Thread.GetData( tlsCounter );
        currentCounter = currentCounter +1;
        Thread.SetData( tlsCounter, currentCounter );
        
        // CHECK EXIT condition based on counter 
        if( currentCounter >= WAIT_ACK_MAX_COUNTER )
            return true; // exit loop
        
        // ... Code checking the packet arrival
        // ... mimic processing with random waiting time
        lock( rndWaitPeriod ) {
            waitTime = rndWaitPeriod.Next(PERIOD*3);
        }
        Console.WriteLine( "Thread {0} WaitForAck during {1} ms.", Thread.CurrentThread.ManagedThreadId, waitTime );        
        Thread.Sleep( TimeSpan.FromMilliseconds(waitTime) );
        
        // Packet not arrived
        return false; 
    }
    
    #region Helper methods
    
    public static void Main()
    {
        try
        {
            RunSnippet();
        }
        catch (Exception e)
        {
            string error = string.Format("---\nThe following error occurred while executing the snippet:\n{0}\n---", e.ToString());
            Console.WriteLine(error);
        }
        finally
        {
            Console.Write("Press any key to continue...");
            Console.ReadKey();
        }
    }

    private static void WL(object text, params object[] args)
    {
        Console.WriteLine(text.ToString(), args);    
    }
    
    private static void RL()
    {
        Console.ReadLine();    
    }
    
    private static void Break() 
    {
        System.Diagnostics.Debugger.Break();
    }

    #endregion
}

Aucun commentaire: