Quando il polling per gli eventi sarebbe meglio dell'utilizzo del modello di osservatore?


41

Esistono scenari in cui il polling per gli eventi sarebbe meglio dell'utilizzo del modello di osservatore ? Ho paura di usare il polling e inizierei ad usarlo solo se qualcuno mi ha dato un buon scenario. Tutto quello a cui riesco a pensare è come il modello di osservatore sia migliore del polling. Considera questo scenario:

Stai programmando un simulatore di auto. L'auto è un oggetto. Non appena l'auto si accende, si desidera riprodurre una clip audio "Vroom Vroom".

Puoi modellarlo in due modi:

Polling : esegue il polling dell'oggetto auto ogni secondo per vedere se è attivo. Quando è acceso, riprodurre la clip audio.

Modello osservatore : trasforma la macchina nel soggetto del modello osservatore. Fai pubblicare l'evento "on" a tutti gli osservatori quando si accende. Crea un nuovo oggetto sonoro che ascolta la macchina. Hanno implementare il callback "on", che riproduce la clip audio.

In questo caso, penso che il modello di osservatore vince. In primo luogo, il polling richiede più processore. In secondo luogo, la clip audio non si attiva immediatamente all'accensione dell'auto. Può esserci un divario fino a 1 secondo a causa del periodo di polling.


Non riesco a pensare a quasi nessuno scenario. Il modello di osservatore è ciò che in realtà è mappato al mondo reale e alla vita reale. Quindi penso che nessuno scenario giustificherebbe mai non usarlo.
Saeed Neamati,

Stai parlando di eventi dell'interfaccia utente o di eventi in generale?
Bryan Oakley,

3
Il tuo esempio non rimuove il problema di polling / osservazione. L'hai semplicemente passato a un livello inferiore. Il tuo programma deve ancora capire se l'auto è accesa o meno con qualche meccanismo.
Dunk,

Risposte:


55

Immagina di voler essere informato su ogni ciclo del motore, ad es. Per visualizzare una misurazione del regime al conducente.

Modello di osservatore: il motore pubblica un evento "ciclo motore" a tutti gli osservatori per ciascun ciclo. Crea un listener che conteggi gli eventi e aggiorni il display RPM.

Polling: il display RPM richiede al motore a intervalli regolari un contatore del ciclo del motore e aggiorna il display RPM di conseguenza.

In questo caso, il modello di osservatore probabilmente si perderebbe: il ciclo del motore è un processo ad alta frequenza e priorità, non si desidera ritardare o arrestare quel processo solo per aggiornare un display. Inoltre, non si desidera bloccare il pool di thread con eventi del ciclo del motore.


PS: utilizzo spesso anche il modello di polling nella programmazione distribuita:

Modello di osservatore: il processo A invia un messaggio al processo B che dice "ogni volta che si verifica un evento E, invia un messaggio al processo A".

Modello di polling: il processo A invia regolarmente un messaggio al processo B che dice "se l'evento E si è verificato dall'ultima volta che ho eseguito il polling, inviami un messaggio ora".

Il modello di polling produce un po 'più carico di rete. Ma il modello di osservatore ha anche degli aspetti negativi:

  • Se il processo A si arresta in modo anomalo, non annullerà mai l'iscrizione e il processo B tenterà di inviargli notifiche per tutta l'eternità, a meno che non sia in grado di rilevare in modo affidabile errori di processo remoti (non è una cosa facile da fare)
  • Se l'evento E è molto frequente e / o le notifiche contengono molti dati, il processo A potrebbe ricevere più notifiche di eventi di quante ne possa gestire. Con il modello di polling, può semplicemente limitare il polling.
  • Nel modello di osservatore, un carico elevato può causare "increspature" nell'intero sistema. Se si utilizzano prese di blocco, queste increspature possono andare in entrambe le direzioni.

1
Buon punto. A volte è meglio anche il sondaggio per motivi di performance.
Falcon,

1
Anche il numero atteso di osservatori è una considerazione. Quando ci si aspetta un gran numero di osservatori, aggiornarli tutti dall'osservato può diventare un collo di bottiglia nelle prestazioni. Molto più semplice quindi scrivere un valore da qualche parte e fare in modo che gli "osservatori" controllino quel valore quando ne hanno bisogno.
Marjan Venema,

1
"a meno che non sia in grado di rilevare in modo affidabile errori di processo remoti (non è una cosa facile da fare)" ... tranne mediante polling; P. Pertanto, il miglior progetto possibile è minimizzare il più possibile la risposta "nulla è cambiato". +1, buona risposta.
pdr,

2
@Jojo: Puoi, sì, ma poi stai inserendo una politica che dovrebbe appartenere al display nel contatore RPM. Forse l'utente desidera occasionalmente avere un display RPM estremamente preciso.
Zan Lynx,

2
@JoJo: pubblicare ogni centesimo evento è un hack. Funziona bene solo se la frequenza dell'evento è sempre nell'intervallo corretto, se l'elaborazione dell'evento non richiede troppo tempo per il motore, se tutti gli abbonati hanno bisogno di una precisione comparabile. E richiede un'operazione di modulo per RPM, che (supponendo alcune migliaia di RPM) è molto più lavoro per la CPU di alcune operazioni di polling al secondo.
Nikie,

7

Il polling è migliore se il processo di polling è considerevolmente più lento rispetto alle polling. Se stai scrivendo eventi in un database, spesso è meglio eseguire il polling di tutti i produttori di eventi, raccogliere tutti gli eventi che si sono verificati dall'ultimo sondaggio, quindi scriverli in un'unica transazione. Se si tenta di scrivere tutti gli eventi nel momento in cui si è verificato, potrebbe non essere possibile tenere il passo e alla fine potrebbero verificarsi problemi durante il riempimento delle code di input. Ha inoltre più senso nei sistemi distribuiti a accoppiamento lento, in cui la latenza è elevata o l'impostazione della connessione e lo smontaggio sono costosi. Trovo i sistemi di polling più facili da scrivere e comprendere, ma nella maggior parte dei casi gli osservatori o i consumatori guidati dagli eventi sembrano offrire prestazioni migliori (secondo la mia esperienza).


7

Il polling è molto più semplice per mettersi al lavoro su una rete quando le connessioni potrebbero non riuscire, i server potrebbero andare a buon fine, ecc. Ricordare che alla fine della giornata un socket TCP ha bisogno di "polling" messaggi keep-a-live altrimenti il ​​server assumerà il client è andato via.

Il polling è utile anche quando desideri mantenere aggiornata un'interfaccia utente, ma gli oggetti sottostanti cambiano molto velocemente , non ha senso aggiornare l'interfaccia utente più di alcune volte al secondo nella maggior parte delle app.

A condizione che il server possa rispondere "senza modifiche" a costi molto bassi e non si esegue il polling troppo spesso e non si dispone di migliaia di polling di client, quindi il polling funziona molto bene nella vita reale.

Tuttavia, per i casi " in memoria ", per impostazione predefinita utilizzo il modello di osservatore in quanto è normalmente il minimo lavoro.


5

Il polling presenta alcuni svantaggi, in pratica li hai già indicati nella tua domanda.

Tuttavia, può essere una soluzione migliore, quando si desidera veramente disaccoppiare l'osservabile da qualsiasi osservatore. Ma a volte potrebbe essere meglio usare un wrapper osservabile per l'oggetto da osservare in questi casi.

Utilizzerei il polling solo quando l'osservabile non può essere osservato con le interazioni degli oggetti, come spesso accade quando si interrogano database, ad esempio, dove non è possibile richiamare. Un altro problema potrebbe essere il multithreading, dove spesso è più sicuro eseguire il polling e l'elaborazione dei messaggi piuttosto che invocare oggetti direttamente, per evitare problemi di concorrenza.


Non sono del tutto sicuro del motivo per cui ritieni che il polling sia più sicuro per il multi threading. Nella maggior parte dei casi, non sarà così. Quando il gestore del polling riceve una richiesta di polling, ha dovuto capire lo stato dell'oggetto sottoposto a polling, se l'oggetto è in fase di aggiornamento, non è sicuro per il gestore del polling. Nello scenario di ascolto, ricevi notifiche solo se lo spintore è in stato coerente, quindi puoi evitare la maggior parte dei problemi di sincronizzazione nell'oggetto polling.
Sdraiati Ryan il

4

Per un buon esempio di quando il polling prende il posto della notifica, guarda gli stack di rete del sistema operativo.

È stato un grosso problema per Linux quando lo stack di rete ha abilitato NAPI, un'API di rete che ha permesso ai driver di passare da una modalità di interruzione (notifica) a una modalità di polling.

Con più interfacce Gigabit Ethernet, gli interrupt sovraccaricherebbero spesso la CPU, facendo sì che il sistema funzioni più lentamente di quanto dovrebbe. Con il polling le schede di rete raccolgono i pacchetti nei buffer fino al polling o le schede scrivono persino i pacchetti in memoria tramite DMA. Quindi, quando il sistema operativo è pronto, esegue il polling della scheda per tutti i suoi dati ed esegue l'elaborazione TCP / IP standard.

La modalità di polling consente alla CPU di raccogliere i dati Ethernet alla massima velocità di elaborazione senza inutili carichi di interruzione. La modalità di interruzione consente alla CPU di restare inattiva tra i pacchetti quando il lavoro non è così occupato.

Il segreto è quando passare da una modalità all'altra. Ogni modalità presenta vantaggi e deve essere utilizzata nel posto giusto.


2

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 Caswellciò 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 PaintSystempotrebbe 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 PaintSystemche 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'scodice, 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"


1
"come usare le variabili di condizione per avvisare i thread di svegliarsi" Sembra un accordo basato su eventi / osservatore, non polling.
Josh Caswell,

Trovo la differenza molto sottile ma la notifica è solo sotto forma di "C'è del lavoro da fare" per riattivare i thread. Ad esempio, un modello di osservatore potrebbe, al ridimensionamento di un controllo principale, il ridimensionamento in cascata delle chiamate verso il basso nella gerarchia utilizzando l'invio dinamico. Le cose avrebbero le loro funzioni di ridimensionamento degli eventi indirettamente chiamate immediatamente. Quindi potrebbero ridipingere se stessi immediatamente. Quindi, se utilizziamo una coda di eventi, il ridimensionamento di un controllo principale potrebbe spingere gli eventi di ridimensionamento nella gerarchia, a quel punto le funzioni di ridimensionamento per ciascun controllo potrebbero essere chiamate in modo differito, a cui ...

... punto che potrebbero quindi spingere gli eventi di ridipingere che, allo stesso modo, vengono chiamati in modo differito dopo che tutto è stato ridimensionato e tutto da un thread centrale di gestione degli eventi. E trovo che la centralizzazione sia un po 'utile almeno per quanto riguarda il debug e la capacità di ragionare facilmente su dove si sta svolgendo l'elaborazione (incluso quale thread) ... Quindi ciò che sto prendendo in considerazione per essere il più vicino al polling non è né di queste soluzioni ...

Sarebbe, ad esempio, avere un LayoutSystemche normalmente sta dormendo, ma quando l'utente ridimensiona un controllo, userebbe una variabile di condizione per riattivare il LayoutSystem. Quindi LayoutSystemridimensiona tutti i controlli necessari e torna a dormire. Nel processo, le aree rettangolari in cui risiedono i widget sono contrassegnate come necessitanti di aggiornamenti, a quel punto una PaintSystemsi sveglia e attraversa quelle regioni rettangolari, ridipingendo quelle che devono essere ridisegnate in un ciclo sequenziale piatto.

Quindi la variabile condizione stessa segue un modello di osservatore per avvisare i thread di svegliarsi, ma non stiamo trasmettendo alcuna informazione più di "c'è del lavoro da fare". E ogni sistema che si sveglia è dedicato all'elaborazione delle cose in un ciclo molto semplice applicando un compito molto omogeneo, al contrario di una coda di eventi che ha compiti non omogenei (potrebbe contenere una miscela eclettica di eventi da elaborare).

-4

Ti sto dando una visione d'insieme del modo concettuale di pensare allo scalpiccio dell'osservatore. Pensa a uno scenario come l'iscrizione a canali YouTube. Ci sono un numero di utenti che si iscrivono al canale e una volta che ci sarà un aggiornamento sul canale che comprende molti video, l'abbonato viene avvisato che c'è un cambiamento in questo particolare canale. Quindi abbiamo concluso che se il canale è SOGGETTO che ha la possibilità di abbonarsi, annullare l'iscrizione e informare tutti gli OBSERVER che sono registrati nel canale.


2
questo non tenta nemmeno di rispondere alla domanda posta, quando il polling per gli eventi sarebbe meglio dell'uso del modello di osservatore. Vedi Come rispondere
moscerino
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.