Quanto costa l'istruzione lock?


111

Ho sperimentato il multi threading e l'elaborazione parallela e avevo bisogno di un contatore per eseguire un conteggio di base e un'analisi statistica della velocità dell'elaborazione. Per evitare problemi con l'uso simultaneo della mia classe ho usato un'istruzione lock su una variabile privata nella mia classe:

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

Ma mi chiedevo ... quanto costa bloccare una variabile? Quali sono gli effetti negativi sulla performance?


10
Bloccare la variabile non è così costoso; è l'attesa su una variabile bloccata che vuoi evitare.
Gabe

53
è molto meno costoso che passare ore a rintracciare un'altra condizione di gara ;-)
BrokenGlass

2
Beh ... se un lucchetto è costoso, potresti volerlo evitare modificando la programmazione in modo che abbia bisogno di meno serrature. Potrei implementare una sorta di sincronizzazione.
Kees C. Bakker

1
Ho avuto un notevole miglioramento delle prestazioni (in questo momento, dopo aver letto il commento di @Gabe) semplicemente spostando molto codice dai miei blocchi di blocco. Bottomline: d'ora in poi lascerò solo la variabile access (solitamente una riga) all'interno di un blocco di blocco, una sorta di "blocco just in time". Ha senso?
heltonbiker

2
@heltonbiker Ovviamente ha senso. Dovrebbe essere anche un principio architettonico, dovresti rendere le serrature il più corte, semplici e veloci possibile. Solo i dati realmente necessari che devono essere sincronizzati. Sulle server box, dovresti anche prendere in considerazione la natura ibrida della serratura. La contesa, anche se non critica per il codice, è dovuta alla natura ibrida del blocco che causa la rotazione dei core durante ogni accesso se il blocco è tenuto da qualcun altro. Stai effettivamente divorando alcune risorse della CPU da altri servizi sul server per un po 'di tempo prima che il tuo thread venga sospeso.
ipavlu

Risposte:


86

Ecco un articolo che va nel costo. La risposta breve è 50 ns.


39
Risposta migliore breve: 50ns + tempo trascorso in attesa se l'altro thread tiene il blocco.
Herman

4
Più thread entrano e escono dal blocco, più costoso diventa. Il costo si espande in modo esponenziale con il numero di thread
Arsen Zahray

16
Qualche contesto: la divisione di due numeri su un 3Ghz x86 richiede circa 10ns (escluso il tempo necessario per recuperare / decodificare l'istruzione) ; e caricare una singola variabile dalla memoria (non memorizzata nella cache) in un registro richiede circa 40 ns. Quindi 50 ns è follemente, incredibilmente veloce: non dovresti preoccuparti del costo dell'utilizzo lockpiù di quanto ti preoccuperesti del costo dell'utilizzo di una variabile.
BlueRaja - Danny Pflughoeft,

3
Inoltre, quell'articolo era vecchio quando è stata posta questa domanda.
Otis

3
Metrica davvero eccezionale, "quasi senza costi", per non parlare di quella errata. Ragazzi, non prendete in considerazione che è breve e veloce solo e SOLO se non ci sono contese, un filo. IN TALE CASO, NON HAI NECESSARIO IL BLOCCO. Secondo problema, il blocco non è un blocco, ma un blocco ibrido, rileva all'interno di CLR che il blocco non è detenuto da nessuno in base a operazioni atomiche e in tal caso evita le chiamate al core del sistema operativo, cioè un anello diverso che non viene misurato da questi test. Ciò che viene misurato come da 25 ns a 50 ns è in realtà il codice di istruzioni interbloccato a livello di applicazione se il blocco non viene preso
ipavlu

50

La risposta tecnica è che questo è impossibile da quantificare, dipende fortemente dallo stato dei buffer di write-back della memoria della CPU e dalla quantità di dati raccolti dal prefetcher che devono essere scartati e riletti. Che sono entrambi molto non deterministici. Uso 150 cicli della CPU come un'approssimazione all'indietro che evita grosse delusioni.

La risposta pratica è che è mooolto più economico del tempo che impiegherai per eseguire il debug del codice quando pensi di poter saltare un blocco.

Per ottenere un numero difficile dovrai misurare. Visual Studio ha un ottimo analizzatore di concorrenza disponibile come estensione.


1
In realtà no, può essere quantificato e misurato. Semplicemente non è così facile come scrivere quei blocchi tutto intorno al codice, quindi affermare che sono tutti solo 50 ns, un mito misurato sull'accesso a thread singolo al blocco.
ipavlu

8
"penso di poter saltare una serratura" ... Penso che sia lì che si trovano molte persone quando leggono questa domanda ...
Snoop

30

Ulteriore lettura:

Vorrei presentare alcuni dei miei articoli, che sono interessati alle primitive di sincronizzazione generali e stanno scavando in Monitor, comportamento dell'istruzione di blocco C #, proprietà e costi a seconda di scenari distinti e numero di thread. È specificamente interessato allo spreco di CPU e ai periodi di throughput per capire quanto lavoro può essere eseguito in più scenari:

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https: // www. codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

Risposta originale:

Oh caro!

Sembra che la risposta corretta contrassegnata qui come LA RISPOSTA sia intrinsecamente errata! Vorrei chiedere all'autore della risposta, rispettosamente, di leggere l'articolo collegato fino alla fine. articolo

L'autore di questo articolo a partire dal 2003 l'articolo stava misurando il unica macchina Dual Core e nel primo caso di misura, ha misurato bloccaggio con un solo filo e il risultato era di circa 50 ns per l'accesso serratura.

Non dice nulla su un blocco nell'ambiente simultaneo. Quindi dobbiamo continuare a leggere l'articolo e nella seconda metà, l'autore stava misurando lo scenario di blocco con due e tre thread, che si avvicina ai livelli di concorrenza dei processori odierni.

Quindi l'autore dice che con due thread su Dual Core, i blocchi costano 120 ns e con 3 thread si arriva a 180 ns. Quindi sembra essere chiaramente dipendente dal numero di thread che accedono al blocco contemporaneamente.

Quindi è semplice, non è 50 ns a meno che non sia un thread singolo, dove il blocco diventa inutile.

Un altro aspetto da considerare è che viene misurato come tempo medio !

Se il tempo delle iterazioni venisse misurato, ci sarebbero anche tempi compresi tra 1 ms e 20 ms, semplicemente perché la maggior parte era veloce, ma pochi thread aspetteranno il tempo dei processori e subiranno ritardi anche millisecondi.

Questa è una cattiva notizia per qualsiasi tipo di applicazione che richiede un throughput elevato e una bassa latenza.

E l'ultimo problema da considerare è che potrebbero esserci operazioni più lente all'interno della serratura e molto spesso è così. Più a lungo viene eseguito il blocco di codice all'interno della serratura, maggiore è la contesa e i ritardi aumentano alle stelle.

Si prega di considerare che è già passato più di un decennio dal 2003, ovvero poche generazioni di processori progettati specificamente per funzionare completamente contemporaneamente e il blocco sta danneggiando considerevolmente le loro prestazioni.


1
Per chiarire, l'articolo non dice che le prestazioni del blocco peggiorano con il numero di thread nell'applicazione; le prestazioni peggiorano con il numero di thread che si contendono il blocco. (Questo è implicito, ma non chiaramente indicato, nella risposta sopra.)
Uva spina

Presumo che tu intenda questo: "Quindi sembra essere chiaramente dipendente dal numero di thread a cui si accede simultaneamente e più è peggio". Sì, la formulazione potrebbe essere migliore. Intendevo "accesso simultaneo" come thread che accedono contemporaneamente al blocco, creando così contese.
ipavlu

20

Questo non risponde alla tua domanda sulle prestazioni, ma posso dire che .NET Framework offre un Interlocked.Addmetodo che ti consentirà di aggiungerlo amountal tuo donemembro senza bloccare manualmente un altro oggetto.


1
Sì, questa è probabilmente la risposta migliore. Ma principalmente per motivi di codice più breve e più pulito. È improbabile che la differenza di velocità sia evidente.
Henk Holterman

grazie per questa risposta. Sto facendo più cose con le serrature. Gli int aggiunti sono uno dei tanti. Adoro il suggerimento, lo userò d'ora in poi.
Kees C. Bakker

i blocchi sono molto, molto più facili da correggere, anche se il codice senza blocco è potenzialmente più veloce. Interlocked.Add da solo ha gli stessi problemi di + = senza sincronizzazione.
hangar

10

lock (Monitor.Enter / Exit) è molto economico, più economico di alternative come Waithandle o Mutex.

Ma se fosse (un po ') lento, preferiresti avere un programma veloce con risultati errati?


5
Haha ... stavo andando per il programma veloce e i buoni risultati.
Kees C. Bakker

@ henk-holterman Ci sono più problemi con le tue affermazioni: in primo luogo, come questa domanda e le risposte hanno mostrato chiaramente, c'è una scarsa comprensione dell'impatto del blocco sulle prestazioni complessive, anche le persone che affermano il mito di 50 ns che è applicabile solo con l'ambiente a thread singolo. Secondo, la tua affermazione è qui e rimarrà per anni e nel frattempo, i processori sono cresciuti in core, ma la velocità dei core non è così tanto. ** Le applicazioni Thrid ** diventano solo più complesse nel tempo, e poi sono strato su strato di blocco nell'ambiente di molti core e il numero è in aumento, 2,4,8,10,20,16,32
ipavlu

Il mio approccio abituale è costruire la sincronizzazione in modo debolmente accoppiato con la minor interazione possibile. Questo va molto velocemente alle strutture di dati prive di blocco. Ho creato per i miei wrapper di codice attorno a spinlock per semplificare lo sviluppo e anche quando TPL ha raccolte simultanee speciali, ho sviluppato raccolte bloccate di rotazione delle mie attorno a elenco, array, dizionario e coda, poiché avevo bisogno di un po 'più di controllo e talvolta del codice in esecuzione sotto spinlock. Posso dirti che è possibile e permette di risolvere molteplici scenari che le collezioni TPL non possono fare e con grande guadagno di prestazioni / throughput.
ipavlu

7

Il costo per una serratura in un anello stretto, rispetto a un'alternativa senza serratura, è enorme. Puoi permetterti di eseguire il loop molte volte ed essere ancora più efficiente di un lucchetto. Ecco perché le code senza blocco sono così efficienti.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            {
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: {0}", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: {0}", stopwatch.ElapsedMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

Produzione:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208

4
Questo potrebbe essere un cattivo esempio perché il tuo ciclo in realtà non fa nulla, a parte l'assegnazione di una singola variabile e un blocco è di almeno 2 chiamate di funzione. Inoltre, 20 ns per serratura che stai ricevendo non è poi così male.
Zar Shardan

5

Esistono diversi modi per definire il "costo". C'è l'effettivo sovraccarico di ottenere e rilasciare il blocco; come scrive Jake, è trascurabile a meno che questa operazione non venga eseguita milioni di volte.

Di più rilevanza è l'effetto che questo ha sul flusso di esecuzione. Questo codice può essere inserito solo da un thread alla volta. Se si dispone di 5 thread che eseguono questa operazione su base regolare, 4 di loro finiranno per attendere il rilascio del blocco e quindi per essere il primo thread programmato per immettere quella parte di codice dopo che il blocco è stato rilasciato. Quindi, il tuo algoritmo soffrirà in modo significativo. Quanto dipende dall'algoritmo e dalla frequenza con cui viene chiamata l'operazione .. Non puoi davvero evitarlo senza introdurre condizioni di gara, ma puoi migliorarlo riducendo al minimo il numero di chiamate al codice bloccato.

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.