2012-01-12 16 views
6

Quali sono alcuni motivi per cui scrivere il seguente pezzo di codice è considerato una cattiva pratica?Perché il blocco anziché il ciclo?

while (someList.isEmpty()) { 
    try { 
     Thread.currentThread().sleep(100); 
    } 
    catch (Exception e) {} 
    } 
    // Do something to the list as soon as some thread adds an element to it. 

Per me, la scelta di un valore arbitrario di sonno non è buona pratica, e vorrei usare un BlockingQueue in questa situazione, ma mi piacerebbe sapere se c'è più di un motivo per cui non si dovrebbe scrivi questo codice

risposta

6

Si impone un ritardo medio di 50 millisecondi prima dell'evento si agisce su, e si risveglia 10 volte al secondo quando non c'è nessun evento da gestire. Se nessuna delle due cose conta in particolare, allora è semplicemente poco elegante.

+0

Basta chiedersi, non è così che le funzioni di attesa funzionano internamente comunque? (Esclusa ovviamente l'eccezione.) – Mehrdad

+1

@ Mehrdad- In genere no. Di solito i thread sono collocati in una "coda di attesa" e non viene fornito alcun tempo di processore. Quando si verificano alcuni eventi che li riattivano, vengono reinseriti nella coda di esecuzione in modo che vengano pianificati. Ciò significa che potresti avere un milione di thread dormienti senza alcuna perdita di prestazioni se solo due o tre thread sono attivi in ​​qualsiasi momento. – templatetypedef

+0

@templatetypedef: Quindi in che modo esattamente il sistema operativo determina se i thread devono essere risvegliati in una particolare fascia oraria? Non dovrebbe controllare lo stato del thread in un loop ad ogni fettina? – Mehrdad

1

Ci sono molte ragioni per non farlo. Innanzitutto, come hai notato, questo significa che potrebbe esserci un ampio ritardo tra il tempo in cui si verifica un evento a cui il thread deve rispondere e il tempo di risposta effettivo, poiché il thread potrebbe dormire. In secondo luogo, dal momento che qualsiasi sistema ha solo tanti processori diversi, se devi tenere a calci i thread importanti da un processore in modo che possano dire al thread di andare a dormire un'altra volta, diminuisci la quantità totale di lavoro utile svolto dal sistema e aumentare l'utilizzo energetico del sistema (che è importante in sistemi come telefoni o dispositivi embedded).

0

si introducono anche le condizioni di gara per la classe. se si stesse utilizzando una coda di blocco anziché un elenco normale, il thread si bloccava finché non vi era una nuova voce nell'elenco. Nel tuo caso un secondo thread potrebbe mettere e ottenere un elemento dall'elenco mentre il tuo thread di lavoro sta dormendo e non te ne accorgeresti nemmeno.

0

da aggiungere alle altre risposte, hai anche una condizione di competizione se si dispone di più di un thread rimuovere elementi dalla coda:

  1. coda è vuota
  2. thread A mette un elemento in coda
  3. thread B controlla se la coda è vuota; non è
  4. filo C controlla se la coda è vuota; non è
  5. filo B prende dalla coda; successo
  6. il thread C prende dalla coda; fallimento

Si può gestire questo atomicamente (all'interno di un blocco synchronized) controllando se la coda è vuota e, sse non, prendendo un elemento da esso; ora il ciclo sembra solo un pelo più brutta:

T item; 
while ((item = tryTake(someList)) == null) { 
    try { 
     Thread.currentThread().sleep(100); 
    } 
    catch (InterruptedException e) { 
     // it's almost never a good idea to ignore these; need to handle somehow 
    } 
} 
// Do something with the item 

synchronized private T tryTake(List<? extends T> from) { 
    if (from.isEmpty()) return null; 
    T result = from.remove(0); 
    assert result != null : "list may not contain nulls, which is unfortunate" 
    return result; 
} 

o si potrebbe avere appena usato un BlockingQueue.

+0

Non intendi 'remove (0)'? Presumo che tu non voglia ignorare il primo elemento. –

+0

erm yes, typo, Fixing now. – yshavit

+0

Nella tua descrizione fai riferimento a 'coda' ma nel tuo codice usi un' Elenco'. Questo potrebbe essere fonte di confusione. Potresti usare 'Queue.remove()'; BTW a LinkedList è anche una coda. –

1

Il ciclo è un ottimo esempio di cosa non fare. ;)


Thread.currentThread().sleep(100); 

Non v'è alcuna necessità di ottenere il currentThread() in quanto questo è un metodo statico. E 'lo stesso che

Thread.sleep(100); 

catch (Exception e) {} 

Questo è molto cattiva pratica.Così male che non ti suggerirei di metterlo anche negli esempi, dato che qualcuno potrebbe copiare il codice. Una buona parte delle domande su questo forum verrebbe risolta stampando e leggendo l'eccezione fornita.


You don't need to busy wait here. esp. when you expect to be waiting for such a long time. Busy waiting can make sense if you expect to be waiting a very very short amount of time. e.g. 

// From AtomicInteger 
public final int getAndSet(int newValue) { 
    for (;;) { 
     int current = get(); 
     if (compareAndSet(current, newValue)) 
      return current; 
    } 
} 

Come si può vedere, dovrebbe essere abbastanza raro che questo ciclo ha bisogno di andare in giro più di una volta, e in modo esponenziale meno probabilità di andare in giro molte volte. (In un'applicazione reale, piuttosto che in un micro-benchmark) Questo ciclo potrebbe essere breve come 10 ns, che non è un lungo ritardo.


Potrebbe attendere 99 ms inutilmente. Supponiamo che il produttore aggiunga una voce 1 ms più tardi, che non abbia atteso molto tempo.

La soluzione è più semplice e chiara.

BlockingQueue<E> queue = 

E e = queue.take(); // blocks until an element is ready. 

L'elenco/coda è solo andare a cambiare in un altro thread e un modello molto più semplice per gestire le discussioni e le code è di usare un ExecutorService

ExecutorService es = 

final E e = 
es.submit(new Runnable() { 
    public void run() { 
     doSomethingWith(e); 
    } 
}); 

Come si può vedere, è non è necessario lavorare direttamente con code o thread. Hai solo bisogno di dire cosa vuoi fare il pool di thread.

0

Non riesco ad aggiungere direttamente alle risposte eccellenti fornite da David, templatetypedef ecc. - se si desidera evitare la latenza delle comunicazioni tra thread e lo spreco di risorse, non eseguire le comunicazioni tra thread con i cicli sleep().

preventiva programmazione/dispacciamento:

A livello di CPU, interrupt sono la chiave. Il sistema operativo non fa nulla fino a quando si verifica un'interruzione che causa l'inserimento del suo codice. Si noti che, in termini di sistema operativo, gli interrupt sono disponibili in due versioni: interruzioni hardware "reali" che causano l'esecuzione dei driver e "interrupt software", chiamate di sistemi operativi da thread già in esecuzione che possono potenzialmente causare il set di thread in esecuzione cambiare. Tasti, movimenti del mouse, schede di rete, dischi, errori di pagina generano interruzioni hardware. Le funzioni wait e signal e sleep() appartengono a quella seconda categoria. Quando un interrupt hardware provoca l'esecuzione di un driver, il driver esegue qualsiasi gestione hardware che è stata progettata per eseguire. Se il driver deve segnalare al sistema operativo che alcuni thread devono essere eseguiti, (forse un buffer del disco è ora pieno e deve essere elaborato), il sistema operativo fornisce un meccanismo di ingresso che il guidatore può chiamare invece di eseguire direttamente un interrupt- restituire se stesso, (importante!).

Gli interrupt come gli esempi precedenti possono rendere i thread che erano in attesa pronti per l'esecuzione e/o possono far entrare un thread in esecuzione in uno stato di attesa. Dopo aver elaborato il codice dell'interrupt, il sistema operativo applica i suoi algoritmi di pianificazione per decidere se il set di thread in esecuzione prima dell'interrupt è lo stesso del set che dovrebbe essere ora eseguito. Se lo sono, il sistema operativo solo interrompe: restituisce, in caso contrario, il sistema operativo deve anticipare uno o più thread in esecuzione. Se il sistema operativo deve anticipare un thread in esecuzione su un core della CPU che non è quello che ha gestito l'interrupt, deve ottenere il controllo di quel core della CPU. Lo fa per mezzo di un interrupt hardware "reale": il driver del processore del sistema operativo imposta un segnale hardware che interrompe il core che esegue il thread che deve essere interrotto.

Quando un thread che deve essere inserito entra nel codice del sistema operativo, il sistema operativo può salvare un contesto completo per il thread.Alcuni dei registri saranno già stati salvati nello stack del thread tramite la voce di interrupt e quindi il salvataggio dello stack-pointer del thread consentirà di "salvare" tutti quei registri, ma normalmente il sistema operativo dovrà fare di più, per esempio. potrebbe essere necessario svuotare le cache, potrebbe essere necessario salvare lo stato della FPU e, nel caso in cui il nuovo thread da eseguire appartenga a un processo diverso da quello da preempire, i registri di protezione della gestione della memoria dovranno essere sostituiti . Di solito, il sistema operativo passa dallo stack di thread interrotto a uno stack di sistemi operativi privati ​​il ​​prima possibile per evitare di imporre requisiti di stack di sistema operativo su ogni stack di thread.

Una volta che il/i contesto/i è/sono stati salvati, il SO può "scambiare" il/i contesto/i esteso/i per il/i nuovo/i thread/i da rendere in esecuzione. Ora il sistema operativo può finalmente caricare lo stack-pointer per il nuovo/i thread/i ed eseguire interrupt-return per far funzionare i nuovi thread pronti.

Il sistema operativo quindi non fa nulla. I thread in esecuzione funzionano finché non si verifica un altro interrupt, (hard o soft).

punti importanti:

1) Il kernel del sistema operativo dovrebbe essere guardato come un grande interrupt-handler che può decidere di interrompere ritorno a un diverso insieme di fili di quelli interrotti.

2) Il sistema operativo può ottenere il controllo e interrompere, se necessario, qualsiasi thread in qualsiasi processo, indipendentemente dallo stato in cui si trova o dal centro su cui può essere eseguito.

3) La pianificazione e l'invio preventivi generano tutti i problemi di sincronizzazione, ecc. Che sono pubblicati su questi forum. Il grande vantaggio è la rapida risposta a livello di thread a interruzioni hard. Senza questo, tutte quelle app ad alte prestazioni che si eseguono sul PC - streaming video, reti veloci ecc. Sarebbero praticamente impossibili.

4) Il timer del sistema operativo è solo una delle numerose interruzioni che possono modificare il set di thread in esecuzione. 'Time-slicing', (ugh - odio quel termine), tra thread pronti si verifica solo quando il computer è sovraccarico, vale a dire. l'insieme di thread pronti è maggiore del numero di core CPU disponibili per eseguirli. Se un testo che pretende di spiegare la pianificazione del sistema operativo menziona "time-slicing" prima di "interrupts", è probabile che causi più confusione che spiegazione. L'interruzione del timer è solo "speciale" in quanto molte chiamate di sistema hanno timeout per il backup della loro funzione principale, (OK, per sleep(), il timeout È la funzione primaria :).

Problemi correlati