Articoli con tag Multithread
Multithreading with WPF
Voglio qui riassumere due concetti “base” (nel senso che non li approfondisco molto) riguardo il multithreading con wpf. In particolare due concetti:
- Eseguire componenti UI su thread diversi
- Aggiornare componenti di UI da thread diversi
Per far questo ho scritto una semplice finestra con il seguente contenuto:
<Button Content="Start another Window with same UI Thread" Margin="5,5,5,2" Click="btnSameThread_Click" /> <Button Content="Start another Window with different UI Thread" Margin="5,2,5,2" Click="btnNewThread_Click" /> <Button Content="Perform Long Operation" Margin="5,10,5,10" Click="btnLongOperation_Click" /> <Button Content="Change Textbox of last window opened" Margin="5,5,5,2" Click="btnChangeTextLastWindow_Click" /> <Button Content="Change Textbox of last window opened (secured)" Margin="5,2,5,5" Click="btnChangeTextLastWindowSecured_Click" /> <TextBox Text="Try change this text" x:FieldModifier="public" x:Name="ChangeTextbox" />
Da notare l’ultima textbox con x:FieldModifier=”public”; in questo modo la textbox è accessibile anche dall’esterno della classe (in questo caso non serve, ma l’esempio deriva da un caso in cui il thread era su una classe esterna).
Ecco il codice:
MainWindow lastWindowOpened = null;private void btnSameThread_Click(object sender, RoutedEventArgs e){ lastWindowOpened = new MainWindow(); //show window with same Thread lastWindowOpened.Show(); } private void btnNewThread_Click(object sender, RoutedEventArgs e) { Thread t = new Thread(new ThreadStart(() => { lastWindowOpened = new MainWindow(); //the window is created in a different thread lastWindowOpened.Show(); //show window lastWindowOpened.Closed += (s, ec) => System.Windows.Threading.Dispatcher.CurrentDispatcher.InvokeShutdown(); System.Windows.Threading.Dispatcher.Run(); //start message loop! })); t.SetApartmentState(ApartmentState.STA);//interoperability with com… t.Start(); } private void btnLongOperation_Click(object sender, RoutedEventArgs e) { Thread.Sleep(5000); } private void btnChangeTextLastWindow_Click(object sender, RoutedEventArgs e) { //not safe for UI running on different thread lastWindowOpened.ChangeTextbox.Text = "Cambio da altra window"; } private void btnChangeTextLastWindowSecured_Click(object sender, RoutedEventArgs e) { lastWindowOpened.Dispatcher.BeginInvoke(new Action(() => {//if closed, if not null… lastWindowOpened.ChangeTextbox.Text = "Cambio da altra window"; })); }
Ora occorre capire due concetti:
- btnNewThread esegue un thread in cui mostra una nuova finestra. Il thread tuttavia non ascolta gli eventi della UI finché non si esegue Dispatcher.Run() (http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.run.aspx). Eseguendo il metodo statico, il thread corrente entra nel loop dei messaggi finché non si esegue InvokeShutdown(). Cosa succede se non si chiama InvokeShutdown()? La finestra viene chiusa, ma il thread continua ad ascoltare il loop dei messaggi per sempre. Quindi il thread non termina e l’applicazione resta appesa. TaskManager e killiamo
- btnChangeTextLastWindowSecured modifica il contenuto della textbox dell’ultima window aperta. In questo caso si “inietta” un delgate al thread che “possiede” l’elemento di UI. In qusto modo l’operazione viene eseguita dal thread affine alla UI senza creare problemi.
Ora il momento dei test:
- lanciamo la finestra, premiamo il primo bottone per lanciare una nuova window. Ora nella prima premiamo btnLongOperation e muoviamo le finestre…
Non si muovono! questo perchè le due finestre sono gestite dallo stesso thread che viene posto in sleep(). Corretto. - lanciamo la finestra, premiamo il secondo bottone e poi btnLongOperation su una finestra e muoviamo l’altra…
Si muove! questo perchè le due finestre sono su due thread diversi e se si pone un thread in sleep mode, quella finestra è bloccata, ma l’altra no ed è libera di leggere i messaggi dal loop e quindi muovere la window. - riprendiamo il secondo punto. Lanciamo due window su thread diversi e premiamo il 4° button. L’applicazione crasha. Questo perchè si accede ad un elemento di UI appartenente ad un altro thread. L’ultimo button è necessario.
- Ultimo punto, un po’ più complicato…
- Lanciamo una window
- Lanciamo la seconda window su un thread diverso con il secondo button
- sulla seconda window eseguiamo btnLongOperation. La seconda window è quindi bloccata e non si può muovere.
- Ora nella prima window premiamo l’ultimo button… cosa succede?
Niente. Il thread della prima window continua il loop dei messagge ed è libera. La seconda finestra è bloccata. Il testo non cambia fino a che il secondo thread non esce dallo sleep. - Cosa succede se invece di BeginInvoke mettiamo Invoke? In questo caso anche il primo thread si blocca perchè l’invoke è sincrono e attende l’esecuzione dell’altro thread.
Liberi di eseguire altre prove (e postare esempi interessanti)
WPF DataGrid Clipboard BUG(?)&Workaround
Il mese scorso mi sono imbattuto in questo strano problema: La mia applicazione crashava su un particolare computer (che, per curiosità, aveva WinXP SP3 ma non credo sia questo il problema). L’utente sostiene che l’applicazione crasha anche stando a guardarla… molto strano.
0) Scoperta
L’eccezione non gestita, ricavata sia dal log applicativo (catturata nell’evento Application.DispatcherUnhandledException) sia da Event Viewer, era la seguente:
OpenClipboard Failed (Exception from HRESULT: 0x800401D0 (CLIPBRD_E_CANT_OPEN))
Dallo stack trace si evinceva che l’eccezione veniva lanciata dalla DataGrid di WPF4. Bene… andiamo in dettaglio.
1) Teoria della clipboard (fonte: Clipboard on MSDN Library)
Come è ovvio, la clipboard è un elemento di sistema a cui possono accedere più applicazioni contemporaneamente. A livello di API, ogni applicazione esegue, in genere, i seguenti passaggi:
– OpenClipboard()
– clear/read/set data
– CloseClipboard()
OpenClipboard() ritorna true se l’applicativo riesce a avere possesso della clipboard. In questo momento solamente lui è in grado di accedervi. Nessun’altra applicazione può accedervi (sia in scrittura che in lettura) prima che essa venga rilasciata con CloseClipboard().
2) Riproduzione del problema:
Create due applicazioni. La prima, un semplice Console Application, che esegue il seguente codice:
[DllImport("user32.dll")]
extern static bool OpenClipboard(IntPtr hWnd);
if (OpenClipboard(IntPtr.Zero))//lock clipboard
Thread.Sleep(60000);//così ho tempo per lanciare l’altra applicazione.
La seconda, WPF application, con un textbox e una DataGrid popolata e con elementi ClipboardContentBinding. Ora lanciate (non da VS) la console application, debuggate l’app WPF.
Premete CTRL+C sulla textbox. Blocco di pochi istanti ma niente. In questo caso il fatto che la clipboard sia lockata è correttamente gestita dalla textbox.
Premete CTRL+C selezionando qualcosa nella griglia… Bam! Ecco l’eccezione.
3) Soluzione:
Le strade sono due. La prima è scrivere una classe che deriva da DataGrid e, nell’override del metodo OnExecutedCopy() gestisce l’eccezione (credo si possa fare, anche se non ne sono troppo sicuro). Il secondo è gestire l’eccezione direttamente in Application.DispatcherUnhandledException. Poiché la DataGrid l’avevo usata in più punti nell’applicativo, non sapevo se questo era un caso isolato o se da altre parti sorgesse lo stesso problema e #soprattutto# dato i tempi stretti, ho optato per la seconda con questo codice
var comEx = e.Exception as System.Runtime.InteropServices.COMException; if (comEx != null) { switch (comEx.ErrorCode) { case -2147221040: //OpenClipboard Failed (Exception from HRESULT: 0x800401D0 (CLIPBRD_E_CANT_OPEN)) try { //Try get info about the application which lock the clipboard IntPtr handle = GetOpenClipboardWindow(); if (handle != IntPtr.Zero) { int capacity = GetWindowTextLength(new HandleRef(this, handle)) * 2; StringBuilder stringBuilder = new StringBuilder(capacity); GetWindowText(new HandleRef(this, handle), stringBuilder, stringBuilder.Capacity); Trace.TraceWarning(string.Format("CLIPBOARD LOCKED BY {0}!", stringBuilder.ToString())); } } finally { e.Handled = true; //Handle Exception. No error to user. No crash of the application. } return; } } Trace.TraceError(e.Exception.Message);
Questo è tutto.
4) Riflessioni:
Chissà quale programma causava il problema… ve lo dirò quando mi farò dare il log (se mai l’avrò)
5) Considerazioni:
Se qualcuno volesse pingare il team di sviluppo per spiegare il problema faccia pure, oppure se volesse spiegarmi come fare contattatemi.