Prevenzione dei blocchi nelle applicazioni Windows
Piattaforme interessate
Client - Windows 7
Server - Windows Server 2008 R2
Descrizione
Blocchi - Prospettiva utente
Utenti come applicazioni reattive. Quando fanno clic su un menu, vogliono che l'applicazione reagisca immediatamente, anche se attualmente sta stampando il lavoro. Quando salvano un documento lungo nel loro elaboratore di testi preferito, vogliono continuare a digitare mentre il disco è ancora in rotazione. Gli utenti ottengono un'impazienza piuttosto rapida quando l'applicazione non reagisce tempestivamente all'input.
Un programmatore potrebbe riconoscere molti motivi legittimi per cui un'applicazione non risponde immediatamente all'input dell'utente. L'applicazione potrebbe essere occupata a ricalcolare alcuni dati o semplicemente in attesa del completamento dell'I/O del disco. Tuttavia, dalla ricerca degli utenti, sappiamo che gli utenti vengono infastiditi e frustrati dopo solo un paio di secondi di mancata risposta. Dopo 5 secondi, tenteranno di terminare un'applicazione bloccata. Accanto agli arresti anomali, l'applicazione si blocca è l'origine più comune dell'interruzione dell'utente quando si lavora con le applicazioni Win32.
Esistono molte cause radice diverse per i blocchi dell'applicazione e non tutti si manifestano in un'interfaccia utente che non risponde. Tuttavia, un'interfaccia utente non risponde è una delle esperienze di blocco più comuni e questo scenario attualmente riceve la maggior parte del supporto del sistema operativo sia per il rilevamento che per il ripristino. Windows rileva automaticamente, raccoglie informazioni di debug e, facoltativamente, termina o riavvia le applicazioni bloccate. In caso contrario, l'utente potrebbe dover riavviare il computer per ripristinare un'applicazione bloccata.
Hangs - Operating System Perspective
Quando un'applicazione (o più accuratamente, un thread) crea una finestra sul desktop, entra in un contratto implicito con Desktop Window Manager (DWM) per elaborare i messaggi della finestra in modo tempestivo. DWM inserisce messaggi (input da tastiera/mouse e messaggi da altre finestre, oltre a se stesso) nella coda di messaggi specifica del thread. Il thread recupera e invia tali messaggi tramite la relativa coda di messaggi. Se il thread non esegue il servizio della coda chiamando GetMessage(), i messaggi non vengono elaborati e la finestra si blocca: non può né ridisegnare né accettare l'input dell'utente. Il sistema operativo rileva questo stato collegando un timer ai messaggi in sospeso nella coda dei messaggi. Se un messaggio non è stato recuperato entro 5 secondi, DWM dichiara che la finestra deve essere bloccata. È possibile eseguire una query su questo particolare stato della finestra tramite l'API IsHungAppWindow().
Il rilevamento è solo il primo passaggio. A questo punto, l'utente non può ancora terminare l'applicazione. Se si fa clic sul pulsante X (Chiudi), verrà visualizzato un messaggio di WM_CLOSE che verrebbe bloccato nella coda dei messaggi esattamente come qualsiasi altro messaggio. Desktop Window Manager consente di nascondere facilmente e quindi sostituire la finestra bloccata con una copia "fantasma" che visualizza una bitmap dell'area client precedente della finestra originale (e aggiungendo "Non rispondere" alla barra del titolo). Purché il thread della finestra originale non recuperi messaggi, DWM gestisce entrambe le finestre contemporaneamente, ma consente all'utente di interagire solo con la copia fantasma. Usando questa finestra fantasma, l'utente può solo spostare, ridurre al minimo e, soprattutto, chiudere l'applicazione che non risponde, ma non modificare lo stato interno.
L'intera esperienza fantasma è simile alla seguente:
Desktop Window Manager esegue un'ultima operazione; si integra con Segnalazione errori Windows, consentendo all'utente di chiudere e, facoltativamente, riavviare l'applicazione, ma anche inviare di nuovo dati di debug importanti a Microsoft. Puoi ottenere questi dati di blocco per le tue applicazioni registrandoti nel sito Web Winqual.
Windows 7 ha aggiunto una nuova funzionalità a questa esperienza. Il sistema operativo analizza l'applicazione bloccata e, in determinate circostanze, offre all'utente la possibilità di annullare un'operazione di blocco e di rendere nuovamente reattiva l'applicazione. L'implementazione corrente supporta l'annullamento delle chiamate Socket di blocco; altre operazioni saranno annullabili dall'utente nelle versioni future.
Per integrare l'applicazione con l'esperienza di ripristino di blocco e per sfruttare al meglio i dati disponibili, seguire questa procedura:
- Assicurarsi che l'applicazione venga registrata per il riavvio e il ripristino, rendendo possibile un blocco senza dolore all'utente. Un'applicazione registrata correttamente può essere riavviata automaticamente con la maggior parte dei dati non salvati intatti. Questa operazione funziona sia per i blocchi dell'applicazione che per gli arresti anomali.
- Ottenere informazioni sulla frequenza, nonché eseguire il debug dei dati per le applicazioni bloccate e arrestate in modo anomalo dal sito Web Winqual. È possibile usare queste informazioni anche durante la versione beta per migliorare il codice. Per una breve panoramica, vedere "Introduzione Segnalazione errori Windows".
- È possibile disabilitare la funzionalità di ghosting nell'applicazione tramite una chiamata a DisableProcessWindowsGhosting (). Tuttavia, ciò impedisce all'utente medio di chiudere e riavviare un'applicazione bloccata e spesso termina con un riavvio.
Blocchi - Prospettiva sviluppatore
Il sistema operativo definisce un blocco dell'applicazione come thread dell'interfaccia utente che non ha elaborato messaggi per almeno 5 secondi. I bug evidenti causano alcuni blocchi, ad esempio un thread in attesa di un evento che non viene mai segnalato e due thread che contengono un blocco e tentano di acquisire gli altri. È possibile correggere questi bug senza troppo sforzo. Tuttavia, molti blocchi non sono così chiari. Sì, il thread dell'interfaccia utente non sta recuperando i messaggi, ma è altrettanto occupato durante l'esecuzione di altre operazioni "importanti" e alla fine tornerà all'elaborazione dei messaggi.
Tuttavia, l'utente percepisce questo come bug. La progettazione deve corrispondere alle aspettative dell'utente. Se la progettazione dell'applicazione porta a un'applicazione che non risponde, la progettazione dovrà cambiare. Infine, e questo è importante, la mancata risposta non può essere risolta come un bug di codice; richiede il lavoro iniziale durante la fase di progettazione. Il tentativo di adattare la codebase esistente di un'applicazione per rendere l'interfaccia utente più reattiva è spesso troppo costosa. Le linee guida di progettazione seguenti potrebbero essere utili.
- Rendere la velocità di risposta dell'interfaccia utente un requisito di primo livello; l'utente deve sempre avere il controllo dell'applicazione
- Assicurarsi che gli utenti possano annullare le operazioni che richiedono più di un secondo per completare e/o che le operazioni possano essere completate in background; fornire l'interfaccia utente di stato appropriata, se necessario
- Accodare operazioni a esecuzione prolungata o di blocco come attività in background (questo richiede un meccanismo di messaggistica ben pensato per informare il thread dell'interfaccia utente al termine del lavoro)
- Mantenere semplice il codice per i thread dell'interfaccia utente; rimuovere il maggior numero possibile di chiamate API di blocco
- Mostra finestre e finestre di dialogo solo quando sono pronte e completamente operative. Se la finestra di dialogo deve visualizzare informazioni che richiedono un utilizzo eccessivo delle risorse da calcolare, visualizzare prima alcune informazioni generica e aggiornarla in tempo reale quando diventano disponibili altri dati. Un buon esempio è la finestra di dialogo delle proprietà della cartella da Esplora risorse. Deve visualizzare le dimensioni totali della cartella, le informazioni che non sono facilmente disponibili dal file system. La finestra di dialogo viene visualizzata immediatamente e il campo "size" viene aggiornato da un thread di lavoro:
Sfortunatamente, non esiste un modo semplice per progettare e scrivere un'applicazione reattiva. Windows non offre un semplice framework asincrono che consente di pianificare facilmente operazioni di blocco o a esecuzione prolungata. Le sezioni seguenti illustrano alcune delle procedure consigliate per prevenire i blocchi ed evidenziare alcune delle insidie comuni.
Procedure consigliate
Mantenere semplice il thread dell'interfaccia utente
La responsabilità principale del thread dell'interfaccia utente è recuperare e inviare messaggi. Qualsiasi altro tipo di lavoro comporta il rischio di appendere le finestre di proprietà di questo thread.
Che cosa fare:
- Spostare algoritmi a elevato utilizzo di risorse o non associati che generano operazioni a esecuzione prolungata nei thread di lavoro
- Identificare il maggior numero possibile di chiamate di funzione bloccanti e provare a spostarle nei thread di lavoro; qualsiasi funzione che chiama in un'altra DLL deve essere sospetta
- Fare un ulteriore sforzo per rimuovere tutte le chiamate api di I/O e di rete del file dal thread di lavoro. Queste funzioni possono bloccarsi per molti secondi se non minuti. Se è necessario eseguire qualsiasi tipo di I/O nel thread dell'interfaccia utente, prendere in considerazione l'uso dell'I/O asincrono
- Tenere presente che il thread dell'interfaccia utente serve anche tutti i server COM a thread singolo (STA) ospitati dal processo; se si effettua una chiamata di blocco, questi server COM non risponderanno fino a quando non si esegue di nuovo la coda dei messaggi
Non:
- Attendere su qualsiasi oggetto kernel (ad esempio Event o Mutex) per più di un breve periodo di tempo; se è necessario attendere, prendere in considerazione l'uso di MsgWaitForMultipleObjects(), che sbloccherà quando arriva un nuovo messaggio
- Condividere la coda di messaggi della finestra di un thread con un altro thread usando la funzione AttachThreadInput(). Non è solo estremamente difficile sincronizzare correttamente l'accesso alla coda, ma può anche impedire al sistema operativo Windows di rilevare correttamente una finestra bloccata
- Usare TerminateThread() in uno dei thread di lavoro. La terminazione di un thread in questo modo non consentirà di rilasciare blocchi o eventi di segnale e può causare facilmente oggetti di sincronizzazione orfani
- Chiamare qualsiasi codice "sconosciuto" dal thread dell'interfaccia utente. Ciò vale soprattutto se l'applicazione ha un modello di estendibilità; non esiste alcuna garanzia che il codice di terze parti segua le linee guida sulla velocità di risposta
- Effettuare qualsiasi tipo di chiamata broadcast bloccante; SendMessage(HWND_BROADCAST) ti mette alla merda di ogni applicazione non scritta attualmente in esecuzione
Implementare modelli asincroni
La rimozione di operazioni a esecuzione prolungata o di blocco dal thread dell'interfaccia utente richiede l'implementazione di un framework asincrono che consente l'offload di tali operazioni nei thread di lavoro.
Che cosa fare:
- Usare le API di messaggio della finestra asincrone nel thread dell'interfaccia utente, in particolare sostituendo SendMessage con uno dei peer non bloccanti: PostMessage, SendNotifyMessage o SendMessageCallback
- Usare i thread in background per eseguire attività a esecuzione prolungata o bloccanti. Usare la nuova API del pool di thread per implementare i thread di lavoro
- Fornire il supporto per l'annullamento per le attività in background a esecuzione prolungata. Per bloccare le operazioni di I/O, usare l'annullamento di I/O, ma solo come ultima risorsa; non è facile annullare l'operazione 'right'
- Implementare una progettazione asincrona per il codice gestito usando il modello IAsyncResult o eventi
Usare i blocchi in modo saggio
L'applicazione o la DLL necessita di blocchi per sincronizzare l'accesso alle relative strutture di dati interne. L'uso di più blocchi aumenta il parallelismo e rende l'applicazione più reattiva. Tuttavia, l'uso di più blocchi aumenta anche la possibilità di acquisire tali blocchi in ordini diversi e causare il deadlock dei thread. Se due thread contengono un blocco e quindi tentano di acquisire il blocco dell'altro thread, le operazioni formeranno un'attesa circolare che blocca lo stato di avanzamento di avanzamento per questi thread. È possibile evitare questo deadlock solo assicurandosi che tutti i thread nell'applicazione acquisiscono sempre tutti i blocchi nello stesso ordine. Tuttavia, non è sempre facile acquisire blocchi nell'ordine "giusto". I componenti software possono essere composti, ma le acquisizioni di blocchi non possono. Se il codice chiama un altro componente, i blocchi del componente diventano ora parte dell'ordine di blocco implicito, anche se non si ha visibilità su tali blocchi.
Le cose diventano ancora più difficili perché le operazioni di blocco includono molto più delle normali funzioni per sezioni critiche, mutex e altri blocchi tradizionali. Tutte le chiamate di blocco che superano i limiti del thread hanno proprietà di sincronizzazione che possono causare un deadlock. Il thread chiamante esegue un'operazione con semantica 'acquire' e non può sbloccare fino a quando il thread di destinazione 'rilascia' che chiama. Alcune funzioni User32 (ad esempio SendMessage) e molte chiamate COM bloccano rientrano in questa categoria.
Peggio ancora, il sistema operativo ha un proprio blocco interno specifico del processo che a volte viene mantenuto durante l'esecuzione del codice. Questo blocco viene acquisito quando le DLL vengono caricate nel processo e quindi viene chiamato "blocco del caricatore". La funzione DllMain viene sempre eseguita nel blocco del caricatore; se si acquisiscono blocchi in DllMain (e non è consigliabile), è necessario impostare il blocco del caricatore come parte dell'ordine di blocco. La chiamata a determinate API Win32 potrebbe anche acquisire il blocco del caricatore per conto dell'utente, ad esempio LoadLibraryEx, GetModuleHandle e in particolare CoCreateInstance.
Per collegare tutto questo insieme, esaminare il codice di esempio seguente. Questa funzione acquisisce più oggetti di sincronizzazione e definisce in modo implicito un ordine di blocco, un elemento che non è necessariamente ovvio durante l'ispezione cursore. Nella voce della funzione, il codice acquisisce una sezione critica e non la rilascia fino all'uscita dalla funzione, rendendolo quindi il nodo principale nella gerarchia di blocco. Il codice chiama quindi la funzione Win32 LoadIcon(), che sotto le quinte potrebbe chiamare nel caricatore del sistema operativo per caricare questo file binario. Questa operazione acquisirà il blocco del caricatore, che ora diventa anche parte di questa gerarchia di blocchi (assicurarsi che la funzione DllMain non acquisisca il blocco g_cs). Successivamente il codice chiama SendMessage(), un'operazione di blocco tra thread, che non restituirà a meno che il thread dell'interfaccia utente non risponda. Anche in questo caso, assicurarsi che il thread dell'interfaccia utente non acquisisca mai g_cs.
bool foo::bar (char* buffer)
{
EnterCriticalSection(&g_cs);
// Get 'new data' icon
this.m_Icon = LoadIcon(hInst, MAKEINTRESOURCE(5));
// Let UI thread know to update icon SendMessage(hWnd,WM_COMMAND,IDM_ICON,NULL);
this.m_Params = GetParams(buffer);
LeaveCriticalSection(&g_cs);
return true;
}
Esaminando questo codice sembra chiaro che è stato eseguito in modo implicito g_cs il blocco di primo livello nella gerarchia di blocchi, anche se si vuole sincronizzare solo l'accesso alle variabili membro della classe.
Che cosa fare:
- Progettare una gerarchia di blocchi e obbedirla. Aggiungere tutti i blocchi necessari. Esistono molte più primitive di sincronizzazione rispetto a Mutex e CriticalSections; devono essere tutti inclusi. Includere il blocco del caricatore nella gerarchia se si accettano blocchi in DllMain()
- Accettare il protocollo di blocco con le dipendenze. Qualsiasi codice chiamato dall'applicazione o che potrebbe chiamare l'applicazione deve condividere la stessa gerarchia di blocchi
- Blocca strutture di dati non funzioni. Spostare le acquisizioni di blocco dai punti di ingresso della funzione e proteggere solo l'accesso ai dati con blocchi. Se un numero minore di codice funziona in un blocco, è meno possibile che si verifichino deadlock
- Analizzare le acquisizioni e le versioni dei blocchi nel codice di gestione degli errori. Spesso la gerarchia di blocchi se dimenticata quando si tenta di eseguire il ripristino da una condizione di errore
- Sostituire i blocchi annidati con i contatori di riferimento. Non possono essere deadlock. Gli elementi bloccati singolarmente in elenchi e tabelle sono candidati validi
- Prestare attenzione quando si attende un handle di thread da una DLL. Si supponga sempre che il codice possa essere chiamato nel blocco del caricatore. È preferibile contare le risorse e consentire al thread di lavoro di eseguire la propria pulizia (e quindi usare FreeLibraryAndExitThread per terminare correttamente)
- Usare l'API di attraversamento della catena di attesa per diagnosticare i propri deadlock
Non:
- Eseguire operazioni di inizializzazione molto semplici nella funzione DllMain(). Per altri dettagli, vedere Funzione di callback DllMain. In particolare, non chiamare LoadLibraryEx o CoCreateInstance
- Scrivere primitive di blocco personalizzate. Il codice di sincronizzazione personalizzato può introdurre facilmente bug sottili nella codebase. Usare invece la selezione avanzata di oggetti di sincronizzazione del sistema operativo
- Eseguire qualsiasi operazione nei costruttori e nei distruttori per le variabili globali, vengono eseguite nel blocco del caricatore
Prestare attenzione alle eccezioni
Le eccezioni consentono la separazione del normale flusso del programma e della gestione degli errori. A causa di questa separazione, può essere difficile conoscere lo stato preciso del programma prima dell'eccezione e il gestore eccezioni potrebbe perdere passaggi cruciali per ripristinare uno stato valido. Ciò vale soprattutto per le acquisizioni di blocchi che devono essere rilasciate nel gestore per evitare deadlock futuri.
Il codice di esempio seguente illustra questo problema. L'accesso non associato alla variabile "buffer" causa occasionalmente una violazione di accesso (AV). Questo av viene intercettato dal gestore eccezioni nativo, ma non ha un modo semplice per determinare se la sezione critica è già stata acquisita al momento dell'eccezione (av potrebbe anche aver avuto luogo da qualche parte nel codice EnterCriticalSection).
BOOL bar (char* buffer)
{
BOOL rc = FALSE;
__try {
EnterCriticalSection(&cs);
while (*buffer++ != '&') ;
rc = GetParams(buffer);
LeaveCriticalSection(&cs);
} __except (EXCEPTION_EXECUTE_HANDLER)
{
return FALSE;
}
return rc;
}
Che cosa fare:
- Rimuovere __try/__except quando possibile; non usare SetUnhandledExceptionFilter
- Eseguire il wrapping dei blocchi in modelli personalizzati simili a auto_ptr se si usano eccezioni C++. Il blocco deve essere rilasciato nel distruttore. Per le eccezioni native, rilasciare i blocchi nell'istruzione __finally
- Prestare attenzione al codice in esecuzione in un gestore di eccezioni nativo; l'eccezione potrebbe aver trapelato molti blocchi, quindi il gestore non deve acquisire alcun
Non:
- Gestire le eccezioni native, se non necessarie o richieste dalle API Win32. Se si usano gestori eccezioni nativi per la creazione di report o il ripristino dei dati dopo errori irreversibili, è consigliabile usare invece il meccanismo predefinito del sistema operativo di Segnalazione errori Windows
- Usare le eccezioni C++ con qualsiasi tipo di codice dell'interfaccia utente (user32); Un'eccezione generata in un callback passerà attraverso livelli di codice C forniti dal sistema operativo. Questo codice non conosce la semantica di annullamento della registrazione in C++
Collegamenti alle risorse
- Segnalazione errori Windows
- Progettazione asincrona
- I/O asincrono
- Funzione AttachThreadInput
- Classe auto_ptr
- Funzione DisableProcessWindowsGhosting
- Funzione di callback DllMain
- Eventi
- Funzione GetMessage
- Annullamento di I/O
- Funzione IsHungAppWindow
- Coda messaggi
- Funzione MsgWaitForMultipleObjects
- Nuova API del pool di thread
- Funzione PostMessage
- Riavviare e ripristinare
- Funzione SendMessageCallback
- Funzione SendNotifyMessage
- Oggetti di sincronizzazione
- Funzione TerminateThread
- Segnalazione errori Windows
- Winqual