Mi sono spesso trovato di fronte al conflitto tra la programmazione Object Oriented e la necessità di usare le caratteristiche multithreading del sistema operativo. Alla fine si tratta di scrivere una procedura, sotto forma di metodo, e pur esistendo delle classi che lo fanno, in se la cosa non si presta bene ad essere racchiusa in un'ottica object oriented. Qui sotto vi propongo una classe che usa il pattern Singleton per incapsulare un thread e tutto ciò che serve per controllarlo.

Io l'ho realizzata per racchiudere i vari thread che lavorano all'interno di un Windows Service che ho creato, e l'ho trovata molto comoda.

using System;
using System.Threading; 

public class WorkingThread
 {
    private static WorkingThread m_Instance;
    private AutoResetEvent m_ExitEvent;
    private Thread m_Thread;
    private bool m_Running;

    private WorkingThread()
    {
        m_ExitEvent = new AutoResetEvent( false );
        m_Running = false;
    }

    public static WorkingThread Instance
    {
        get
        {
            if ( m_Instance == null )
                m_Instance = new WorkingThread();

            return m_Instance;
        }
    }

    public bool IsRunning
    {
        get { return m_Running; }
    }

    public void Start()
    {
        if ( !IsRunning )
        {
            m_ExitEvent.Reset();

            ThreadStart startInfo = new ThreadStart( Thread_Proc );
            m_Thread = new Thread( startInfo );
            m_Thread.Start();

            m_Running = true;
        }
        else
            throw new InvalidOperationException( "Thread already running" );
    }

    public void Stop()
    {
        if ( IsRunning )
        {
            m_ExitEvent.Set();
            m_Running = false;
        }
       
else
            throw new
InvalidOperationException( "Thread already stopped" );
    }

    protected void Thread_Proc()
    {
        WaitHandle [] handles = new WaitHandle [] { m_ExitEvent };

        while( WaitHandle.WaitAny( handles, 1000, false ) == WaitHandle.WaitTimeout )
        {
       
     // do something
            Thread.Sleep( 1000 );
        }
    }
}

L'uso è banale. La classe ha una proprietà Instance che rappresenta il thread che deve essere lanciato e ha i metodi Start() e Stop() per avviarlo e fermarlo.

public void Main()
{
    WorkerThread.Instance.Start();
    //
    WorkerThread.Instance.Stop();
}

Ancora una volta l'applicazione dei design pattern dimostra la sua potenza.

N.B. Approfondite i commenti a questo post, perchè c'è molto da imparare.


Commenti (5) -

# | Federico Dal Maso | 16.09.2004 - 08.55

La tua implementazione del singleton è errata. Nel senso che non è thread-safe.



Corri il rischio che due chiamate di WorkerThread.Instance su thread distinti creino due istanze reali del singleton.



Scrissi qualcosa tempo fa qui:

http://blogs.ugidotnet.org/471/archive/2003/10/09/731.aspx

# | Andrea Boschin | 16.09.2004 - 10.31

Ebbene sì, hai ragione da vendere.



Non c'è dubbio che se il metodo Start() viene evocato da due thread distinti, l'eventualità di lanciare due thread anzichè uno si presenta. Però non era questo il mio caso. Io avevo esclusivamente la necessità di incapsulare alcuni thread che venivano avviati e arrestati tutti dal thread principale del Windows Service. Infatti, e mi scuso di non averlo riportato nell'esempio, nel caso in cui un Thread-Singleton chiama un metodo di un altro Thread-Singleton, ad esempio per fornirgli dati da elaborare, uso un lock( this ), proprio per garantirmi che in quel preciso momento solo ed esclusivamente un thread possa accedere al metodo.



public void Enqueue( string data )

{

    lock( this )

    {

        m_Queue.Enqueue( data );

    }

}



Non conoscevo invece il dettaglio sull'ultimo esempio che porti nel tuo post. Mi sembra fantastico.



Non mi è del tutto chiara, invece, la necessità del doppio check. Comprendo l'esempio che hai portato, ma mi pare che l'if interno al lock da solo dovrebbe essere sufficiente a evitare condizioni di questo tipo.

# | Federico Dal Maso | 16.09.2004 - 19.20

"Non mi è del tutto chiara, invece, la necessità del doppio check. Comprendo l'esempio che hai portato, ma mi pare che l'if interno al lock da solo dovrebbe essere sufficiente a evitare condizioni di questo tipo."



Certo, ma porre un lock ad ogni creazione d'istanza farebbe crollare le performance.



---



Per quel che riguarda l'utilizzo su un singolo thread fai attenzione. Se ad esempio Instance è chiamato in un callback asincrono (quindi anche in risposta ad un evento grafico) in realtà va in esecuzione parallela in un thread del thread-pool dell'applicazione. Il runtime di .NET gestisce così l'asincronia.

# | Andrea Boschin | 16.09.2004 - 20.39

Concordo. Se devo chiamare Start() alla pressione di un pulsante, la cosa peggiore che mi può capitare è che l'utente lo clikki due volte consecutive in rapida sequenza e così facendo mi crei due istanze del singleton.



Ora che mi hai posto il problema mi sovviene che questo potrebbe avvenire anche nell'evento OnStart() di un servizio Windows. Devo indagare, anche se ad occhio direi che non trattandosi di un callback il problema non dovrebbe sussistere.



Intanto per non saper ne leggere ne scrivere... il controllo in più l'ho messo. Di certo non guasta.

# | FoxyBlog | 14.09.2005 - 15.09

Aggiungi Commento