Perché è responsabilità del chiamante garantire la sicurezza dei thread nella programmazione della GUI?


37

Ho visto, in molti luoghi, che è saggezza canonica 1 che è responsabilità del chiamante assicurarsi di essere sul thread dell'interfaccia utente durante l'aggiornamento dei componenti dell'interfaccia utente (in particolare, in Java Swing, che si è sul thread di invio eventi ) .

Perché è così? Il thread di invio eventi è una preoccupazione della vista in MVC / MVP / MVVM; per gestirlo ovunque ma la vista crea un accoppiamento stretto tra l'implementazione della vista e il modello di threading dell'implementazione di quella vista.

In particolare, supponiamo che io abbia un'applicazione progettata MVC che utilizza Swing. Se il chiamante è responsabile dell'aggiornamento dei componenti sul thread di invio eventi, quindi se provo a scambiare la mia implementazione Swing View per un'implementazione JavaFX, devo invece modificare tutto il codice Presenter / Controller per utilizzare invece il thread dell'applicazione JavaFX .

Quindi, suppongo di avere due domande:

  1. Perché è responsabilità del chiamante garantire la sicurezza del thread dei componenti dell'interfaccia utente? Dov'è il difetto nel mio ragionamento sopra?
  2. Come posso progettare la mia applicazione in modo da avere un accoppiamento libero di questi problemi di sicurezza della filettatura, ma allo stesso tempo essere adeguatamente sicuro per la filettatura?

Consentitemi di aggiungere un po 'di codice Java MCVE per illustrare ciò che intendo per "responsabile del chiamante" (ci sono alcune altre buone pratiche che non sto facendo, ma sto cercando di essere il più minimale possibile):

Il chiamante è responsabile:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        view.setData(data);
      }
    });
  }
}
public class View {
  void setData(Data data) {
    component.setText(data.getMessage());
  }
}

Visualizza essere responsabile:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    view.setData(data);
  }
}
public class View {
  void setData(Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        component.setText(data.getMessage());
      }
    });
  }
}

1: L'autore di quel post ha il punteggio più alto in Swing su Stack Overflow. Lo dice dappertutto e ho anche visto che è responsabilità del chiamante anche in altri posti.


1
Eh, spettacolo IMHO. Quelle pubblicazioni di eventi non sono gratuite e in app non banali dovresti minimizzare il loro numero (e assicurarti che nessuna sia troppo grande), ma che la minimizzazione / condensazione dovrebbe essere logicamente fatta nel presentatore.
Ordous,

1
@Ordous Puoi comunque assicurarti che le registrazioni siano minime mentre metti il ​​handoff del thread nella vista.
durron597,

2
Ho letto un blog davvero buono qualche tempo fa che discute di questo problema, ciò che dice sostanzialmente è che è molto pericoloso provare a rendere sicuro il thread del kit UI, in quanto introduce possibili deadlock e, a seconda di come è implementato, le condizioni di gara nel struttura. C'è anche una considerazione delle prestazioni. Non molto ora, ma quando Swing è stato rilasciato per la prima volta è stato fortemente criticato per le sue prestazioni (è stato un male), ma non è stata davvero colpa di Swing, è stata la mancanza di conoscenza delle persone su come usarlo.
MadProgrammer

1
SWT applica il concetto di sicurezza del thread generando eccezioni se lo violate, non è carino, ma almeno ne siete consapevoli. Hai detto di cambiare da Swing a JavaFX, ma avresti questo problema con praticamente qualsiasi framework dell'interfaccia utente, Swing sembra proprio essere quello che evidenzia il problema. È possibile progettare un livello intermedio (un controller di un controller?) Il cui compito è garantire che le chiamate all'interfaccia utente siano sincronizzate correttamente. È impossibile sapere esattamente come potresti progettare le parti non UI della tua API dal punto di vista dell'API UI
MadProgrammer

1
e la maggior parte degli sviluppatori si lamenterebbe che qualsiasi protezione dei thread implementata nell'API UI fosse restrittiva o non soddisfacesse le loro esigenze. Meglio permetterti di decidere come risolvere questo problema, in base alle tue esigenze
MadProgrammer

Risposte:


22

Verso la fine del suo saggio sui sogni fallito , Graham Hamilton (un grande architetto Java) menziona se gli sviluppatori "devono preservare l'equivalenza con un modello di coda di eventi, dovranno seguire varie regole non ovvie" e avere un visibile ed esplicito modello di coda eventi "sembra aiutare le persone a seguire il modello in modo più affidabile e quindi a costruire programmi GUI che funzionano in modo affidabile".

In altre parole, se si tenta di posizionare una facciata multithread in cima a un modello di coda eventi, l'astrazione a volte cola in modi non ovvi che sono estremamente difficili da eseguire il debug. Sembra che funzionerà sulla carta, ma finisce per cadere a pezzi in produzione.

L'aggiunta di piccoli wrapper attorno a singoli componenti probabilmente non sarà problematico, come aggiornare una barra di avanzamento da un thread di lavoro. Se provi a fare qualcosa di più complesso che richiede più blocchi, inizia a diventare davvero difficile ragionare su come interagiscono il livello multithread e il livello coda eventi.

Si noti che questo tipo di problemi sono universali per tutti i toolkit della GUI. Presumere che un modello di invio di eventi nel tuo presentatore / controller non ti stia legando strettamente a un solo modello di concorrenza del toolkit GUI specifico, ma ti sta abbinando a tutti loro . L'interfaccia di accodamento degli eventi non dovrebbe essere così difficile da astrarre.


25

Perché rendere sicuro il thread lib della GUI è un grosso mal di testa e un collo di bottiglia.

Il flusso di controllo nelle GUI spesso va in 2 direzioni dalla coda degli eventi alla finestra principale ai widget gui e dal codice dell'applicazione al widget propagato fino alla finestra principale.

È difficile definire una strategia di blocco che non blocchi la finestra di root (causerà molte contese) . Il blocco dal basso verso l'alto mentre un altro thread sta bloccando dall'alto verso il basso è un ottimo modo per ottenere un deadlock istantaneo.

E verificare se il thread corrente è il thread della GUI ogni volta che è costoso e può causare confusione su ciò che sta realmente accadendo con la GUI, specialmente quando si sta eseguendo una sequenza di scrittura di aggiornamento di lettura. Ciò deve avere un blocco sui dati per evitare le gare.


1
È interessante notare che risolvo questo problema disponendo di un framework di accodamento per questi aggiornamenti che ho scritto che gestisce il trasferimento dei thread.
durron597,

2
@ durron597: E non hai mai avuto aggiornamenti a seconda dello stato corrente dell'interfaccia utente che un altro thread potrebbe influenzare? Quindi potrebbe funzionare.
Deduplicatore

Perché avresti bisogno di blocchi nidificati? Perché è necessario bloccare l'intera finestra principale quando si lavora sui dettagli in una finestra figlio? L'ordine di blocco è un problema risolvibile fornendo un unico metodo esposto per bloccare i blocchi multipli nell'ordine giusto (dall'alto verso il basso o dal basso verso l'alto, ma non lasciare la scelta al chiamante)
MSalters

1
@MSalters con root Intendevo la finestra corrente. Per ottenere tutti i blocchi che è necessario acquisire, è necessario risalire la gerarchia che richiede il blocco di ciascun contenitore quando lo si incontra, ottenendo il genitore e lo sblocco (per assicurarsi di bloccare solo dall'alto verso il basso) e quindi sperare che non sia cambiato tu dopo aver ottenuto la finestra di root e fai il blocco dall'alto verso il basso.
maniaco del cricchetto,

@ratchetfreak: se il bambino che provi a bloccare viene rimosso da un altro thread mentre lo stai bloccando, è leggermente sfortunato ma non rilevante per il blocco. Non puoi semplicemente operare su un oggetto che un altro thread ha appena rimosso. Ma perché quell'altro thread rimuove oggetti / finestre che il thread sta ancora utilizzando? Non va bene in nessuno scenario, non solo nell'interfaccia utente.
Salterio

17

Il threadedness (in un modello di memoria condivisa) è una proprietà che tende a sfidare gli sforzi di astrazione. Un semplice esempio è un Settipo: mentre Contains(..)ed Add(...)ed Update(...)è un'API perfettamente valida in uno scenario a thread singolo, lo scenario a thread multipli richiede un AddOrUpdate.

Lo stesso vale per l'interfaccia utente: se si desidera visualizzare un elenco di elementi con un conteggio degli elementi in cima all'elenco, è necessario aggiornare entrambi su ogni modifica.

  1. Il toolkit non può risolvere questo problema poiché il blocco non assicura che l'ordine delle operazioni rimanga corretto.
  2. La vista può risolvere il problema, ma solo se si consente alla regola aziendale che il numero in cima all'elenco debba corrispondere al numero di elementi nell'elenco e aggiornare l'elenco solo attraverso la vista. Non esattamente quello che dovrebbe essere MVC.
  3. Il relatore può risolverlo, ma deve sapere che la vista ha esigenze particolari in merito al threading.
  4. Un'altra opzione è rappresentata dal collegamento a un modello con capacità multi-threading. Ma ciò complica il modello con cose che dovrebbero essere preoccupazioni dell'interfaccia utente.

Nessuno di questi sembra davvero allettante. Rendere il presentatore responsabile della gestione del threading è consigliato non perché è buono, ma perché funziona e le alternative sono peggiori.


Forse un livello potrebbe essere introdotto tra la Vista e il Presenter e potrebbe anche essere sostituito.
durron597,

2
.NET ha il System.Windows.Threading.Dispatchercompito di gestire il dispacciamento sia con i thread dell'interfaccia utente di WPF che WinForms. Quel tipo di strato tra presentatore e vista è sicuramente utile. Ma fornisce solo indipendenza dal toolkit, non threading indipendence.
Patrick,

9

Qualche tempo fa ho letto un ottimo blog che discute di questo problema (menzionato da Karl Bielefeldt), quello che dice in sostanza è che è molto pericoloso provare a rendere sicuro il thread del kit UI, poiché introduce possibili deadlock e dipende da come è implementato, condizioni di gara nel quadro.

C'è anche una considerazione delle prestazioni. Non molto ora, ma quando Swing è stato rilasciato per la prima volta è stato fortemente criticato per le sue prestazioni (è stato un male), ma non è stata davvero colpa di Swing, è stata la mancanza di conoscenza delle persone su come usarlo.

SWT applica il concetto di sicurezza del thread generando eccezioni se lo violate, non è carino, ma almeno ne siete consapevoli.

Se osservi il processo di verniciatura, ad esempio, l'ordine in cui gli elementi sono dipinti è molto importante. Non vuoi che il dipinto di un componente abbia un effetto collaterale su qualsiasi altra parte dello schermo. Immagina di poter aggiornare la proprietà text di un'etichetta, ma è stata dipinta da due thread diversi, potresti finire con un output danneggiato. Quindi tutta la verniciatura viene eseguita all'interno di un singolo filo, normalmente basato sull'ordine dei requisiti / richieste (ma a volte condensato per ridurre il numero di cicli di verniciatura fisici effettivi)

Dici di cambiare da Swing a JavaFX, ma avresti questo problema con praticamente qualsiasi framework dell'interfaccia utente (non solo client spessi, ma anche web), Swing sembra proprio essere quello che evidenzia il problema.

È possibile progettare un livello intermedio (un controller di un controller?) Il cui compito è garantire che le chiamate all'interfaccia utente siano sincronizzate correttamente. È impossibile sapere esattamente come potresti progettare le parti non UI della tua API dal punto di vista dell'API UI e la maggior parte degli sviluppatori si lamenterebbe che qualsiasi protezione thread implementata nell'API UI fosse restrittiva o non soddisfaceva le loro esigenze. Meglio consentirti di decidere come risolvere questo problema, in base alle tue esigenze

Uno dei maggiori problemi che devi considerare è la capacità di giustificare un determinato ordine di eventi, sulla base di input noti. Ad esempio, se l'utente ridimensiona la finestra, il modello Coda eventi garantisce che si verifichi un determinato ordine di eventi, questo potrebbe sembrare semplice, ma se la coda consentiva agli eventi di essere attivati ​​da altri thread, non è più possibile garantire l'ordine in quali eventi potrebbero verificarsi (una condizione di gara) e all'improvviso devi iniziare a preoccuparti di diversi stati e non fare una cosa finché non succede qualcos'altro e inizi a dover condividere bandiere di stato in giro e finisci con gli spaghetti.

Ok, potresti risolverlo avendo una specie di coda che ordina gli eventi in base al momento in cui sono stati emessi, ma non è quello che già abbiamo? Inoltre, non puoi ancora garantire che il thread B genererà i suoi eventi DOPO il thread A

Il motivo principale per cui le persone si arrabbiano nel dover pensare al proprio codice è perché sono costretti a pensare al proprio codice / design. "Perché non può essere più semplice?" Non può essere più semplice, perché non è un problema semplice.

Ricordo quando è stata rilasciata la PS3 e Sony parlava del processore Cell ed è in grado di eseguire linee separate di logiche, decodificare audio, video, caricare e risolvere i dati del modello. Uno sviluppatore di giochi ha chiesto: "È tutto fantastico, ma come si sincronizzano i flussi?"

Il problema di cui parlava lo sviluppatore era, ad un certo punto, che tutti quei flussi separati avrebbero dovuto essere sincronizzati su un singolo tubo per l'output. Il povero presentatore si strinse semplicemente nelle spalle perché non era un problema a loro familiare. Ovviamente, hanno avuto soluzioni per risolvere questo problema ora, ma al momento è divertente.

I computer moderni stanno ricevendo contemporaneamente molti input da molti luoghi diversi, tutti gli input devono essere elaborati e consegnati all'utente in remoto, il che non interferisce con la presentazione di altre informazioni, quindi è un problema complesso, senza un'unica soluzione semplice.

Ora, avendo la possibilità di cambiare framework, non è una cosa facile da progettare, MA prendi MVC per un momento, MVC può essere multistrato, cioè potresti avere un MVC che si occupa direttamente della gestione del framework UI, tu potrebbe quindi concludere che, ancora una volta, in un MVC di livello superiore che si occupa delle interazioni con altri framework (potenzialmente multi threaded), sarebbe responsabilità di questo layer determinare come viene notificato / aggiornato il layer MVC inferiore.

Dovresti quindi utilizzare la codifica per interfacciare i modelli di progettazione e i modelli di fabbrica o di costruzione per costruire questi diversi livelli. Ciò significa che i framework multithread vengono disaccoppiati dal livello dell'interfaccia utente attraverso l'uso di un livello intermedio, come idea.


2
Non avresti questo problema con il web. JavaScript non ha deliberatamente supporto per il threading: un runtime JavaScript è effettivamente solo una grande coda di eventi. (Sì, lo so che JS in senso stretto ha WebWorker - questi sono thread nerfati e si comportano più come attori in altre lingue).
James_pic,

1
@James_pic Quello che hai effettivamente è la parte del browser che funge da sincronizzatore per la coda degli eventi, che è fondamentalmente ciò di cui abbiamo parlato, il chiamante è stato responsabile di garantire che si verifichino aggiornamenti con la coda degli eventi dei toolkit
MadProgrammer

Si, esattamente. La differenza fondamentale, nel contesto del Web, è che questa sincronizzazione si verifica indipendentemente dal codice del chiamante, poiché il runtime non fornisce alcun meccanismo che consenta l'esecuzione di codice al di fuori della coda degli eventi. Quindi il chiamante non deve assumersene la responsabilità. Credo che sia anche una grande parte della motivazione alla base dello sviluppo di NodeJS - se l'ambiente di runtime è un loop di eventi, allora tutto il codice è consapevole del loop di eventi per impostazione predefinita.
James_pic

1
Detto questo, ci sono framework dell'interfaccia utente del browser che hanno il proprio loop di eventi all'interno di un loop di eventi (ti sto guardando Angular). Poiché questi framework non sono più protetti dal runtime, i chiamanti devono anche assicurarsi che il codice venga eseguito all'interno del loop degli eventi, in modo simile al codice multithread in altri framework.
James_pic,

Ci sarebbero problemi particolari con i controlli "solo display" che forniscono automaticamente la sicurezza del thread? Se uno ha un controllo di testo modificabile che sembra essere scritto da un thread e letto da un altro, non c'è alcun modo in cui uno possa rendere la scrittura visibile al thread senza sincronizzare almeno una di quelle azioni con lo stato UI effettivo del controllo, ma tali problemi sono importanti per i controlli di sola visualizzazione? Penso che quelli potrebbero facilmente fornire qualsiasi blocco o interblocco necessario per le operazioni eseguite su di essi e consentire al chiamante di ignorare i tempi di quando ...
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.