2009-09-09 1 views
18

Apprezzo pienamente l'atomicità fornita dalla classe Threading.Interlocked; Non capisco, però, perché la funzione Aggiungi offre solo due sovraccarichi: uno per i numeri interi, un altro per i contatti lunghi. Perché non raddoppia, o qualsiasi altro tipo numerico per quella materia?Perché non c'è sovraccarico di Interlocked.Add che accetta Doubles come parametri?

Chiaramente, il metodo previsto per la modifica di un Double è CompareExchange; Sto supponendo che questo è dovuto al fatto che la modifica di un Double è un'operazione più complessa della modifica di un Integer. Tuttavia non mi è chiaro il motivo per cui, se CompareExchange e Add possono entrambi accettare numeri interi, non possono anche accettare entrambi i doppi.

risposta

24

Gli impacchi classe Interlocked intorno alle API di Windows funzioni ** intrecciata.

Questi sono, a loro volta, avvolgono l'API del processore nativo, utilizzando il prefisso di istruzioni LOCK per x86. Supporta solo anteponendo le seguenti istruzioni:

BT, BTS, BTR, BTC, XCHG, xadd, aggiungere o, ADC, FFS, E, SUB, XOR, NOT, NEG, INC, dicembre

Noterete che questi, a loro volta, sono praticamente mappati ai metodi interbloccati. Sfortunatamente, le funzioni ADD per tipi non interi non sono supportate qui. Aggiungi per lunghi 64 bit è supportato su piattaforme a 64 bit.

Ecco un ottimo articolo discussing lock semantics on the instruction level.

+2

Il link non è più attivo. Ecco un backup da Internet Archive https://web.archive.org/web/20160319061137/http://www.codemaestro.com/reviews/8 –

-1

Come Adam Robinson ha sottolineato, v'è un sovraccarico per Interlocked.Increment che richiede un Int64, ma nota:

Il metodo Read e il 64-bit overload del incremento, decremento, e aggiungere metodi sono veramente atomici solo su sistemi in cui System.IntPtr è lungo 64 bit. Su altri sistemi, questi metodi sono atomici rispetto allo tra loro, ma non rispetto a altri modi per accedere ai dati. Pertanto, per essere thread-safe sui sistemi a 32 bit, qualsiasi accesso a un valore a 64 bit deve essere effettuato tramite i membri di della classe Interlocked.

1

Ho il sospetto che ci siano due ragioni.

  1. I processori interessati da .Net supportano l'incremento interbloccato solo per i tipi interi. Credo che questo sia il prefisso LOCK su x86, probabilmente esistono istruzioni simili per altri processori.
  2. Aggiungendo uno a un numero in virgola mobile si può ottenere lo stesso numero se è abbastanza grande, quindi non sono sicuro se è possibile chiamare tale incremento. Forse i progettisti del framework stanno cercando di evitare comportamenti non intuitivi in ​​questo caso.
+0

Per quanto riguarda il secondo punto: sì, ma sto chiedendo di Interlocked.Add, not Interlocked.Increment. –

7

Come ha detto Reed Copsey, la mappa delle operazioni interbloccate (tramite le funzioni API di Windows) alle istruzioni supportate direttamente dai processori x86/x64. Dato che una di queste funzioni è XCHG, è possibile eseguire un'operazione XCHG atomica senza preoccuparsi realmente di ciò che rappresentano i bit nella posizione di destinazione. In altre parole, il codice può "fingere" che il numero in virgola mobile a 64 bit che si sta scambiando sia in realtà un numero intero a 64 bit e che l'istruzione XCHG non conosca la differenza. Pertanto, .Net può fornire Interlocked.Le funzioni di scambio per float e doubles "fingendo" sono rispettivamente numeri interi e interi lunghi.

Tuttavia, tutte le altre operazioni effettivamente fare operare sui singoli bit della destinazione, e quindi non funziona a meno che i valori effettivamente rappresentano numeri interi (o array di bit in alcuni casi.)

21

Altri hanno indirizzato al "perché?". E 'facile comunque a rotolare il proprio Add(ref double, double), utilizzando il CompareExchange primitiva:

public static double Add(ref double location1, double value) 
{ 
    double newCurrentValue = location1; // non-volatile read, so may be stale 
    while (true) 
    { 
     double currentValue = newCurrentValue; 
     double newValue = currentValue + value; 
     newCurrentValue = Interlocked.CompareExchange(ref location1, newValue, currentValue); 
     if (newCurrentValue == currentValue) 
      return newValue; 
    } 
} 

CompareExchange imposta il valore di location1 essere newValue, se il valore corrente è uguale a currentValue. Così facendo in modo atomico e sicuro per i thread, possiamo fare affidamento solo su di esso senza ricorrere alle serrature.

Perché il ciclo while (true)? I cicli come questo sono standard quando si implementano algoritmi concorrentemente ottimistici. CompareExchange non cambierà location1 se il valore corrente è diverso da currentValue. Ho inizializzato currentValue su location1 - facendo una lettura non volatile (che potrebbe essere stantia, ma che non cambia la correttezza, come CompareExchange controllerà il valore). Se il valore corrente (ancora) è quello che abbiamo letto da location, CompareExchange cambierà il valore in newValue. In caso contrario, è necessario riprovare CompareExchange con il nuovo valore corrente, come restituito da CompareExchange.

Se il valore viene modificato da un altro thread fino al momento del nostro prossimo CompareExchange, fallirà di nuovo, rendendo necessario un altro tentativo, e questo in teoria può andare avanti per sempre, da qui il ciclo. A meno che non si modifichi costantemente il valore da più thread, è probabile che il numero CompareExchange venga chiamato una sola volta, se il valore corrente è ancora quello restituito dalla lettura non volatile di location1 o due volte se era diverso.

+0

Puoi spiegare un po 'di più come funziona? Perché fai un ciclo? E qual è il ruolo di CompareExchange? – Mzn

+1

@Mzn Aggiunto alla mia risposta. Spero che lo spieghi. –

+2

Questa è ora una risposta eccellente! Almeno ora penso di sapere che cosa significhi una concorrenza ottimistica: "Scrivere un codice concorrente con l'assunto che le cose andranno bene". In caso di nostro (vero) questo significa che non ci fermeremo per sempre (siamo ragionevolmente ottimisti). Molte grazie! – Mzn