Applicazioni multi-threading: accediamo ai controlli in modo sicuro

Probabilmente vi sarà capitato, anche in ambiti diversi di sviluppo, di osservare che operazioni relativamente banali di accesso a controlli della GUI -l’interfaccia utente- da parte di thread diversi dal principale producono situazioni UNSAFE quali race condition o dedalock.

In questo articolo, corredato di screencast e progetto di esempio, analizzerò la relazione fra Thread e Controlli dell’interfaccia utente; argomento, questo, indispensabile da conoscere per chiunque sviluppi applicazioni multithreading e quindi tocchi temi quali asincronia e parallelismo, ma background fondamentale anche per qualunque sviluppatore Windows.

Questo l’assioma: solamente il thread che crea un controllo dell’interfaccia utente ha diritto ad utilizzarlo, sia in scrittura che in lettura. Quindi gli sono vietate operazioni anche relativamente banali quali impostare o anche solo leggere lo stato di un controllo, aggiungere un elemento ad una listbox e così via.

Vediamo un esempio pratico: partiamo da un’applicazione Windows Forms che contiene una listbox e un bottone; premendo il bottone, supponiamo che venga creato un nuovo trhead e che il thread principale rimanga in attesa che il secondo si completi (metodo Wait, o Join in .NET); infine immaginiamo che il thread così creato, su cui è in attesa il thread principale, esegua un’operazione apparentemente innocua come aggiungere un elemento alla listbox:

Post_01

Ora, come sappiamo, quando si richiede questo effetto sotto Windows il sistema operativo invierà una SendMessage alla listbox o alla sua finestra parent (la Form, in questo caso); è noto anche che la finestra è stata creata dal thread principale, nel quale “gira” la pompa dei messaggi che riceve e distribuisce tutti i messaggi destinati alla finestra principale e alle sua finestre figlie, fra cui la listbox.

Il problema però è che il trhead principale è in attesa della terminazione del thread secondario da cui è partita la richiesta di aggiunta di un elemento alla listbox. Benvenuti nel mondo dei deadlock.

Da notare che da queste situazioni si esce solamente “killando” brutalmente il processo. Sotto Visual Studio, se eseguiamo l’applicazione in modalità di debug, verremo notificati del problema dall’ambiente stesso:

Post_02

Questo il problema. Ora vediamo le possibili soluzioni: ce ne sono almeno tre.

La prima soluzione –la più semplice- consiste nell’evitare di toccare la GUI da un thread diverso dal principale, eventualmente condividendo da e verso il thread principale determinate informazioni o richieste.

La seconda soluzione è più complessa e prevede tre passi:

  1. aggiungere un oggetto delegate alla classe che rappresenta l’oggetto di interfaccia (come la listbox di prima, oppure la Form che è la sua finestra padre)
  2. “incapsulare” il metodo che accede all’interfaccia nell’istanza del delegate
  3. -il terzo passo è il più significativo- si richiama il delegate dal thread secondario attraverso il metodo Invoke. La Invoke infatti esegue il metodo incapsulato nel delegate che le si passa all’interno del thread che ha creato l’oggetto cui il delegate appartiene.

 

C’è infine una terza soluzione che funziona esclusivamente in ambito .NET e permette di risolvere il problema in modo ancora più semplice. La soluzione consiste nell’utilizzare la classe BackgroundWorker.

La classe BackgroundWorker consente di isolare un pezzo di codice all’interno di un evento chiamato DoWork perché venga eseguito in un thread separato e dedicato lanciato attraverso il metodo RunWorkerAsync.

Anche in questo caso quindi bisogna stare attenti ad evitare di accedere ai controlli dall’interno di DoWork, tuttavia dall’interno di DoWork è possibile utilizzare il metodo ReportProgress che fa scattare un evento (che si chiama ProgressChanged) il quale viene lanciato nel contesto del thread principale.

Per comprendere ancora meglio questi principi vi invito a guardare lo screencast che segue e a scaricare la demo sviluppata da zero durante la stessa registrazione.

mauro