2013-02-24 12 views
25

Per qualcosa di semplice come un contatore se più thread aumenteranno il numero. Ho letto che i blocchi mutex possono ridurre l'efficienza poiché i thread devono attendere. Quindi, per me, un contatore atomico sarebbe il più efficiente, ma ho letto che internamente è fondamentalmente un lucchetto? Quindi credo di essere confuso su come uno potrebbe essere più efficiente dell'altro.Quale è il più efficace, blocco mutex di base o numero intero atomico?

+0

Questa risposta dovrebbe essere per tutte le piattaforme e linguaggi di programmazione che supportano pthreads o qualche sottoinsieme? Non capisco completamente le relazioni tra pthreads, sistemi operativi e linguaggi di programmazione, ma sembra che queste relazioni potrebbero essere rilevanti. –

risposta

13

supporto operazioni atomica processore leva finanziaria (confrontare e istruzioni di swap) e non utilizzare i blocchi a tutti, mentre le serrature sono più dipende dal sistema operativo ed eseguire in modo diverso su, per esempio, Win e Linux.

I blocchi sospendono effettivamente l'esecuzione del thread, liberando risorse cpu per altre attività, ma si verificano in ovvi cambiamenti di contesto durante l'arresto/riavvio del thread. Al contrario, i thread che tentano di eseguire operazioni atomiche non attendono e continuano a tentare fino al successo (il cosiddetto busy-waiting), quindi non incorrono in un overhead di commutazione di contesto, ma non liberano le risorse della CPU.

Riassumendo, in generale le operazioni atomiche sono più veloci se il conflitto tra i fili è sufficientemente basso. Dovresti assolutamente fare benchmark in quanto non esiste un altro metodo affidabile per sapere qual è il più basso overhead tra commutazione di contesto e attesa.

16

Se si dispone di un contatore per il quale sono supportate le operazioni atomiche, sarà più efficiente di un mutex.

Tecnicamente, l'atomico bloccherà il bus di memoria sulla maggior parte delle piattaforme. Tuttavia, ci sono due dettagli di miglioramento:

  • È impossibile sospendere un thread durante il blocco del bus di memoria, ma è possibile sospendere un thread durante un blocco di mutex. Questo è ciò che ti permette di ottenere una garanzia senza blocco (che non dice nulla sul non blocco - garantisce solo che almeno un thread faccia progressi).
  • Alla fine i mutex vengono implementati con l'atomica. Poiché è necessario almeno un'operazione atomica per bloccare un mutex e un'operazione atomica per sbloccare un mutex, è necessario almeno due volte per eseguire un blocco mutex, anche nel migliore dei casi.
+0

È importante capire che dipende dal modo in cui il compilatore o l'interprete supporta la piattaforma per generare le migliori istruzioni della macchina (in questo caso istruzioni senza blocco) per la piattaforma. Penso che questo sia ciò che @Cort Ammon intende per "supportato". Inoltre alcuni mutex potrebbero fornire garanzie sui progressi in avanti o sull'equità per alcuni o tutti i thread che non sono fatti da semplici istruzioni atomiche. –

1

intero atomica è una modalità utente oggetto lì per è molto più efficiente di un mutex che viene eseguito in modalità kernel . L'ambito del numero intero atomico è una singola applicazione, mentre l'ambito del mutex è per tutti i software in esecuzione sulla macchina.

+0

Questo è quasi vero. Le moderne implementazioni mutex, come il Futex di Linux, tendono a sfruttare le operazioni atomiche per evitare il passaggio alla modalità kernel sul percorso veloce. Tali mutex devono solo passare alla modalità kernel se l'operazione atomica non riesce a eseguire l'operazione desiderata (come nel caso in cui il thread deve bloccare). –

1

Mutex è una semantica a livello del kernel che fornisce l'esclusione reciproca anche allo Process level. Si noti che può essere utile per estendere l'esclusione reciproca oltre i limiti del processo e non solo all'interno di un processo (per i thread). È più costoso.

Il contatore atomico, AtomicInteger, ad esempio, è basato su CAS e in genere tenta di eseguire un'operazione fino al suo esito positivo. Fondamentalmente, in questo caso, i thread corrono o competono per incrementare \ decrementare il valore atomicamente. Qui, potresti vedere dei buoni cicli della CPU usati da un thread che tenta di operare su un valore corrente.

Poiché si desidera mantenere il contatore, AtomicInteger \ AtomicLong sarà il migliore per il proprio caso d'uso.

1

La maggior parte dei processori ha supportato una lettura o scrittura atomica e spesso uno scambio atomico di cmp &. Ciò significa che il processore stesso scrive o legge il valore più recente in una singola operazione e potrebbero esserci alcuni cicli persi rispetto a un normale accesso intero, specialmente perché il compilatore non può ottimizzare le operazioni atomiche quasi come al solito.

D'altra parte un mutex è un numero di righe di codice da inserire e partire, e durante l'esecuzione altri processori che accedono alla stessa posizione sono completamente in stallo, quindi chiaramente un grande sovraccarico su di essi. Nel codice di alto livello non ottimizzato, il mutex entra/esce e l'atomico sarà chiamate di funzione, ma per mutex, qualsiasi processore concorrente verrà bloccato mentre ritorna la funzione di mutex enter e mentre viene avviata la funzione di uscita. Per atomico, è solo la durata dell'operazione effettiva che viene bloccata. L'ottimizzazione dovrebbe ridurre quel costo, ma non tutto.

Se si sta tentando di incrementare, il processore moderno probabilmente supporta l'incremento/decremento atomico, il che sarà grande.

In caso contrario, viene implementato utilizzando il processore atomico cmp & o utilizzando un mutex.

Mutex:

get the lock 
read 
increment 
write 
release the lock 

atomica cmp & swap:

atomic read the value 
calc the increment 
do{ 
    atomic cmpswap value, increment 
    recalc the increment 
}while the cmp&swap did not see the expected value 

Quindi questa seconda versione ha un ciclo [incassa un altro processore incrementa il valore tra le nostre operazioni atomiche, quindi il valore non corrisponde più, e l'incremento sarebbe sbagliato] che può diventare lungo [se ci sono molti concorrenti], ma in generale dovrebbe essere ancora più veloce della versione mutex, ma la versione mutex potrebbe consentire al processore di passare da un'attività all'altra.

3

Un minimo (conforme agli standard) implementazione mutex richiede 2 ingredienti di base:

  • Un modo per trasmettere atomicamente un cambiamento di stato tra i thread (lo stato 'bloccato')
  • barriere di memoria per far rispettare le operazioni di memoria protetta dal mutex per rimanere all'interno dell'area protetta.

Non c'è modo di renderlo più semplice di così, a causa della relazione "sincronizza-con" richiesta dallo standard C++.

Una minima (corretta) implementazione potrebbe essere simile a questo:

class mutex { 
    std::atomic<bool> flag{false}; 

public: 
    void lock() 
    { 
     while (flag.exchange(true, std::memory_order_relaxed)); 
     std::atomic_thread_fence(std::memory_order_acquire); 
    } 

    void unlock() 
    { 
     std::atomic_thread_fence(std::memory_order_release); 
     flag.store(false, std::memory_order_relaxed); 
    } 
}; 

Grazie alla sua semplicità (che non può sospendere l'thread di esecuzione), è probabile che, in condizioni di scarsa contesa, questa implementazione supera un std::mutex . Ma anche allora, è facile vedere che ogni incremento intero, protetto da questo mutex, richiede le seguenti operazioni:

  • un atomic negozio per rilasciare il mutex
  • un atomic confrontare-e-swap (leggi -modify-scrittura) per acquisire il mutex (possibilmente più volte)
  • un incremento intero

Se si confronta che con un standalone std::atomic<int> che viene incrementato con un singolo (incondizionata) leggere -modificare-scrivere (es. fetch_add), è ragionevole aspettarsi che un'operazione atomica (utilizzando lo stesso modello di ordinamento) superi il caso in cui viene utilizzato un mutex.