2012-11-30 14 views
8

mi sono imbattuto in C++ 03 del codice che assume questa forma:le letture integer devono essere protette dalla sezione critica?

struct Foo { 
    int a; 
    int b; 
    CRITICAL_SECTION cs; 
} 

// DoFoo::Foo foo_; 

void DoFoo::Foolish() 
{ 
    if(foo_.a == 4) 
    { 
     PerformSomeTask(); 

     EnterCriticalSection(&foo_.cs); 
     foo_.b = 7; 
     LeaveCriticalSection(&foo_.cs); 
    } 
} 

Condivide la lettura da foo_.a devono essere protetti? es .:

void DoFoo::Foolish() 
{ 
    EnterCriticalSection(&foo_.cs); 
    int a = foo_.a; 
    LeaveCriticalSection(&foo_.cs); 

    if(a == 4) 
    { 
     PerformSomeTask(); 

     EnterCriticalSection(&foo_.cs); 
     foo_.b = 7; 
     LeaveCriticalSection(&foo_.cs); 
    } 
} 

Se sì, perché?

Si supponga che gli interi siano allineati a 32 bit. La piattaforma è ARM.

+2

Si noti inoltre che C++ 11 afferma che qualsiasi condizione di competizione che coinvolge una scrittura come comportamento non definito. Quindi se scrivi a 'foo_.a' in una discussione diversa, allora sì, è UB. (§1.10/4 e §1.10/21) C++ 03 non dice nulla sulla concorrenza. – Mysticial

+0

Usa 'std :: atomic' se puoi e non ti preoccupare. – GManNickG

+0

Vedi la modifica. Questo codice è limitato ai costrutti C++ 03. – PaulH

risposta

9

Tecnicamente sì, ma non su molte piattaforme. Innanzitutto, supponiamo che int sia a 32 bit (che è piuttosto comune, ma non quasi universale).

È possibile che le due parole (16 parti) di bit a 32 bit int verranno letti o scritti separatamente. Su alcuni sistemi, verranno letti separatamente se lo int non è allineato correttamente.

Immaginate un sistema in cui è possibile eseguire solo letture e scritture a 32 bit allineate a 32 bit (e letture e scritture 16 bit allineate a 16 bit) e uno int che si trova a cavallo di tale limite. Inizialmente il int è zero (cioè, 0x00000000)

Un thread scrive 0xBAADF00D al int, l'altro lo legge "allo stesso tempo".

Il thread di scrittura scrive prima 0xBAAD nella parola alta dello int. Il thread del lettore legge quindi l'intero int (sia alto che basso) ottenendo 0xBAAD0000 - che è uno stato in cui lo int non è mai stato inserito di proposito!

Il thread dello scrittore quindi scrive la parola bassa 0xF00D.

Come notato, su alcune piattaforme tutte le letture/scritture a 32 bit sono atomiche, quindi questa non è una preoccupazione. Ci sono altre preoccupazioni, tuttavia.

La maggior parte del codice di blocco/sblocco include istruzioni al compilatore per impedire il riordino attraverso il blocco. Senza quella prevenzione del riordino, il compilatore è libero di riordinare le cose fintanto che si comporta come "se" in un contesto a thread singolo avrebbe funzionato in quel modo. Quindi, se leggete a quindi nel codice, il compilatore potrebbe leggere b prima di leggere a, a condizione che non venga visualizzata un'opportunità nel thread per da modificare in tale intervallo.

Probabilmente il codice che stai leggendo usa questi blocchi per assicurarti che la lettura della variabile avvenga nell'ordine scritto nel codice.

Altri problemi vengono sollevati nei commenti seguenti, ma non mi sento competente a risolverli: problemi di cache e visibilità.

+0

@dmajj, su molte piattaforme, sì, ma non necessariamente su tutte le piattaforme. –

+3

Hai dimenticato di menzionare gli effetti della cache. –

+0

Su tutte le principali architetture CPU attuali (incluso ARM), le letture e le scritture di 'int's allineati naturalmente e i puntatori sono atomici. Si noti che è ancora necessaria la sezione critica sulla scrittura a causa di problemi di ordinamento del compilatore e della CPU. – Cameron

2

Penso che sia possibile utilizzare C++ 11 per garantire che le letture integer siano atomiche, utilizzando (ad esempio) std::atomic<int>.

0

Sebbene l'operazione di lettura/scrittura dei numeri interi sia molto probabilmente atomica, le ottimizzazioni del compilatore e la cache del processore continueranno a fornire problemi se non lo si fa correttamente.

Per spiegare: il compilatore normalmente presupporrà che il codice sia a thread singolo e apporti molte ottimizzazioni basate su questo. Ad esempio, potrebbe cambiare l'ordine delle istruzioni. Oppure, se vede che la variabile è solo scritta e mai letta, potrebbe ottimizzarla completamente.

La CPU memorizzerà anche il numero intero, quindi se un thread lo scrive, l'altro potrebbe non vederlo molto tempo dopo.

Ci sono due cose che puoi fare. Uno è quello di avvolgere in una sezione critica come nel tuo codice originale. L'altro è per contrassegnare la variabile come volatile. Questo segnalerà al compilatore che questa variabile sarà accessibile da più thread e disabiliterà un intervallo di ottimizzazioni, oltre a mettere speciali istruzioni cache-sync (dette anche "barriere della memoria") intorno agli accessi alla variabile (o almeno così capisco). Apparentemente questo è sbagliato.

Aggiunto: Inoltre, come notato da un'altra risposta, Windows ha Interlocked API che possono essere utilizzate per evitare questi problemi per non volatile variabili.

+0

Secondo molti, contrassegnare una variabile come "volatile" non era realmente intesa per le variabili che potrebbero essere modificate su un altro thread, sebbene la documentazione Microsoft per volatile ne suggerisca l'esistenza, e la domanda viene codificata con WinAPI. –

+0

@AdrianMcCarthy - Beh, non sono un esperto. :) So solo che questo è l'unico uso di "volatile" a cui riesco a pensare. –

+1

Ci scusiamo per -1, ma non abbiamo bisogno di diffondere la bugia secondo cui "volatile" non ha più nulla a che fare con il multithreading. Se così fosse, C++ 11 non avrebbe aggiunto 'std :: atomic'. ** Taglia qualsiasi idea che abbia di volatile è utile per il multithreading. ** (Disclaimer: alcuni compilatori, in particolare MSVC, hanno fornito estensioni deprecate a volatile che rendono falsa la mia precedente affermazione basata sulla lingua: questo è stato visto a posteriori come un errore .) – GManNickG

3

Guardando a this sembra che il braccio abbia un modello di memoria abbastanza rilassato quindi è necessaria una forma di barriera di memoria per garantire che le scritture in un thread siano visibili quando ci si aspetterebbe che fossero in un altro thread. Quindi, quello che stai facendo, altrimenti usare std :: atomic sembra probabilmente necessario sulla tua piattaforma. A meno che non si tenga conto di questo, è possibile visualizzare gli aggiornamenti in ordine in diversi thread che potrebbero inficiare il tuo esempio.

2

Il C++ standard di dice che c'è una gara di dati se un thread scrive ad una variabile al tempo stesso come un altro thread legge da quella variabile, o se due thread scrivono alla stessa variabile, allo stesso tempo. Inoltre afferma che una corsa di dati produce un comportamento indefinito. Quindi, formalmente, è necessario che sincronizzi tali letture e scritture con.

Ci sono tre problemi separati quando un thread legge dati scritti da un altro thread. In primo luogo, c'è lacerazione: se la scrittura richiede più di un ciclo di bus, è possibile che un interruttore di thread si verifichi nel mezzo dell'operazione e un altro thread potrebbe visualizzare un valore semigrafico; c'è un problema analogo se una lettura richiede più di un singolo ciclo di bus. In secondo luogo, c'è visibilità: ogni processore ha una propria copia locale dei dati su cui ha lavorato di recente, e la scrittura nella cache di un processore non necessariamente aggiorna la cache di un altro processore. Terzo, ci sono le ottimizzazioni del compilatore che riordina le letture e le scritture in modi che vadano bene all'interno di un singolo thread, ma interromperà il codice multi-thread. Il codice thread-safe deve trattare con tutti e tre i problemi. Questo è il lavoro delle primitive di sincronizzazione: mutex, variabili di condizione e atomica.