Sfide che affronterai nella programmazione multithreading:
Le seguenti quattro differenze fondamentali distinguono le applicazioni multithread tra cui ArcGIS Pro, da una classica applicazione a singolo thread singolo:
- Per garantire un’esperienza d’uso (UX) responsiva, il thread dell'interfaccia grafica utente (GUI) deve essere in grado di ricevere l'input da parte dell'utente e produrre output senza intoppi e senza interruzioni. Ciò significa che le esecuzioni delle azioni programmate devono essere eseguite in modo asincrono su thread di lavoro separati; il thread della GUI non dovrebbe mai svolgere un lavoro di qualsiasi tipo che blocchi nell’attesa. Ciò è in contrasto con le esistenti applicazioni ArcGIS desktop dove la maggior parte del lavoro viene eseguito direttamente sul singolo thread della GUI.
- Mentre il lavoro è in esecuzione sui thread in background, agli utenti deve essere presentata un’interfaccia utente logicamente coerente e informativa. Comandi, strumenti, e altre parti dell'interfaccia utente devono essere attivate o disattivate opportunamente basandosi su quali operazioni sono in esecuzione, e dovrebbe essere fornito un adeguato feedback. Se un'operazione di lunga esecuzione è logicamente annullabile, dovrebbe essere fornita la possibilità di annullarla.
- Operazioni che possono generare conflitto non dovrebbero essere eseguite simultaneamente e devono sempre essere eseguite in una sequenza logica appropriata. Ad esempio, le operazioni su una mappa non possono essere eseguite mentre il progetto che contiene la mappa è ancora in fase di caricamento ed un insieme di feature selezionate non può essere cancellato finché la selezione stessa non è stata completata. La maggior parte delle operazioni avviate tramite l'interazione dell'utente sono logicamente dipendenti dall’ordine e dovrebbero essere eseguite in serie.
- Bisogna fare attenzione a garantire che l'accesso allo stato volatile, cioè l'accesso alle variabili all'interno del programma, sia correttamente sincronizzato quando tale stato è condiviso tra i thread. Ad esempio, se un oggetto collection è condiviso tra un thread di lavoro e il thread della GUI, entrambi i thread devono essere coordinati per l’accesso alla collection in modo che un thread non legga gli elementi della collection, mentre un altro thread simultaneamente sta aggiungendo o rimuovendo elementi. Questo tipo di protezione è comune a tutti i tipi di programmazione multithreaded e viene normalmente realizzato utilizzando un lock. In un'applicazione in cui più parti indipendenti possono estendere il comportamento dell'applicazione, le operazioni di coordinamento possono diventare complesse e fuori controllo senza un comune framework che orchestra i vari componenti a lavorare insieme.
Il modello di threading interno ad ArcGIS Pro
Gli ingegneri di Esri hanno posto la massima priorità per rendere ArcGIS Pro facile da programmare il più possibile nella nuova architettura multithread. A tal fine, ArcGIS Pro incorpora le più recenti funzionalità della programmazione asincrona fornita da Microsoft insieme a nuove infrastrutture threading specifiche dell'applicazione su misura per ridurre la complessità del codice.Nella maggior parte dei casi, gli sviluppatori di add-in dovrebbero solo aver bisogno di trattare con due thread: il thread dell'interfaccia utente e un thread di lavoro specializzato forniti dall'applicazione.
Internamente, ArcGIS Pro utilizza un gran numero di thread per vari scopi, tra cui la rasterizzazione, il rendering dei grafici, il caricamento dei dati e la selezione degli algoritmi di geoprocessing che sfruttano il parallelismo per velocizzare il calcolo. Mantenere tutte queste attività senza blocchi e senza conflitti richiede una notevole attività di coordinamento e di complessità associata; per questo motivo, questi thread sono completamente interni e isolati agli sviluppatori nell'ambito delle implementazioni con l’SDK pubblico. Quando viene chiamato un metodo nella API pubblica, l'implementazione interna può, quando applicabile, dividere l’operazione e delegare parti ad uno o più di questi thread interni specializzati, o accodare le operazioni che alla fine vengono eseguite all'interno di un processo esterno o un servizio web .
Attività (Tasks) e il modello asincrono basato su attività (TAP)
I metodi all'interno ArcGIS Pro SDK rientrano in tre categorie:- Metodi asincroni che possono essere chiamati in qualsiasi thread. Metodi di questo tipo sono nominati utilizzando il suffisso Async e di solito restituiscono Tasks. In alcuni casi, può essere fornita sia la versione sincrona che asincrona del metodo.
- Metodi sincroni che dovrebbero essere chiamati solo sul thread di lavoro. Metodi di questo tipo sono segnalati nella guida in linea delle API e un tip sul codice appariranno quando siamo col cursore sul metodo.
- Metodi sincroni che dovrebbero essere chiamati solo sul thread della GUI. Questi tipi di metodi sono di solito associati con WPF.
Se un metodo su un particolare oggetto viene chiamato sul thread sbagliato, la chiamata genera un'eccezione di tipo ArcGIS.Core.CalledOnWrongThreadException. Se non sei sicuro su un particolare caso, è possibile consultare la guida del componente SDK o l’help fornito da Microsoft per determinare se un particolare metodo o una proprietà ha delle restrizioni.
All'interno dell’SDK - in particolare all'interno del namespace ArcGIS.Core – il thread di lavoro associa metodi e proprietà che tendono ad essere a grana fine. Per ridurre il tempo associato con la schedulazione e la commutazione di contesto, questi metodi sono sincroni e devono essere utilizzati nel codice usando task.
Il .NET Task Parallel Library di Microsoft (TPL) e il modello di programmazione associata noto come il modello asincrono basato su attività (TAP) semplificano la creazione di codice asincrono all'interno di un'applicazione multithread. La classe Task viene utilizzata per rappresentare un'operazione eseguita in modo asincrono.
Nel seguente esempio, il metodo PrintReportAsync viene richiamato e restituisce immediatamente un oggetto Task al chiamante. Nel frattempo, la funzione di stampa continua l’esecuzione in background su un altro thread.
private void Button_Click(object sender, RoutedEventArgs e)
{
Task t = PrintReportAsync ("HP1");
// Attendo che il task abbia finito.
t.Wait ();
MessageBox.Show ("Il Report è pronto!");
}
Questo esempio vuole mostrare un messaggio quando la stampa è completata e utilizza il metodo Wait sull'oggetto Task restituito per sospendere il thread chiamante fino a quando il task ha completato il suo compito.
Questo approccio presenta due grandi inconvenienti: in primo luogo il thread chiamante non può fare altro mentre è in attesa; in realtà è meno efficiente che semplicemente chiamare una versione sincrona della funzione di stampa. In secondo luogo, poiché il thread chiamante è un thread della GUI, l'interfaccia utente risulta ‘congelata’. Un thread sospeso, ovviamente, non è in grado di elaborare l'input dell'utente, aggiornare elementi grafici o fare qualsiasi altra cosa. Per queste ragioni, non si dovrebbe mai usare il metodo Wait su un thread della GUI.
Fortunatamente, .NET introduce le funzionalità al linguaggio async e await. Il modificatore async segna il metodo in modo che il compilatore sappia che il metodo è asincrono e usi l'operatore await. L'operatore await è molto utile in quanto è utilizzato per chiamare i metodi in modo asincrono e dopo per forzare il thread chiamante a tornare automaticamente alla riga successiva e continuare l'esecuzione una volta che l'operazione asincrona è stata completata. Il thread chiamante - normalmente il thread della GUI - non è bloccato ed è libero di adottare altre azioni mentre il task sul thread di lavoro è ancora in esecuzione.
Si noti che possiamo modificare ora l'obiettivo originale con poche variazioni, ma in questo caso l'interfaccia utente non si blocca.
private async void Button_Click(object sender, RoutedEventArgs e)
{
Task t = PrintReportAsync ("HP1");
// Attendere (senza blocco) fino a quando il task è completato.
await t;
// riparte da qui quando il task è completato.
MessageBox.Show ("Il report è pronto!");
}
L’utilizzo di Run
Quando una funzione asincrona non è disponibile, è possibile scrivere facilmente le proprie funzioni wrapper che eseguono internamente uno o più metodi sincroni. L'esempio seguente utilizza il metodo statico Run per accodare l'esecuzione della funzione WorkFunc ad una thread casuale nel pool di thread dei Task. Si noti che il metodo click restituisce immediatamente il controllo al chiamante, mentre il WorkFunc continua a eseguire sul thread di lavoro.private void Button_Click(object sender, RoutedEventArgs e)
{
Task t = Task.Run ((Azione) WorkFunc);
}
private void WorkFunc ()
{
// my work
}
Invece di utilizzare una funzione separata, può essere impiegata una funzione anonima chiamata anche lambda. Utilizzando le lambda, mantieniamo il codice della funzione di lavoro nella funzione stessa consentendoci di utilizzare gli argomenti e le variabili locali all'interno della lambda come se fossero parte della funzione stessa.
private void Button_Click(object sender, RoutedEventArgs e)
{
int steps = GetSteps();
Task t = Task.Run (() =>
{
// Posso utilizzare la variabile steps qui anche se mi trovo in una
// diversa funzione in esecuzione su un diverso thread!
// my work
});
}
I Task possono anche essere parametrizzati per restituire un tipo particolare, come risultato di un calcolo della lambda.
Task<double> t = Task.Run<double>(() =>
{
double risultato;
// Calcolo della variabile risultato …
return risultato;
});
L'operatore await può essere utilizzato anche in linea per ottenere il risultato della funzione asincrona e senza doverlo estrarre dal Task restituito.
private async void Button_Click(object sender, RoutedEventArgs e)
{
double computedValue = await Task.Run <double>(() =>
{
double risultato;
// Calcolo della variabile risultato ...
return risultato;
});
// L’esecuzione riprende automaticamente qui quando il task sopra ha completato!
MessageBox.Show (String.Format ("Il risultato era {0}", computedValue.ToString ()));
}
C'è un piccolo ‘carico’ associato ad await, quindi è sempre più efficiente chiamare più metodi sincroni all'interno della propria lambda che chiamare molte funzioni asincrone utilizzando await. Questo è particolarmente vero quando si scrive codice in un loop, dove il costo di utilizzo await attraverso centinaia o migliaia di iterazioni diventa sostanziale.
L’utilizzo di QueuedTask
Mentre i Task sono un appuntamento fisso all'interno di qualsiasi codice dell’add-in, i task devono essere forniti in ArcGIS Pro in modo diverso dal tradizionale TAP. Il framework fornisce una schedulazione personalizzata del Task che dovrebbe essere utilizzato quando fornisci Task che effettuano chiamate ai metodi sincroni di ArcGIS Pro SDK. Invece di chiamare Task.Run, gli sviluppatori di add-in devono chiamare QueuedTask.Run.Task t = QueuedTask.Run (() =>
{
// Chiama metodi SDK sincroni
});
La classe QueuedTask viene utilizzata al posto della classe Task per i seguenti motivi:
Controllo della concorrenza e delle code
Quando i Task vengono inviati utilizzando Task.Run, il Task associato sarà in esecuzione su un thread a caso nel pool di thread gestito ogni volta che viene chiamato. Se una chiamata successiva a Task.Run viene fatta da qualsiasi altra parte dell'applicazione, il nuovo Task inizierà a funzionare immediatamente su un altro thread mentre il primo Task è ancora in esecuzione sul primo thread. Tornando alla lista delle sfide da affrontare nel codice multithread, dovrebbe essere ovvio che l'esecuzione delle operazioni non organizzate e concorrenti rischia di far andare in crash l’applicazione o di corrompere lo stato dell'applicazione. Il comportamento di accodamento QueuedTask.Run garantisce il corretto ordine delle chiamate e riduce il rischio di conflitti. Ricordate che il parallelismo all'interno di ArcGIS Pro si realizza internamente; questo semplifica il modello di programmazione pubblica e riduce notevolmente la probabilità di conflitti.Affinità e stato
Per motivi di prestazioni, ArcGIS Pro mantiene un notevole stato su specifici thread e, in molti casi, utilizza oggetti che hanno affinità di thread. Affinità di thread significa che un oggetto è legato ad un particolare thread e non deve interagire con qualsiasi thread ma con il thread col quale ha affinità. Vincoli di affinità sono comuni in sistemi operativi e componenti, tra cui connessioni a database, windows, controlli, code di input, timer, WPF Bitmap e server COM. In WPF, per esempio, chiamate a metodi su un qualsiasi oggetto derivato dalla classe WPF DependencyObject si tradurrà in un'eccezione se la chiamata viene effettuata da un thread da dove l'oggetto non è stato creato.Thread nel pool di thread gestiti sono anche incompatibili con la maggior parte dei componenti COM, per cui non si dovrebbe tentare di utilizzare Task.Run con codice che potrebbe eseguire direttamente o indirettamente, componenti COM.
Integrazione dell’applicazione
Quando i Task vengono eseguiti utilizzando QueuedTask.Run, essi vengono integrati automaticamente con varie funzionalità all'interno dell'applicazione come segue:- L’estensione della Progress/Cancellation del framework, in cui la progress include la finestra di dialogo con lo stato di avanzamento programmabile, viene visualizzata e nascosta automaticamente e lo stato cancellazione è correttamente comunicato tra le parti interessate dell’applicazione.
- Lo stato occupato del sistema dell’applicazione dove gli elementi dell'interfaccia utente come pulsanti e strumenti sono attivati e disattivati automaticamente quando i Task sono in esecuzione. Esecuzione dei task possono essere coordinati anche con fasi critiche quali la creazione di viste e chiusura dell'applicazione.
- La coda dei Task è scritta nelle strutture di diagnostica del framework, quando abilitate. Questo consente agli sviluppatori di monitorare la sequenza di esecuzione dei task, i task in esecuzione e la durata dell'esecuzione. Questo tipo di informazione è preziosa per il debugging e l'analisi delle prestazioni.
Casi in cui è accettabile l'utilizzo di Task.Run
Ci sono casi in cui l'uso di Task.Run è accettabile, ad esempio quando si eseguono operazioni in background indipendenti costituite interamente da codice in modo gestito a condizione che i particolari componenti gestiti in uso non abbiano affinità di thread. Lo sviluppatore si assume la piena responsabilità della gestione della cancellazione, della visualizzazione dello stato di avanzamento, dell'abilitazione/disabilitazione dell'interfaccia utente in modo appropriato e del coordinamento delle operazioni e della gestione della logica dei conflitti.Progress e cancellation
I metodi asincroni possono talvolta accettare un argomento Progressor, un oggetto che viene utilizzato dal chiamante per configurare le impostazioni della finestra di dialogo per lo stato di avanzamento e annullamento e di coordinare la comunicazione tra il chiamante e il chiamato. I metodi asincroni che non sono annullabili prendono una classe Progressor, mentre i metodi annullabili prendono una classe CancelableProgressor.Gli oggetti Progressor seguono il modello stabilito dal CancelationToken di Microsoft e non possono essere creati direttamente; invece, lo sviluppatore deve creare un ProgressorSource o CancelableProgressorSource.
Gli oggetti "source" consentono di configurare come il progressor gestirà il progress senza esporre queste impostazioni al codice esterno, che potrebbero accedere al Progressor. L'oggetto ProgressorSource espone i seguenti costruttori:
public ProgressorSource (Action<Progressor> callback)
public ProgressorSource (ProgressDialog progDlg)
public ProgressorSource (string message, bool delayedShow = false)
Il primo override prende un delegato che sarà chiamato, a intervalli regolari, mentre il Task è in esecuzione. Questa opzione è appropriata quando si desidera fornire un feedback specializzato durante l'esecuzione del Task.
Il secondo override prende una finestra dialogo di avanzamento, oggetto costruito separatamente. Se non è già visualizzato, il progressor mostrerà automaticamente questa finestra di dialogo di avanzamento quando il Task inizia l’esecuzione e si nasconderà automaticamente al completamento del Task. Se la finestra di dialogo è già visibile, il progressor aggiornerà il contenuto della finestra di dialogo durante l'esecuzione e sarà compito dello sviluppatore nascondere la finestra di dialogo di avanzamento al momento opportuno. Questa opzione è appropriata quando si desidera controllare manualmente la visibilità della finestra dello stato di avanzamento, ad esempio quando è necessario mantenere la finestra dello stato di avanzamento tra diverse attività separate.
Il terzo override creerà automaticamente e mostrerà una finestra di dialogo di avanzamento quando il Task inizia l'esecuzione e lo nasconde al termine del completamento del Task. Il parametro delayedShow controlla se la finestra di dialogo di avanzamento deve essere mostrata immediatamente o ritardare la sua apparizione per consentire operazioni rapide da completare ed evitare di apparire, se non necessario. Se si prevede che il Task sia rapido nel completamento dell’esecuzione, impostare questo parametro a true. Se si prevede che il Task abbia bisogno di più di uno o due secondi per completare l’operazione, impostare delayedShow a false in modo che la finestra di dialogo di avanzamento appaia immediatamente per trasmettere responsività.
CancelableProgressors richiedono un ulteriore argomento che specifica cosa dovrebbe visualizzare il messaggio di annullamento. Verrà visualizzato il messaggio di annullamento, non appena l'utente fa clic sul pulsante Annulla nella finestra di dialogo.
public CancelableProgressorSource(Action <CancelableProgressor> callback)
public CancelableProgressorSource(ProgressDialog progDlg)
public CancelableProgressorSource(string message, string cancelMessage, bool delayedShow = false)
Esempio di implementazione del metodo utilizzando cancellation
Lo specializzato CancelableProgressor espone una proprietà CancellationToken che può essere utilizzata per comunicare l’annullamento. All'interno dell'implementazione del metodo, il codice in esecuzione nel loop dovrebbe controllare la proprietà IsCancellationRequested e uscire dal metodo gettando l’eccezione OperationCanceledException (che riconosce la richiesta di cancellazione) come illustrato di seguito:public Task<long> CalcFactorialAsync(int x, CancelableProgressor progressor)
{
return QueuedTask.Run<long>(() =>
{
long result = 1;
for (int i = 1; i < x; ++i)
{
if (progressor.CancellationToken.IsCancellationRequested)
throw new OperationCanceledException();
result *= i;
}
return result;
});
}
Utilizzare con i metodi asincroni la finestra di dialogo integrata
Se il Progressor è stato configurato per mostrare lo stato di avanzamento, il Task in esecuzione può aggiornare le informazioni visualizzate nella finestra di avanzamento utilizzando il progressor (entrambe Progressor e CancelableProgressor supportano finestre di dialogo di avanzamento):public Task<long> CalcFactorialAsync(int x, Progressor progressor)
{
return QueuedTask.Run<long>(() =>
{
long result = 1;
for (int i = 1; i < x; ++i)
{
progressor.Message = string.Format("Working on step:{0}", i);
result *= i;
}
return result;
}, progressor);
}
Complicazioni comuni
Ipotesi di stato costante
Si consideri il seguente esempio. Questa chiamata viene richiamata dal thread della GUI e l'intento è quello di eliminare lo specifico layer dalla mappa della vista attiva.private Task DeleteSelectedLayerAsync(Layer layer)
{
return QueuedTask.Run(() =>
{
MapView.Active.Map.RemoveLayer(layer);
});
}
Anche se semplice in apparenza, questa funzione a volte può causare un'eccezione quando in uso all'interno dell'applicazione. L'errore qui è stato quello di pensare che lo stato del sistema rimanga statico attraverso i thread. Ci possono essere precedentemente delle operazioni in coda in esecuzione e queste devono essere completate prima che un'altra operazione possa iniziare l'esecuzione. Durante questo tempo, lo stato dell’applicazione può cambiare a causa dell’interazione con l'utente o il risultato di una operazioni ancora in esecuzione. In questo caso, l’oggetto attivo potrebbe diventare una tabella prima che la lambda effettivamente inizi l’esecuzione, nel qual caso la mappa sarà nulla determinando un'eccezione. L'approccio sicuro è quello di evitare "concatenamenti" di chiamate su variabili membro o variabili passate tra thread; utilizzare variabili locali come istantanee dello stato dell'applicazione quando il metodo è stato chiamato, dal momento che non cambieranno al loro interno.
private Task DeleteSelectedLayerAsync (Layer layer)
{
// Prendere una "istantanea" della mappa sulla vista attiva.
Map m = MapView.Active.Map;
return QueuedTask.Run (() =>
{
m.RemoveLayer(layer);
});
}
In un ambiente multithreading si dovrebbe gestire il codice allestendo una strategia difensiva. Si consideri un Task che modifica come un particolare layer debba essere simboleggiato. Se tale Task finisce in una coda dietro un altro Task che rimuove questo stesso layer dalla mappa, la seconda operazione è logicamente invalidata dalla prima. Per gestire questo caso correttamente, il secondo Task dovrebbe essere gestito per visualizzare un avviso o annullare l'operazione in modalità silente quando viene a sapere che il layer è stato eliminato.
Thread safe con associazione dati in WPF
Per impostazione predefinita, i dati di collection associati a WPF devono essere modificati sul thread dove è stata creata l’associazione al controllo WPF. Questa limitazione diventa un problema quando si desidera riempire la collection da un thread di lavoro per produrre una buona esperienza per l’utente. Ad esempio, un elenco dei risultati di ricerca che viene riempito gradualmente man mano che vengono trovate corrispondenze, senza costringere l'utente ad attendere fino a quando l'intera ricerca è completata.Per ovviare a questa limitazione, WPF fornisce una classe statica BindingOperations che permette di stabilire un'associazione tra un lock e una collection (ad esempio, ObservableCollection<T>). Tale associazione consente a collection associate di essere aggiornate dai thread esterni al thread principale della GUI, in modo coordinato senza generare la consueta eccezione.
BindingOperations.EnableCollectionSynchronization(Items, _lockObj);
Nell'esempio sopra, la variabile _lockObj - di tipo oggetto - è in genere istanziata quando viene creata la classe contenitore e servirà come lock per il coordinamento. Una volta che si chiama EnableCollectionSynchronization, WPF entrerà nello specificato lock quando c’è lettura o scrittura alla collection associata. Come proprietari della collection, si è obbligati a inserire il lock durante la lettura alla collection o scrittura della collection.
I wrapper ReadOnlyObservableCollection sono comunemente usati per forzare le sole letture semantiche sulle proprietà della collection osservabile. Per impostare correttamente la sincronizzazione con multithreading, è necessario chiamare EnableCollectionSynchronization sul wrapper, invece che sulla collection stessa, dal momento che è il wrapper al quale il WPF sarà associato effettivamente.
internal class HelloWorld
{
private ObservableCollection<string> _items = new ObservableCollection<string>();
private ReadOnlyObservableCollection<string> _itemsRO;
private Object _lockObj = new Object();
internal HelloWorld()
{
_itemsRO = new ReadOnlyObservableCollection<string>(_items);
BindingOperations.EnableCollectionSynchronization(_itemsRO, _lockObj);
}
//la proprietà pubblica utilizzata per il binding
public ReadOnlyObservableCollection<string> Items { get { return _itemsRO; } }
//all’interno della funzione di lavoro, il lock è inserito prima di modificare //la collection:
public void FillCollectionAsync()
{
QueuedTask.Run(() =>
{
// letture e scritture dovrebbe essere fatte all’interno del lock
lock (_lockObj)
{
_items.Add( GetData() );
}
});
}
Oggetti "Live" come proprietà
Si deve prestare attenzione quando si espongono oggetti – soprattutto collection - come proprietà pubbliche se la collezione è destinata a cambiare in un thread separato. Se qualcuno ottiene e trattiene tale proprietà e poi inizia l'enumerazione attraverso un thread A, un'eccezione può essere generata se il proprio codice modifica la collection sul thread B in quanto non vi è alcun lock di collaborazione tra essi. Distribuire in sola lettura istantanee della collection è più sicuro.Eseguire codice sul thread della GUI
Ci sono casi in cui di tanto in tanto, mentre il codice viene eseguito su un thread di lavoro, si incontrano situazioni in cui è necessario chiedere input da parte dell'utente prima di procedere. Non si dovrebbe cercare di presentare una finestra direttamente dal thread di lavoro poiché le finestre hanno affinità di thread. Una finestra di dialogo creata sul thread di lavoro non si collega alla coda di input del thread della GUI e non rispetta lo z-order e la politica di focus stabilita dal thread GUI. In generale, è possibile eseguire codice sul thread della GUI da un thread di lavoro usando l'oggetto dispatcher dell'applicazione.Questo può essere fatto in modo sincrono.
FrameworkApplication.Current.Dispatcher.Invoke (() =>
{
// Fare qualcosa sul thread della GUI
System.Windows.MessageBox.Show ("Pronto!");
});
O in modo asincrono:
FrameworkApplication.Current.Dispatcher.BeginInvoke (() =>
{
// Fare qualcosa sul thread della GUI
System.Windows.MessageBox.Show ("Pronto!");
});
Si dovrebbe cercare di raccogliere le informazioni necessarie da parte dell'utente sul thread della GUI prima di eseguire il lavoro in modo da non dover utilizzare questa scorciatoia. Bloccando chiamate tra thread c’è rischio di deadlock e di trattenere operazioni in esecuzione sul thread di lavoro.
La gestione delle eccezioni in ambiente asincrono
Come le funzioni sincrone, le funzioni asincrone possono gettare eccezioni. Questo introduce un problema interessante da quando il chiamante fornisce il try/catch in un thread, e l'eccezione viene generata su un altro. Inoltre, il frame del chiamante non è solitamente ancora nello stack quando viene generata l'eccezione.Tuttavia, .NET permette di utilizzare async/await con try/catch in modo che, se viene generata un'eccezione dal codice in esecuzione all'interno del Task, sarete in grado di catturare di nuovo da dove la funzione asincrona è stata chiamata. Si noti che la funzione asincrona deve restituire Task o Task<T> per eccezioni asincrone per essere adeguatamente rediretta (non void).
try
{
var result = await PrintMapAsync ();
}
catch (Exception e)
{
// gestione eccezione.
}
Se viene generata un'eccezione da parte del worker e non è stato fornito un try/catch intorno a dove si attende il worker, il runtime .NET si collegherà all'eccezione come eccezione inner, UnobservedException.
Eccezioni Unobserved solitamente compaiono solo quando l'oggetto eccezione viene raccolto dalla garbage collection di .NET, in nessun posto vicino a dove in realtà si è verificata l'eccezione. Se si ottiene uno di questi, esaminare l'eccezione interna per ottenere lo stack delle chiamate fallite. Nella finestra di controllo di VisualStudio, è possibile utilizzare la pseudo variabile $exception per esaminare il corrente oggetto eccezione.
Oggetti Freezable
WPF definisce un modello in cui determinati tipi di oggetti possono essere "congelati". Una volta che l'oggetto è congelato, modifiche non possono essere apportate all'oggetto senza che si generi un'eccezione. Oggetti freezable possono migliorare le prestazioni in alcune situazioni, e permettono anche di condividere l'oggetto tra i thread (vedi affinità di thread). Ad esempio, se un BitmapImage viene creato su un thread di lavoro, non è possibile utilizzarlo in seguito sul thread della GUI a meno di congelarlo prima.Si consideri un caso comune in cui l'associazione venga utilizzata in combinazione con le immagini che sono state generate su un thread di lavoro. La classe di esempio VM qui sotto espone una proprietà chiamata Img:
public class VM: INotifyPropertyChanged
{
public BitmapImage Img {get {return _image; }}
...
}
Questa proprietà restituisce un'istanza di BitmapImage (un oggetto Freezable), che è poi associata ad un pulsante in XAML:
<Button>
<Image Source = "{Binding Img}"> </ Immagine>
</ Button>
La bitmap sottostante viene periodicamente aggiornata sul thread di lavoro come segue; notare che verrà creata la bitmap sul thread di lavoro:
public Task Refresh ()
{
return QueuedTask.Run (() =>
{
var uri = GenerateThumbnail();
Img = new BitmapImage (uri);
});
}
Nel processo di rendering dell'interfaccia utente, WPF tenterà di accedere alla proprietà bitmap dal thread della GUI ... ma questo si tradurrà in un'eccezione perché la bitmap è ancora "scongelata" e quindi ancorata al suo thread di lavoro principale. Questo problema può essere risolto semplicemente "congelando" la Bitmap dopo il suo l'aggiornamento.
var uri = GenerateThumbnail ();
Img = new BitmapImage (uri);
Img.Freeze ();
Nota: non tutte le classi che ereditano da System.Windows.Freezable possono essere congelate. Utilizzare la proprietà CanFreeze per verificare se l’oggetto può essere congelato.
Nessun commento:
Posta un commento