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

37

Ho visto, in molti posti, 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 ci si trova in 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.

Specificamente, diciamo che ho un'applicazione con architettura MVC che usa Swing. Se il chiamante è responsabile dell'aggiornamento dei componenti sul thread di invio eventi, se provo a sostituire l'implementazione di Swing View per un'implementazione JavaFX, devo modificare tutto il codice presenter / controller per utilizzare il thread dell'applicazione JavaFX invece .

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 lento di questi problemi di sicurezza dei thread, e tuttavia essere ancora adeguatamente thread-safe?

Lasciatemi aggiungere del codice Java MCVE per illustrare cosa intendo per "responsabile chiamante" (ci sono altre buone pratiche qui che non sto facendo, ma sto cercando di essere il più minimale possibile): / p>

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 il 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 di tag più alto in Swing on Stack Overflow. Lo dice dappertutto e ho anche visto che è la responsabilità del chiamante anche in altri posti.

    
posta durron597 02.09.2015 - 21:00
fonte

4 risposte

22

Verso la fine del suo saggio sui sogni fallito , Graham Hamilton (a importante architetto Java) menziona se gli sviluppatori "devono preservare l'equivalenza con un modello di coda degli eventi, dovranno seguire vari regole non ovvie, "e avere un modello di coda di eventi visibile ed esplicito" 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 mettere una facciata multithread su un modello di coda di eventi, l'astrazione a volte perde in modi non ovvi che sono estremamente difficili da eseguire il debug. Sembra che funzionerà su carta, ma finisce per andare a pezzi in produzione.

L'aggiunta di piccoli wrapper attorno ai singoli componenti probabilmente non sarà problematica, 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 layer multithread e il livello della coda degli eventi.

Si noti che questi tipi di problemi sono universali a tutti i toolkit della GUI. Presumendo che un modello di invio di eventi nel tuo presentatore / controllore non accoppi strettamente al solo modello di concorrenza di uno specifico toolkit GUI, ti sta accoppiando a tutti . L'interfaccia di accodamento degli eventi non dovrebbe essere così difficile da astrarre.

    
risposta data 03.09.2015 - 00:03
fonte
25

Perché rendere sicuro il thread lib della GUI è un enorme 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 radice ai widget GUI e dal codice dell'applicazione al widget propagato fino alla finestra radice.

L'ideazione di una strategia di blocco che non blocchi la finestra di root (causerà molti conflitti) è difficile . Il blocco verso il basso mentre un altro thread è bloccato 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 è costoso e può causare confusione su ciò che sta effettivamente accadendo con la GUI, specialmente quando si sta eseguendo una sequenza di scrittura di aggiornamento di lettura. Questo deve avere un blocco sui dati per evitare le gare.

    
risposta data 02.09.2015 - 21:23
fonte
17

Threadedness (in un modello di memoria condivisa) è una proprietà che tende a sfidare gli sforzi di astrazione. Un semplice esempio è un Set -type: mentre Contains(..) e Add(...) e Update(...) è un'API perfettamente valida in uno scenario a thread singolo, lo scenario multi-thread ha bisogno di un AddOrUpdate .

La stessa cosa vale per l'interfaccia utente: se si desidera visualizzare un elenco di elementi con un conteggio degli elementi in cima all'elenco, è necessario aggiornarli ad ogni modifica.

  1. Il toolkit non può risolvere questo problema poiché il blocco non garantisce 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 solo l'elenco attraverso la vista. Non esattamente quello che dovrebbe essere MVC.
  3. Il relatore può risolverlo, ma deve essere consapevole del fatto che la visualizzazione ha esigenze particolari per quanto riguarda il threading.
  4. L'associazione di dati a un modello multi-threading è un'altra opzione. Ma questo complica il modello con cose che dovrebbero essere problemi di interfaccia utente.

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

    
risposta data 02.09.2015 - 21:42
fonte
9

Ho letto un buon blog qualche tempo fa che discute di questo problema (citato da Karl Bielefeldt), quello che sostanzialmente dice è che è molto pericoloso cercare di rendere sicuro il thread del kit UI, poiché introduce possibili deadlock e su come è implementato, le condizioni di gara nel framework.

C'è anche una considerazione prestazionale. Non tanto adesso, ma quando Swing è stato rilasciato per la prima volta, è stato pesantemente criticato per le sue prestazioni (state male), ma non è stata colpa di Swing, è stata la mancanza di conoscenza della gente su come usarlo.

SWT applica il concetto di sicurezza del thread generando eccezioni se lo violano, non grazioso, ma almeno ne sei consapevole.

Se osservi il processo di pittura, ad esempio, l'ordine in cui gli elementi vengono 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à di testo di una etichetta, ma è stata dipinta da due thread diversi, potresti finire con un output corrotto. Quindi tutta la verniciatura viene eseguita all'interno di un singolo thread, normalmente basato sull'ordine dei requisiti / richieste (ma a volte condensato per ridurre il numero di cicli di verniciatura fisici effettivi)

Hai menzionato il passaggio da Swing a JavaFX, ma avresti questo problema con qualsiasi framework dell'interfaccia utente (non solo i thick client, ma anche il web), Swing sembra essere quello che evidenzia il problema.

Potresti 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 progettare le parti non UI dell'API dal punto di vista dell'API dell'interfaccia utente e la maggior parte degli sviluppatori si lamenterebbe che qualsiasi protezione dei thread implementata nell'API dell'interfaccia utente fosse restrittiva o non soddisfacesse le proprie esigenze. Meglio permetterti di decidere come vuoi 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 conosciuti. Ad esempio, se l'utente ridimensiona la finestra, il modello Event Queue garantisce che si verifichi un determinato ordine di eventi, questo potrebbe sembrare semplice, ma se la coda ha consentito agli eventi di essere attivati da altri thread, non è più possibile garantire l'ordine in che gli eventi potrebbero accadere (una condizione di competizione) e all'improvviso devi iniziare a preoccuparti dei diversi stati e non fare una cosa finché non accade qualcos'altro e inizi a dover condividere le bandiere di stato e finisci con gli spaghetti.

Okay, potresti risolvere questo problema avendo una sorta di coda che ordinava gli eventi in base al momento in cui sono stati rilasciati, ma non è quello che abbiamo già? Inoltre, non si può ancora garantire che il thread B possa generare i suoi eventi DOPO il thread A

Il motivo principale per cui le persone si arrabbiano per il fatto di dover pensare al loro codice, è perché sono costretti a pensare al loro 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 stava parlando 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 sincronizzi i flussi?"

Il problema di cui lo sviluppatore stava parlando era che, a un certo punto, tutti quei flussi separati avrebbero dovuto essere sincronizzati fino alla pipe singola per l'output. Il povero presentatore si limitò a scrollare le spalle in quanto non era un problema a cui erano familiari. Ovviamente, hanno avuto soluzioni per risolvere questo problema ora, ma è divertente al momento.

I computer moderni stanno prendendo molto input da molti luoghi diversi simultaneamente, tutti gli input devono essere elaborati e consegnati all'utente in modo da non interferire con la presentazione di altre informazioni, quindi è un problema complesso , senza una sola soluzione semplice.

Ora, avendo la possibilità di cambiare framework, non è una cosa facile da progettare, BUT, prendi MVC per un momento, MVC può essere multistrato, cioè potresti avere un MVC che tratta direttamente con la gestione dell'interfaccia utente framework, è quindi possibile eseguire il wrapping che, di nuovo, in un MVC di livello superiore che tratta le interazioni con altri framework (potenzialmente multi-thread), sarebbe responsabilità di questo layer determinare come viene informato / aggiornato il layer MVC inferiore.

Utilizzerai quindi 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 multithreading vengono disaccoppiati dal livello dell'interfaccia utente tramite l'uso di un livello intermedio, come idea.

    
risposta data 03.09.2015 - 02:34
fonte

Leggi altre domande sui tag