In che modo questa implementazione pseudo-RAII consente un blocco con scope in C #?

2

Per i programmi di concorrenza che ho scritto in C #, i miei blocchi / sincronizzazione tendono a seguire questo schema:

try
{
    Monitor.Enter(locker);

    // critical region
}
finally
{
    Monitor.Exit(locker);
}

Questo è uno schema classico che ho scoperto in uno dei libri C # di Paul Deitel anni fa e da allora lo sto seguendo. Questo ha funzionato molto bene per molte applicazioni che ho richiesto. Tuttavia, recentemente ho avuto una discussione con un altro sviluppatore sulla concorrenza e mi hanno chiesto ... perché lo stai facendo in quel modo quando potresti usare "serrature con scope".

A questo punto ammetterò che non avevo idea di cosa fossero. Ho iniziato a scavare per capire se questo potesse essere utile per scrivere le mie applicazioni. Nella mia ricerca ho scoperto questo pattern RAII (l'acquisizione delle risorse è in fase di inizializzazione), che alla mia comprensione consente di bloccare gli ambiti. Immagino che questo pattern sia usato principalmente in C ++ ma ci sono alcune pseudo-implementazioni in C # come segue (tratto da L'acquisizione delle risorse è inizializzazione in C # ):

using System;

namespace RAII
{
    public class DisposableDelegate : IDisposable
    {
        private Action dispose;

        public DisposableDelegate(Action dispose)
        {
            if (dispose == null)
            {
                throw new ArgumentNullException("dispose");
            }

            this.dispose = dispose;
        }

        public void Dispose()
        {
            if (this.dispose != null)
            {
                Action d = this.dispose;
                this.dispose = null;
                d();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.Out.WriteLine("Some resource allocated here.");

            using (new DisposableDelegate(() => Console.Out.WriteLine("Resource deallocated here.")))
            {
                Console.Out.WriteLine("Resource used here.");

                throw new InvalidOperationException("Test for resource leaks.");
            }
        }
    }
}

Sto solo cercando di capire come questo modello produce la sincronizzazione in C # ... La sincronizzazione è inerente a qualcosa che ha a che fare con l'allocazione delle risorse?

Inoltre, non è un modo di abusare della lingua per costringere un utente a usare un costruttore w / using per far funzionare tutto questo? Cosa succede se qualcosa viene cancellato e viene generata un'eccezione in un costruttore? Non vedo dove sia molto meglio del buon vecchio Enter / Exit pattern.

    
posta Snoop 18.06.2018 - 15:08
fonte

2 risposte

2

La tua domanda mi sembra un po 'confusa, dal momento che entrambi i frammenti di codice non sono equivalenti, ma suppongo che quello che volevi dire fosse il seguente: perché qualcuno dovrebbe usare qualcosa di simile

 Monitor.Enter(locker);
 using (new DisposableDelegate(() => Monitor.Exit(locker))
 {
     // critical region
 }

al posto del blocco try/finally della tua domanda?

Questa è davvero una domanda sensata, perché entrambi i modi sono semanticamente equivalenti, e non c'è IMHO che non abbia il vantaggio di reinventare la ruota con una classe di DisposableDelegate .

Naturalmente, quando segui il link dal commento sotto la domanda a cui ti sei collegato, trovi una migliore implementazione del "delegato usa e getta": il ResourceProtector , che consente al tuo codice di scrittura come

 using (new ResourceProtector(()=> Monitor.Enter(locker), () => Monitor.Exit(locker))
 {
     // critical region
 }

Questo ha in effetti un certo vantaggio su try/finally , poiché raggruppa insieme le istruzioni di allocazione / deallocazione corrispondenti. Pertanto, il codice diventa meno incline agli errori, in particolare quando viene utilizzato nel contesto di allocazioni di risorse multiple, altre istruzioni prima e dopo l'allocazione o quando si evolve.

    
risposta data 18.06.2018 - 22:32
fonte
0

I am just trying to understand how this pattern produces synchronization in C#... Is the synchronization inherent to something having to do with resource allocation?

La controvento ( } ) del blocco using è una chiamata implicita a Dispose .

La risorsa è acquisita alla = (inizializzazione) e rilasciata alla fine di tale ambito. Il nome RAII deriva da C ++, in cui gli ambiti demarcano la durata degli oggetti dichiarati al loro interno e il codice nei distruttori viene eseguito, tuttavia l'ambito viene lasciato (ritorno anticipato, eccezione generata, controllo dei flussi dal basso)

Also, isn't it kind of abusing the language to force a user to use a constructor w/using in order for this to work?

Dipende da cosa intendi con "abusare". Questo è l'unico modo in C # di pulire in modo deterministico

What happens if something is cancelled and an exception is thrown in a constructor?

La stessa cosa che succede se un'eccezione viene lanciata nel codice prima di un try . Spero che qualunque cosa sia, è anche sicura. Io trovo (moderno) il C ++ migliore in questo senso, dato che è facile scrivere una classe che cancella correttamente da un'istanza semi-formata throw ing.

I don't see where this is that much better than good old Enter/Exit pattern.

È piuttosto facile definire un avviso per

CA0000 You created an IDisposable (MyClass myObj) and there is a code path that fails to Dispose it

anziché (centinaia di variazioni di)

CA0000 You called myObj.SomeSetup() and there is a code path that doesn't reach myObj.SomeTeardown()

Soprattutto perché non esiste un meccanismo per un autore di MyClass per indicare al compilatore che MyClass.SomeSetup deve essere abbinato a MyClass.Teardown

Nel caso specifico della sincronizzazione su un oggetto, lock statement è più ragionevole, ma potresti scrivere qualcosa come

class ScopedLock : IDisposable
{
    private object obj;
    public ScopedLock(object obj)
    {
        this.obj = obj;
        if (this.obj != null) { Monitor.Enter(obj); }
    }
    public void Dispose()
    {
        if (this.obj != null) { Monitor.Exit(obj); }
        this.obj = null;
    }
}
    
risposta data 18.06.2018 - 17:47
fonte

Leggi altre domande sui tag