TaskScheduler.FromCurrentSynchronizationContext() (.NET 4.0 Beta 2)

by Marco 7. gennaio 2010 20.39

Tutti coloro che hanno sviluppato interfaccie che gestiscono una grande quantità di dati o che eseguono lunghe operazioni, si sono dovuti scontrare prima o poi con il problema del “blocco” dell’interfaccia dovuto al fatto che la “pesantezza” dell’operazione veniva eseguita nel famoso “thread di UI” cioè quel thread che ha il compito di gestire la coda dei messaggi associata ai controlli che lui stesso ha creato. L’indisponibilità di questo thread(speso a fare operazioni “lunghe”) porta l’interfaccia a bloccarsi in quanto i messaggi nella coda non vengono gestiti e quindi la nostra interfaccia non si può ridisegnare correttamente o reagire all’attività dei nostri utenti.

Per ovviare a questo problema l’unica soluzione è quella di effettuare l’operazione “lunga” in un thread separato, così da liberare il “thread di UI” che può senza problemi gestire la sua bella coda di messaggi. L’unico problema di questo approccio è il fatto che nel momento che dobbiamo riflettere il risultato dell’ operazione “lunga” sull’interfaccia non lo possiamo fare da un thread che non sia quello di UI. L’unico modo di risolvere questo problema è quello di fare il “marshal” sul “thread di UI” ovvero ci serve un meccanismo per passare i dati al thread di UI e far fare a lui la modifica dei nostri controlli. I 2 principali framework grafici che “soffrono” di questo problema sono chiaramente Windows Form e WPF. Su internet si trovano tonnellate di articoli che spiegano come fare il “marshal” nelle varie piattaforme, l’operazione è abbastanza semplice, l’unico difetto è che purtroppo le modalità sono differenti da un framework ad un’altro(esempio l’interfaccia ISyncronizeInvoke per Windows Form e la classe Dispatcher per WPF) e questo è un bel problema per chi deve ad esempio scrivere una libreria che deve essere asincrona e utilizzabile ALLO STESSO MODO su qualsiasi piattaforma grafica venga usata(diciamo che più che di piattaforma grafica possiamo parlare di contesto di esecuzione).

Una delle possibilità che abbiamo per generalizzare la possibilità di gestire il marshal è quella di utilizzare la classe SyncronizationContext che attraverso la proprietà statica Current ci mette a disposizione un paio di metodi(Post se vogliamo un’esecuzione asincrona o Send la vogliamo sincrona) per eseguire un delegate nel giusto Context(diciamo il thread di UI se stiamo usando WPF o WindowsForm, ma context è un concetto generico).

Ad esempio se vogliamo modificare il content di un bottone in WPF un’ipotetico handler dell’evento click potrebbe essere:

private void button1_Click(object sender, RoutedEventArgs e)
{          
  SynchronizationContext context = SynchronizationContext.Current;
   ThreadPool.QueueUserWorkItem(a =>
           {
              System.Threading.Thread.Sleep(5000);//Simulo il carico di lavoro
             context.Post(o=>this.button1.Content=o,"Nuova Content del bottone"); //modifico una proprietà di interfaccia
           });
  }

in questo esempio prima salvo il Current context attraverso la proprietà SyncronizationContext.Current e poi in un thread del thread pool di .NET(quindi non un thread di UI) faccio il “marshal” asincrono usando il metodo Post sul context salvato(il context nel thread di thread pool non è valido). Il fatto che sto utilizzando WPF significa che qualcuno(il runtime di WPF) ha “attaccato” come Current context una implementazione personalizzata(la classe è la DispatcherSynchronizationContext nel namespace System.Windows.Threading) della classe SynchronizationContext che al suo interno usa la classe Dispatcher per fare il marshal in modo corretto.

Questo significa che se devo eseguire operazioni asincrone, ma non voglio legarmi a nessuna piattaforma(non solo UI) applicativa utilizzando questa classe(sperando che qualcuno abbia correttamente implementato una versione della classe SyncronizationContext) e non voglio avere problemi nel momento in cui dovrò eseguire la “callback” di finalizzazione dell’operazione, utilizzare l’astrazione attraverso SyncronizationContext mi risolve il mio problema di “generalizzazione”.

Se volete scrivere ancora meno codice e restare ad un livello ancora più alto sotto il namespace System.ComponentModel troviamo la classe AsyncOperationManager che fa un pò di lavoro per noi(come ad esempio salvarsi il context o inizializzare un context di default se non è presente, come ad esempio in una console application, il default context utilizza il thread pool nel Send/Post)attraverso un metodo “factory” di operazioni asincrone, permettendoci di focalizzarci di più sul problema di “business” e meno su quello “tecnico”, il System.ComponentModel.BackgroundWorker si basa su questa classe.

Esempio:

private void button1_Click(object sender, RoutedEventArgs e)

//creo il wrapper per l’operazione asyncrona, controlla se il context esiste o ne
//imposta uno di default che usa il ThreadPool .NET
AsyncOperation operation = AsyncOperationManager.CreateOperation(null);                   

System.Threading.ThreadPool.QueueUserWorkItem(a =>
               {
                   System.Threading.Thread.Sleep(5000);//simulo il carico di lavoro
                  

                   //Faccio il post dell’operazione sul contesto giusto e notifico il
                   //context sottostante
                   operation.PostOperationCompleted(o =>
                   {
                       this.button1.Content = o;
                   }, "Nuova Content del bottone");                                      
               });
}

Il metodo PostOperationComplete notifica in modo automatico il Context sottostante che l’operazione è terminata, in caso il context in questione avesse bisogno di saperlo per motivi architetturali(altrimenti avremmo dovuto chiamare prima il metodo Post() e poi il metodo OperationCompleted()). L’esempio non è esaustivo, ma da l’idea del senso per cui è stato creato per approfondimenti cercate nella guida o sul web, lo scopo di questo blog non è l’AsyncOperationManager.

Fortunatamente anche con le Task Parallel Library abbiamo la possibilità di sfruttare questo meccanismo in modo veramente semplice…il codice è più chiaro della spiegazione:

private void button1_Click(object sender, RoutedEventArgs e)
       {
              Task.Factory.StartNew(() =>
               {
                   Thread.Sleep(5000);//Simulo il carico
                   return "Nuova Content del bottone";
               })
               .ContinueWith(taskPrecedente =>
                   {
                       this.button1.Content = taskPrecedente.Result;
                   }, TaskScheduler.FromCurrentSynchronizationContext()
                   );

       }

Il metodo ContinueWith accetta come parametro tra i suoi overload un’oggetto di tipo TaskScheduler il quale ha un metodo statico FromCurrentSynchronizationContext che si occupa di creare un TaskScheduler del giusto tipo per il contesto in cui siamo.
Questo significa che l’operazione pesante(StartNew()) viene fatta in un thread separato e l’assegnazione del risultato(ContinueWith()) al nostro controllo viene “postato” sul thread di UI, grazie al fatto che abbiamo indicato uno scheduler conscio del fatto che siamo in un contesto di funzionamento per quanto riguarda il threading “particolare”(sotto le coperte utilizza il SyncronizationContext).

Non so cosa ne pensate, ma la sintassi per scrivere codice asincrono lato client con le TPL(task parallel library) è un bel passo avanti e alla portata di tutti…buon divertimento.

Vota questo post per primo

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , ,

Multithreading | .Net

Disclaimer
Le opinioni espresse in questo blog sono mie opinioni personali.

© Copyright 2010 Knowledge.CreateAsync()