Adoro il polling! Io? Sì! Io? Sì! Io? Sì! Lo faccio ancora? Sì! E adesso? Sì!
Come altri hanno già detto, può essere incredibilmente inefficiente se si esegue il polling solo per ripristinare lo stesso stato invariato più e più volte. Tale è una ricetta per bruciare i cicli della CPU e ridurre significativamente la durata della batteria sui dispositivi mobili. Ovviamente non è dispendioso tornare a uno stato nuovo e significativo ogni volta ad una velocità non più veloce di quanto desiderato.
Ma il motivo principale per cui amo il polling è per la sua semplicità e natura prevedibile. Puoi rintracciare il codice e vedere facilmente quando e dove accadranno le cose e in quale thread. Se, in teoria, vivessimo in un mondo in cui il polling era uno spreco trascurabile (anche se la realtà è lontana da esso), allora credo che semplificherebbe il mantenimento del codice un affare tremendo. E questo è il vantaggio del polling e del pull quando vedo se potremmo ignorare le prestazioni, anche se non dovremmo in questo caso.
Quando ho iniziato a programmare nell'era DOS, i miei piccoli giochi ruotavano attorno al polling. Ho copiato un codice assembly da un libro che ho capito a malapena in relazione agli interrupt di tastiera e l'ho memorizzato in un buffer di stati della tastiera, a quel punto il mio loop principale faceva sempre polling. Il tasto Su è abbassato? No. Il tasto Su è abbassato? No. Che ne dici adesso? No. Adesso? Sì. Va bene, sposta il giocatore.
E sebbene incredibilmente dispendioso, ho scoperto che è molto più facile ragionare rispetto a questi giorni di programmazione multi-tasking e guidata dagli eventi. Sapevo esattamente quando e dove sarebbero accadute le cose in ogni momento ed era più facile mantenere frame rate stabili e prevedibili senza singhiozzi.
Quindi da allora ho sempre cercato di trovare un modo per ottenere alcuni dei vantaggi e della prevedibilità di ciò senza effettivamente bruciare i cicli della CPU, come usare le variabili di condizione per avvisare i thread di svegliarsi a quel punto in cui possono ottenere il nuovo stato, fanno le loro cose e tornano a dormire in attesa di essere nuovamente avvisati.
E in qualche modo trovo che le code degli eventi siano molto più facili da lavorare con almeno i modelli di osservatori, anche se non rendono ancora così facile prevedere dove finirai o cosa succederà. Almeno centralizzano il flusso di controllo della gestione degli eventi in alcune aree chiave del sistema e gestiscono sempre quegli eventi nello stesso thread invece di rimbalzare da una funzione a un posto completamente remoto e imprevisto all'improvviso al di fuori di un thread di gestione degli eventi centrale. Quindi la dicotomia non deve sempre essere tra osservatori e sondaggi. Le code degli eventi sono una specie di via di mezzo.
Ma sì, in qualche modo trovo molto più facile ragionare su sistemi che fanno cose che sono analogamente più vicine al tipo di flussi di controllo prevedibili che ero solito fare durante il polling anni fa, mentre mi limitavo a contrastare la tendenza al lavoro volte in cui non si sono verificati cambiamenti di stato. Quindi c'è questo vantaggio se puoi farlo in un modo che non sta bruciando i cicli della CPU inutilmente come con le variabili di condizione.
Cicli omogenei
Va bene, ho ricevuto un ottimo commento da Josh Caswell
ciò che ha sottolineato una certa sciocchezza nella mia risposta:
"come usare le variabili di condizione per avvisare i thread di svegliarsi" Sembra un accordo basato su eventi / osservatore, non polling
Tecnicamente la variabile condizione stessa sta applicando il modello di osservatore per riattivare / notificare i thread, quindi chiamare quel "polling" sarebbe probabilmente incredibilmente fuorviante. Ma trovo che fornisca un vantaggio simile a quello che ho riscontrato come sondaggio dai giorni di DOS (solo in termini di flusso di controllo e prevedibilità). Proverò a spiegarlo meglio.
Ciò che ho trovato interessante in quei giorni era che potevi guardare una sezione di codice o tracciarla e dire: "Va bene, questa intera sezione è dedicata alla gestione degli eventi della tastiera. Nulla accadrà in questa sezione di codice E so esattamente cosa succederà prima, e so esattamente cosa succederà dopo (fisica e rendering, ad es.) " Il polling degli stati della tastiera ti ha dato quel tipo di centralizzazione del flusso di controllo per quanto riguarda la gestione di ciò che dovrebbe succedere in risposta a questo evento esterno. Non abbiamo risposto immediatamente a questo evento esterno. Abbiamo risposto a nostro piacimento.
Quando utilizziamo un sistema basato su push basato su un modello Observer, perdiamo spesso quei vantaggi. È possibile ridimensionare un controllo che attiva un evento di ridimensionamento. Quando lo rintracciamo, troviamo che siamo dentro un controllo esotico che fa molte cose personalizzate nel suo ridimensionamento che innesca più eventi. Finiamo per essere completamente sorpresi di rintracciare tutti questi eventi a cascata su dove finiamo nel sistema. Inoltre, potremmo scoprire che tutto ciò non si verifica nemmeno in modo coerente in un dato thread perché il thread A potrebbe ridimensionare un controllo qui mentre il thread B ridimensiona un controllo in seguito. Quindi ho sempre trovato molto difficile ragionare su quanto sia difficile prevedere dove tutto accade e cosa accadrà.
La coda degli eventi è un po 'più semplice per me su cui ragionare perché semplifica dove accadono tutte queste cose almeno a livello di thread. Tuttavia, potrebbero accadere molte cose diverse. Una coda di eventi potrebbe contenere una miscela eclettica di eventi da elaborare, e ognuno potrebbe ancora sorprenderci per quanto riguarda la cascata di eventi che si sono verificati, l'ordine in cui sono stati elaborati e il modo in cui finiamo per rimbalzare ovunque nella base del codice .
Quello che sto considerando "il più vicino" al polling non userebbe una coda di eventi ma rinvierebbe un tipo di elaborazione molto omogeneo. A PaintSystem
potrebbe essere avvisato attraverso una variabile di condizione che c'è un lavoro di pittura da fare per ridipingere alcune celle della griglia di una finestra, a quel punto fa un semplice ciclo sequenziale attraverso le celle e ridipinge tutto al suo interno nel giusto ordine z. Potrebbe esserci un livello di chiamata indiretta / invio dinamico qui per attivare gli eventi di disegno in ogni widget che risiede in una cella che deve essere ridipinta, ma è tutto - solo uno strato di chiamate indirette. La variabile di condizione utilizza il modello di osservatore per avvisare PaintSystem
che ha del lavoro da fare, ma non specifica nulla di più ePaintSystem
è dedicato a un compito uniforme e molto omogeneo a quel punto. Quando eseguiamo il debug e tracciamo il PaintSystem's
codice, sappiamo che non succederà nient'altro che la pittura.
Quindi si tratta principalmente di portare il sistema nel punto in cui hai queste cose eseguendo cicli omogenei sui dati applicando una responsabilità molto singolare su di esso invece di cicli non omogenei su diversi tipi di dati che eseguono numerose responsabilità come potremmo ottenere con l'elaborazione della coda degli eventi.
Puntiamo a questo tipo di cose:
when there's work to do:
for each thing:
apply a very specific and uniform operation to the thing
Al contrario di:
when one specific event happens:
do something with relevant thing
in relevant thing's event:
do some more things
in thing1's triggered by thing's event:
do some more things
in thing2's event triggerd by thing's event:
do some more things:
in thing3's event triggered by thing2's event:
do some more things
in thing4's event triggered by thing1's event:
cause a side effect which shouldn't be happening
in this order or from this thread.
E così via. E non deve essere un thread per attività. Un thread potrebbe applicare la logica dei layout (ridimensionamento / riposizionamento) per i controlli della GUI e ridipingerli, ma potrebbe non gestire i clic della tastiera o del mouse. Quindi potresti considerare questo come semplicemente migliorare l'omogeneità di una coda di eventi. Ma non dobbiamo nemmeno utilizzare una coda di eventi e intercalare le funzioni di ridimensionamento e disegno. Possiamo fare come:
in thread dedicated to layout and painting:
when there's work to do:
for each widget that needs resizing/reposition:
resize/reposition thing to target size/position
mark appropriate grid cells as needing repainting
for each grid cell that needs repainting:
repaint cell
go back to sleep
Quindi l'approccio sopra usa solo una variabile di condizione per avvisare il thread quando c'è del lavoro da fare, ma non interleave diversi tipi di eventi (ridimensionare in un ciclo, dipingere in un altro ciclo, non una combinazione di entrambi) e non funziona non preoccuparti di comunicare ciò che il lavoro è esattamente ciò che deve essere fatto (il thread "scopre" quello al risveglio osservando gli stati dell'ECS a livello di sistema). Ogni ciclo che esegue è quindi di natura molto omogenea, il che rende facile ragionare sull'ordine in cui tutto accade.
Non sono sicuro di come chiamare questo tipo di approccio. Non ho visto altri motori della GUI fare questo ed è una specie del mio approccio esotico al mio. Ma prima quando ho cercato di implementare framework GUI multithread utilizzando osservatori o code di eventi, ho avuto un'enorme difficoltà nel debug e ho anche incontrato alcune condizioni di gara oscure e deadlock che non ero abbastanza intelligente da risolvere in un modo che mi ha fatto sentire sicuro sulla soluzione (alcune persone potrebbero essere in grado di farlo ma non sono abbastanza intelligente). Il mio primo progetto di iterazione ha appena chiamato uno slot direttamente attraverso un segnale e alcuni slot genererebbero altri thread per fare il lavoro asincrono, e questo è stato il più difficile da ragionare e stavo inciampando su condizioni di gara e deadlock. La seconda iterazione utilizzava una coda di eventi ed era un po 'più facile ragionare, ma non abbastanza facile per il mio cervello da farlo senza ancora imbattersi nell'oscuro stallo e nella condizione di razza. La terza e ultima iterazione ha utilizzato l'approccio sopra descritto, e alla fine mi ha permesso di creare un framework GUI multithread che anche un semplice scemo come me poteva implementare correttamente.
Quindi questo tipo di design finale della GUI multithread mi ha permesso di inventare qualcos'altro che era molto più facile ragionare ed evitare quei tipi di errori che tendevo a fare, e uno dei motivi per cui ho trovato molto più facile ragionare su il minimo è dovuto a questi loop omogenei e al modo in cui somigliavano un po 'al flusso di controllo simile a quando stavo eseguendo il polling nei giorni di DOS (anche se non è realmente polling e esegue il lavoro solo quando c'è del lavoro da fare). L'idea era di allontanarsi il più possibile dal modello di gestione degli eventi, il che implica cicli non omogenei, effetti collaterali non omogenei, flussi di controllo non omogenei e lavorare sempre più verso circuiti omogenei operando uniformemente su dati omogenei e isolando e unificando gli effetti collaterali in modi che hanno reso più semplice concentrarsi solo su "cosa"