2012-03-01 23 views
7

Sto tentando di scrivere un metodo thread-safe che può essere chiamato solo una volta (per istanza di oggetto). Un'eccezione dovrebbe essere lanciata se è stata chiamata prima.Procedura: scrivere un metodo thread-safe che può essere chiamato una sola volta?

Ho trovato due soluzioni. Sono entrambi corretti? In caso contrario, cosa c'è di sbagliato in loro?

  1. Con lock:

    public void Foo() 
    { 
        lock (fooLock) 
        { 
         if (fooCalled) throw new InvalidOperationException(); 
         fooCalled = true; 
        } 
        … 
    } 
    private object fooLock = new object(); 
    private bool fooCalled; 
    
  2. Con Interlocked.CompareExchange:

    public void Foo() 
    { 
        if (Interlocked.CompareExchange(ref fooCalled, 1, 0) == 1) 
         throw new InvalidOperationException(); 
        … 
    } 
    private int fooCalled; 
    

    Se non mi sbaglio, questa soluzione ha il vantaggio di essere senza blocchi (che sembra irrilevante nel mio caso) e che richiede meno campi privati .

io sono aperto anche a pareri motivati ​​quale soluzione dovrebbe essere preferito, e per ulteriori suggerimenti se c'è un modo migliore.

risposta

5

La soluzione Interlocked.CompareExchange sembra il migliore, e (come hai detto) è senza blocchi. È anche significativamente meno complicato rispetto ad altre soluzioni. I blocchi sono piuttosto pesanti, mentre lo CompareExchange può essere compilato in un'unica istruzione CAS cpu. Dico andare con quello.

+0

Per curiosità: quando dici che è "meno complicato", ti sembra di riferirti a tutto ciò che accade dietro i bui; come giudichi la leggibilità/facilità di comprensione del costrutto 'Interlocked.ExchangeCompare' per un programmatore medio? – stakx

+1

@stakx: ecco a cosa servono i commenti. Quando un programmatore incontra qualcosa che non capisce, dovrebbe cercarlo in modo che lo capisca. È così che diventano un programmatore migliore. – thecoop

+0

@thecoop Non sono d'accordo, sì è la soluzione più corretta, ma non è semplice, è necessario conoscere le operazioni atomiche ecc. Questo è in qualche modo un processo di inizializzazione e consiglierei di seguire i modelli di inizializzazione che sono ampiamente utilizzati (es. blocco selezionato). Inoltre, questi schemi ti impediscono di perdere qualcosa che può facilmente accadere quando si fa il threading. – ntziolis

0

Il doppio scalpiccio di blocco controllato è quello che stai cercando:

Questo è quello che stai cercando:

class Foo 
{ 
    private object someLock = new object(); 
    private object someFlag = false; 


    void SomeMethod() 
    { 
    // to prevent locking on subsequent calls   
    if(someFlag) 
     throw new Exception(); 

    // to make sure only one thread can change the contents of someFlag    
    lock(someLock) 
    { 
     if(someFlag) 
     throw new Exception(); 

     someFlag = true;      
    } 

    //execute your code 
    } 
} 

In generale, quando esposti a questioni come queste provare e seguire ben sapete schemi come quello sopra.
Questo lo rende riconoscibile e meno incline agli errori poiché è meno probabile che manchi qualcosa quando si segue un modello, specialmente quando si tratta di eseguire il threading.
Nel tuo caso il primo se non ha molto senso, spesso vorrai eseguire la logica attuale e quindi impostare il flag. Un secondo thread verrebbe bloccato mentre si sta eseguendo il codice (forse piuttosto costoso).

Circa il secondo esempio:
Sì, è corretto, ma non renderlo più complicato di quello che è. Dovresti avere ottime ragioni per non usare il semplice locking e in questa situazione rende il codice più complicato (perché Interlocked.CompareExchange() è meno conosciuto) senza ottenere nulla (come hai sottolineato essere meno lock contro locking per impostare un flag booleano in realtà non è un vantaggio in questo caso).

+0

** 1. ** Questo sembra rendere lettura/scrittura 'someFlag' non atomico. Sei sicuro che sia corretto? ** 2. ** E la soluzione basata su 'Interlocked.CompareExchange'? – stakx

+0

sry ha dimenticato la riga più importante – ntziolis

+0

Hmm ... sta bloccando un problema di questo tipo se hai intenzione di lanciare un'eccezione, comunque? – stakx

-1
Task task = new Task((Action)(() => { Console.WriteLine("Called!"); })); 
    public void Foo() 
    { 
     task.Start(); 
    } 

    public void Bar() 
    { 
     Foo(); 
     Foo();//this line will throws different exceptions depends on 
       //whether task in progress or task has already been completed 
    }  
+1

Mi dispiace dire che questo non risponde alla domanda. (A proposito, sono a conoscenza della Task Parallel Library, ma non può essere utilizzata ovunque, ad esempio dove il metodo in questione implementa un metodo di interfaccia.) – stakx

+1

corretto in base al tuo caso – pamidur

+0

@pamidur stai provando a risolvere un altro problema – ntziolis