Perché lock (this) {…} è male?


484

La documentazione MSDN dice questo

public class SomeObject
{
  public void SomeOperation()
  {
    lock(this)
    {
      //Access instance variables
    }
  }
}

è "un problema se è possibile accedere all'istanza pubblicamente". Mi chiedo perché? È perché il blocco si terrà più a lungo del necessario? O c'è qualche motivo più insidioso?

Risposte:


508

È una cattiva forma da usare thisnelle istruzioni di blocco perché è generalmente fuori dal tuo controllo chi altro potrebbe bloccare quell'oggetto.

Al fine di pianificare correttamente le operazioni parallele, è necessario prestare particolare attenzione a considerare possibili situazioni di deadlock e la presenza di un numero sconosciuto di punti di ingresso di blocco lo ostacola. Ad esempio, chiunque abbia un riferimento all'oggetto può bloccarlo senza che il progettista / creatore di oggetti lo sappia. Ciò aumenta la complessità delle soluzioni multi-thread e potrebbe comprometterne la correttezza.

Un campo privato è di solito un'opzione migliore in quanto il compilatore imporrà restrizioni di accesso ad esso e incapsulerà il meccanismo di blocco. L'uso della thisviolazione incapsula esponendo al pubblico parte dell'implementazione del blocco. Inoltre, non è chiaro che acquisirai un blocco a thismeno che non sia stato documentato. Anche in questo caso, affidarsi alla documentazione per prevenire un problema non è ottimale.

Infine, esiste un malinteso comune che lock(this)modifica effettivamente l'oggetto passato come parametro e in qualche modo lo rende di sola lettura o inaccessibile. Questo è falso . L'oggetto passato come parametro lockserve semplicemente come chiave . Se è già presente un blocco su quella chiave, non è possibile effettuare il blocco; in caso contrario, il blocco è consentito.

Questo è il motivo per cui è male usare le stringhe come chiavi nelle lockistruzioni, dal momento che sono immutabili e sono condivise / accessibili attraverso parti dell'applicazione. Dovresti invece usare una variabile privata, Objectun'istanza andrà bene.

Esegui il seguente codice C # come esempio.

public class Person
{
    public int Age { get; set;  }
    public string Name { get; set; }

    public void LockThis()
    {
        lock (this)
        {
            System.Threading.Thread.Sleep(10000);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var nancy = new Person {Name = "Nancy Drew", Age = 15};
        var a = new Thread(nancy.LockThis);
        a.Start();
        var b = new Thread(Timewarp);
        b.Start(nancy);
        Thread.Sleep(10);
        var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 };
        var c = new Thread(NameChange);
        c.Start(anotherNancy);
        a.Join();
        Console.ReadLine();
    }

    static void Timewarp(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // A lock does not make the object read-only.
        lock (person.Name)
        {
            while (person.Age <= 23)
            {
                // There will be a lock on 'person' due to the LockThis method running in another thread
                if (Monitor.TryEnter(person, 10) == false)
                {
                    Console.WriteLine("'this' person is locked!");
                }
                else Monitor.Exit(person);
                person.Age++;
                if(person.Age == 18)
                {
                    // Changing the 'person.Name' value doesn't change the lock...
                    person.Name = "Nancy Smith";
                }
                Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
            }
        }
    }

    static void NameChange(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // You should avoid locking on strings, since they are immutable.
        if (Monitor.TryEnter(person.Name, 30) == false)
        {
            Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\".");
        }
        else Monitor.Exit(person.Name);

        if (Monitor.TryEnter("Nancy Drew", 30) == false)
        {
            Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
        }
        else Monitor.Exit("Nancy Drew");
        if (Monitor.TryEnter(person.Name, 10000))
        {
            string oldName = person.Name;
            person.Name = "Nancy Callahan";
            Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
        }
        else Monitor.Exit(person.Name);
    }
}

Uscita console

'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.

2
Mentre grok: (1) Nancy è nel thread1 con lock (questo). (2) SAME Nancy sta invecchiando thread2 mentre è ancora bloccato in thread1 - dimostrando che un oggetto bloccato non è di sola lettura. ANCHE (2a) mentre nel thread 2, anche questo oggetto Nancy è bloccato su Name. (3) Creare un oggetto DIVERSO con lo stesso nome . (4) Passa a thread3 e prova a bloccare con Nome. (finitura grande) MA "le stringhe sono immutabili", il che significa che qualsiasi oggetto che fa riferimento alla stringa "Nancy Drew" sta letteralmente guardando la stessa istanza di stringa in memoria. Quindi object2 non può ottenere un blocco su una stringa quando object1 è bloccato sullo stesso valore
radarbob

L'uso di una variabile standard invece di lock(this)è un consiglio standard; è importante notare che ciò renderà generalmente impossibile per il codice esterno causare il blocco associato all'oggetto tra chiamate di metodo. Questa potrebbe essere o meno una buona cosa . Esiste un pericolo nel consentire al codice esterno di mantenere un lucchetto per una durata arbitraria, e le classi dovrebbero generalmente essere progettate in modo da rendere tale uso non necessario, ma non ci sono sempre alternative pratiche. Come semplice esempio, a meno che implementa la raccolta di una ToArrayo ToListmetodo per il proprio ...
Supercat

4
(a differenza dei metodi di estensione `IEnumerable <T>), l'unico modo per un thread che vuole ottenere un'istantanea della raccolta potrebbe essere quello di enumerarlo mentre blocca tutte le modifiche . Per fare ciò, deve avere accesso a un blocco acquisito da qualsiasi codice che cambierebbe la raccolta. La mancata esposizione del blocco può rendere impossibile, ad esempio, che il programma esegua periodicamente un'istantanea asincrona della raccolta (ad esempio per aggiornare un'interfaccia utente di esplorazione della raccolta).
supercat

there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false- Credo che quei discorsi riguardino il bit SyncBlock nell'oggetto CLR, quindi formalmente questo è giusto - blocca l'oggetto modificato stesso
sll

@Esteban, adoro il tuo esempio, è fantastico. Ho una domanda per voi. Il codice del metodo NameChange (..) termina con: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . } else Monitor.Exit (person.Name); </code> Non dovrebbe finire con: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . Monitor.Exit (person.Name); } </code>
AviFarah,

64

Perché se le persone possono accedere al thispuntatore della tua istanza di oggetto (cioè: tuo ), allora possono anche provare a bloccare lo stesso oggetto. Ora potrebbero non essere consapevoli del fatto che ti stai bloccando thisinternamente, quindi questo potrebbe causare problemi (probabilmente un deadlock)

Oltre a ciò, è anche una cattiva pratica, perché blocca "troppo"

Ad esempio, potresti avere una variabile membro di List<int>e l'unica cosa che devi effettivamente bloccare è quella variabile membro. Se blocchi l'intero oggetto nelle tue funzioni, altre cose che chiamano quelle funzioni verranno bloccate in attesa del blocco. Se queste funzioni non hanno bisogno di accedere all'elenco dei membri, causerai l'attesa di altro codice e rallenterai l'applicazione senza motivo.


44
L'ultimo paragrafo di questa risposta non è corretto. Il blocco non rende in alcun modo l'oggetto inaccessibile o di sola lettura. Lock (this) non impedisce a un altro thread di chiamare o modificare l'oggetto a cui fa riferimento.
Esteban Brenes,

3
Lo fa se anche gli altri metodi chiamati fanno un lock (questo). Credo che sia questo il punto che stava sollevando. Nota "Se blocchi l'intero oggetto nelle tue funzioni" ...
Herms,

@Orion: è più chiaro. @Herms: Sì, ma non è necessario utilizzare 'this' per ottenere quella funzionalità, la proprietà SyncRoot negli elenchi serve a tale scopo, ad esempio, mentre chiarisce che la sincronizzazione dovrebbe essere fatta su quella chiave.
Esteban Brenes,

Ri: bloccare "troppo": è un buon equilibrio che decide cosa bloccare. Tenere presente che il blocco richiede operazioni della CPU con svuotamento della cache ed è piuttosto costoso. In altre parole: non bloccare e aggiornare ogni singolo numero intero. :)
Zan Lynx,

L'ultimo paragrafo non ha ancora senso. Se hai solo bisogno di limitare l'accesso all'elenco, perché le altre funzioni dovrebbero avere dei blocchi se non accedono all'elenco?
Joakim MH,

44

Dai un'occhiata alla sincronizzazione dei thread dell'argomento MSDN (Guida per programmatori C #)

In genere, è meglio evitare il blocco su un tipo pubblico o su istanze di oggetti al di fuori del controllo dell'applicazione. Ad esempio, il blocco (questo) può essere problematico se si può accedere pubblicamente all'istanza, perché anche il codice al di fuori del proprio controllo potrebbe bloccare l'oggetto. Ciò potrebbe creare situazioni di deadlock in cui due o più thread attendono il rilascio dello stesso oggetto. Il blocco su un tipo di dati pubblico, anziché su un oggetto, può causare problemi per lo stesso motivo. Il blocco delle stringhe letterali è particolarmente rischioso perché le stringhe letterali sono internate dal Common Language Runtime (CLR). Ciò significa che esiste una sola istanza di una determinata stringa letterale per l'intero programma, lo stesso identico oggetto rappresenta la letterale in tutti i domini dell'applicazione in esecuzione, su tutti i thread. Di conseguenza, un blocco posizionato su una stringa con lo stesso contenuto in qualsiasi punto del processo dell'applicazione blocca tutte le istanze di quella stringa nell'applicazione. Di conseguenza, è meglio bloccare un membro privato o protetto che non è internato. Alcune classi forniscono membri specifici per il blocco. Il tipo di array, ad esempio, fornisce SyncRoot. Molti tipi di raccolta forniscono anche un membro SyncRoot.


34

So che questo è un vecchio thread, ma poiché le persone possono ancora cercarlo e fare affidamento su di esso, sembra importante sottolineare che lock(typeof(SomeObject))è significativamente peggio di lock(this). Avendolo detto; complimenti sinceri ad Alan per aver sottolineato che lock(typeof(SomeObject))è una cattiva pratica.

Un'istanza di System.Typeè uno degli oggetti più generici a grana grossa che ci sia. Per lo meno, un'istanza di System.Type è globale per un AppDomain e .NET può eseguire più programmi in un AppDomain. Ciò significa che due programmi completamente diversi potrebbero potenzialmente causare interferenze l'uno con l'altro fino al punto di creare un deadlock se entrambi tentano di ottenere un blocco di sincronizzazione sulla stessa istanza del tipo.

Quindi lock(this)non è una forma particolarmente robusta, può causare problemi e dovrebbe sempre sollevare le sopracciglia per tutti i motivi citati. Eppure esiste un codice ampiamente usato, relativamente rispettato e apparentemente stabile come log4net che usa ampiamente il pattern lock (questo), anche se preferirei vedere personalmente quel pattern change.

Ma lock(typeof(SomeObject))apre una lattina completamente nuova e migliorata di worm.

Per quello che vale.


26

... e gli stessi stessi argomenti si applicano anche a questo costrutto:

lock(typeof(SomeObject))

17
lock (typeof (SomeObject)) è in realtà molto peggio di lock (questo) ( stackoverflow.com/a/10510647/618649 ).
Craig,

1
bene, bloccare (Application.Current) è ancora peggio allora, ma chi proverebbe comunque una di queste stupide cose? lock (questo) sembra logico e succinto, ma questi altri esempi no.
Zar Shardan,

Non sono d'accordo che lock(this)sembra particolarmente logico e conciso. È un blocco terribilmente grossolano e qualsiasi altro codice potrebbe bloccare un oggetto, causando potenzialmente interferenze nel codice interno. Prendi più blocchi granulari e assumi un controllo più stretto. Quello lock(this)che è successo è che è molto meglio di lock(typeof(SomeObject)).
Craig,

8

Immagina di avere un segretario qualificato nel tuo ufficio che è una risorsa condivisa nel dipartimento. Di tanto in tanto ti precipiti verso di loro perché hai un compito, solo per sperare che un altro dei tuoi colleghi non li abbia già reclamati. Di solito devi solo aspettare un breve periodo di tempo.

Poiché la cura è la condivisione, il tuo manager decide che i clienti possono utilizzare direttamente anche il segretario. Ma questo ha un effetto collaterale: un cliente potrebbe persino reclamarli mentre lavori per questo cliente e ne hai anche bisogno per eseguire parte delle attività. Si verifica un deadlock, poiché l'affermazione non è più una gerarchia. Ciò avrebbe potuto essere evitato tutti insieme non consentendo ai clienti di reclamarli in primo luogo.

lock(this)è brutto come abbiamo visto. Un oggetto esterno potrebbe bloccarsi sull'oggetto e poiché non controlli chi sta usando la classe, chiunque può bloccarlo ... Qual è l'esempio esatto come descritto sopra. Ancora una volta, la soluzione è limitare l'esposizione dell'oggetto. Tuttavia, se hai una private, protectedo internalclasse, potresti già controllare chi sta bloccando il tuo oggetto , perché sei sicuro di aver scritto tu stesso il codice. Quindi il messaggio qui è: non esporlo come public. Inoltre, assicurarsi che venga utilizzato un blocco in scenari simili per evitare deadlock.

L'esatto contrario è bloccare le risorse condivise in tutto il dominio dell'app, lo scenario peggiore. È come mettere fuori il tuo segretario e permettere a tutti là fuori di reclamarli. Il risultato è il caos assoluto - o in termini di codice sorgente: è stata una cattiva idea; buttalo via e ricomincia. Quindi come lo facciamo?

I tipi sono condivisi nel dominio dell'app come la maggior parte delle persone qui sottolinea. Ma ci sono anche cose migliori che possiamo usare: le stringhe. Il motivo è che le stringhe sono raggruppate . In altre parole: se hai due stringhe che hanno gli stessi contenuti in un dominio dell'app, è possibile che abbiano lo stesso puntatore. Dal momento che il puntatore viene utilizzato come chiave di blocco, ciò che si ottiene sostanzialmente è un sinonimo di "preparare comportamenti indefiniti".

Allo stesso modo, non dovresti bloccare oggetti WCF, HttpContext.Current, Thread.Current, Singletons (in generale), ecc. Il modo più semplice per evitare tutto questo? private [static] object myLock = new object();


3
In realtà avere una classe privata non impedisce il problema. Il codice esterno può ottenere un riferimento a un'istanza di una classe privata ...
Rashack,

1
@Rashack mentre sei tecnicamente corretto (+1 per averlo sottolineato), il mio punto era che dovresti avere il controllo di chi sta bloccando l'istanza. Restituire istanze del genere lo interrompe.
atlaste il

4

Il blocco su questo puntatore può essere negativo se si sta bloccando una risorsa condivisa . Una risorsa condivisa può essere una variabile statica o un file sul tuo computer, ovvero qualcosa che è condiviso tra tutti gli utenti della classe. Il motivo è che questo puntatore conterrà un riferimento diverso a una posizione in memoria ogni volta che viene creata un'istanza della classe. Quindi, bloccando oltre questo in una volta istanza di una classe è diverso bloccaggio su questo in un'altra istanza di una classe.

Dai un'occhiata a questo codice per vedere cosa intendo. Aggiungi il seguente codice al tuo programma principale in un'applicazione Console:

    static void Main(string[] args)
    {
         TestThreading();
         Console.ReadLine();
    }

    public static void TestThreading()
    {
        Random rand = new Random();
        Thread[] threads = new Thread[10];
        TestLock.balance = 100000;
        for (int i = 0; i < 10; i++)
        {
            TestLock tl = new TestLock();
            Thread t = new Thread(new ThreadStart(tl.WithdrawAmount));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
        Console.Read();
    }

Crea una nuova classe come la seguente.

 class TestLock
{
    public static int balance { get; set; }
    public static readonly Object myLock = new Object();

    public void Withdraw(int amount)
    {
      // Try both locks to see what I mean
      //             lock (this)
       lock (myLock)
        {
            Random rand = new Random();
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
            }
            else
            {
                Console.WriteLine("Can't process your transaction, current balance is :  " + balance + " and you tried to withdraw " + amount);
            }
        }

    }
    public void WithdrawAmount()
    {
        Random rand = new Random();
        Withdraw(rand.Next(1, 100) * 100);
    }
}

Ecco una sequenza del blocco del programma su questo .

   Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  94400
    Balance before Withdrawal :  100000
    Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  88800
    Withdraw        : -5600
    Balance after Withdrawal  :  83200
    Balance before Withdrawal :  83200
    Withdraw        : -9100
    Balance after Withdrawal  :  74100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance after Withdrawal  :  55900
    Balance after Withdrawal  :  65000
    Balance before Withdrawal :  55900
    Withdraw        : -9100
    Balance after Withdrawal  :  46800
    Balance before Withdrawal :  46800
    Withdraw        : -2800
    Balance after Withdrawal  :  44000
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  41200
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  38400

Ecco una sequenza del blocco del programma su myLock .

Balance before Withdrawal :  100000
Withdraw        : -6600
Balance after Withdrawal  :  93400
Balance before Withdrawal :  93400
Withdraw        : -6600
Balance after Withdrawal  :  86800
Balance before Withdrawal :  86800
Withdraw        : -200
Balance after Withdrawal  :  86600
Balance before Withdrawal :  86600
Withdraw        : -8500
Balance after Withdrawal  :  78100
Balance before Withdrawal :  78100
Withdraw        : -8500
Balance after Withdrawal  :  69600
Balance before Withdrawal :  69600
Withdraw        : -8500
Balance after Withdrawal  :  61100
Balance before Withdrawal :  61100
Withdraw        : -2200
Balance after Withdrawal  :  58900
Balance before Withdrawal :  58900
Withdraw        : -2200
Balance after Withdrawal  :  56700
Balance before Withdrawal :  56700
Withdraw        : -2200
Balance after Withdrawal  :  54500
Balance before Withdrawal :  54500
Withdraw        : -500
Balance after Withdrawal  :  54000

1
qual è la cosa da notare nel tuo esempio, ad esempio cosa stai mostrando che non è corretto. è difficile individuare cosa c'è che non va quando usi Random rand = new Random();nvm, penso di vedere il suo ripetuto Balance
Seabizkit

3

C'è un ottimo articolo a riguardo http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects di Rico Mariani, architetto delle prestazioni per il runtime Microsoft® .NET

Estratto:

Il problema di base qui è che non possiedi l'oggetto type e non sai chi altro potrebbe accedervi. In generale, è una pessima idea fare affidamento sul blocco di un oggetto che non è stato creato e non si sa a chi altri potrebbero accedere. In questo modo invita deadlock. Il modo più sicuro è bloccare solo oggetti privati.



2

Ecco un'illustrazione molto più semplice (presa dalla domanda 34 qui ) perché il blocco (questo) è negativo e può causare deadlock quando anche i consumatori della tua classe tentano di bloccare l'oggetto. Di seguito, solo uno dei tre thread può procedere, gli altri due sono bloccati.

class SomeClass
{
    public void SomeMethod(int id)
    {
        **lock(this)**
        {
            while(true)
            {
                Console.WriteLine("SomeClass.SomeMethod #" + id);
            }
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        SomeClass o = new SomeClass();

        lock(o)
        {
            for (int threadId = 0; threadId < 3; threadId++)
            {
                Thread t = new Thread(() => {
                    o.SomeMethod(threadId);
                        });
                t.Start();
            }

            Console.WriteLine();
        }

Per aggirare il problema, questo ragazzo ha usato Thread.TryMonitor (con timeout) anziché lock:

            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }

https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks


Per quanto vedo, quando sostituisco il blocco (questo) con un blocco su un membro di istanza privato SomeClass, ottengo ancora lo stesso deadlock. Inoltre, se il blocco nella classe principale viene eseguito su un altro membro dell'istanza privata del Programma, si verifica lo stesso blocco. Quindi, non sono sicuro che questa risposta non sia fuorviante e errata. Vedi quel comportamento qui: dotnetfiddle.net/DMrU5h
Bartosz,

1

Perché qualsiasi blocco di codice che può vedere l'istanza della tua classe può anche bloccare quel riferimento. Si desidera nascondere (incapsulare) l'oggetto di blocco in modo che solo il codice che deve fare riferimento a esso possa fare riferimento a esso. La parola chiave si riferisce all'istanza della classe corrente, quindi un numero qualsiasi di cose potrebbe fare riferimento ad essa e usarla per eseguire la sincronizzazione dei thread.

Per essere chiari, questo è negativo perché alcuni altri blocchi di codice potrebbero utilizzare l'istanza della classe per bloccare e potrebbero impedire al codice di ottenere un blocco tempestivo o potrebbero creare altri problemi di sincronizzazione dei thread. Caso migliore: nient'altro usa un riferimento alla tua classe per bloccare. Caso centrale: qualcosa usa un riferimento alla tua classe per fare blocchi e causa problemi di prestazioni. Caso peggiore: qualcosa usa un riferimento della tua classe per fare i blocchi e causa problemi davvero cattivi, molto sottili, davvero difficili da debug.


1

Scusate ragazzi ma non posso essere d'accordo con l'argomento che bloccare questo potrebbe causare un deadlock. Stai confondendo due cose: deadlock e fame.

  • Non è possibile annullare il deadlock senza interrompere uno dei thread, quindi dopo essere entrati in un deadlock non è possibile uscire
  • La fame terminerà automaticamente dopo che uno dei thread ha terminato il suo lavoro

Ecco un'immagine che illustra la differenza.

Conclusione
Puoi comunque usare in sicurezza lock(this)se la fame nel thread non è un problema per te. Devi ancora tenere presente che quando il thread, che sta utilizzando il thread affamato, lock(this)termina in un blocco con l'oggetto bloccato, alla fine si concluderà con l'inedia eterna;)


9
C'è una differenza ma è del tutto irrilevante per questa discussione. E la prima frase della tua conclusione è completamente sbagliata.
Ben Voigt,

1
Per essere chiari: non sto difendendo lock(this): questo tipo di codice è semplicemente sbagliato. Penso solo che chiamarlo deadlock sia un po 'offensivo.
SOReader,

2
Il collegamento all'immagine non è più disponibile. :( Qualche possibilità che tu possa fare nuovamente riferimento? Thx
VG1


1

Ecco un codice di esempio che è più semplice da seguire (IMO): (Funzionerà in LinqPad , riferimento ai seguenti spazi dei nomi: System.Net e System.Threading.Tasks)

Qualcosa da ricordare è che lock (x) è fondamentalmente zucchero sintattico e quello che fa è usare Monitor.Enter e quindi usa un blocco try, catch, infine per chiamare Monitor.Exit. Vedi: https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (sezione note)

oppure usa l'istruzione lock C # (istruzione SyncLock in Visual Basic), che avvolge i metodi Enter ed Exit in un blocco try ... finally.

void Main()
{
    //demonstrates why locking on THIS is BADD! (you should never lock on something that is publicly accessible)
    ClassTest test = new ClassTest();
    lock(test) //locking on the instance of ClassTest
    {
        Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Parallel.Invoke(new Action[]
        {
            () => {
                //this is there to just use up the current main thread. 
                Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
                },
            //none of these will enter the lock section.
            () => test.DoWorkUsingThisLock(1),//this will dead lock as lock(x) uses Monitor.Enter
            () => test.DoWorkUsingMonitor(2), //this will not dead lock as it uses Montory.TryEnter
        });
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        lock(this) //this can be bad if someone has locked on this already, as it will cause it to be deadlocked!
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
        }
        Console.WriteLine($"End ClassTest.DoWorkUsingThisLock Done {i}  CurrentThread {Thread.CurrentThread.ManagedThreadId}");
    }

    public void DoWorkUsingMonitor(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        if (Monitor.TryEnter(this))
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            Monitor.Exit(this);
        }
        else
        {
            Console.WriteLine($"Skipped lock section!  {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        }

        Console.WriteLine($"End ClassTest.DoWorkUsingMonitor Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine();
    }
}

Produzione

CurrentThread 15
CurrentThread 15
Start ClassTest.DoWorkUsingMonitor 2 CurrentThread 13
Start ClassTest.DoWorkUsingThisLock 1 CurrentThread 12
Skipped lock section!  2 CurrentThread 13
End ClassTest.DoWorkUsingMonitor Done 2 CurrentThread 13

Nota che il thread n. 12 non finisce mai perché è completamente bloccato.


1
sembra che il secondo DoWorkUsingThisLockthread non sia necessario per illustrare il problema?
Jack Lu,

non intendi il blocco esterno in main, un thread aspetterebbe semplicemente che l'altro si completi? che quindi invaliderebbe il Parallelo ... sento che abbiamo bisogno di migliori esempi del mondo reale ..
Seabizkit,

@Seabizkit, aggiornato il codice per renderlo un po 'più chiaro. Parallel è lì solo per creare un nuovo thread ed eseguire il codice in modo asincrono. In realtà, il secondo thread avrebbe potuto essere invocato in qualsiasi modo (clic sul pulsante, richiesta separata, ecc.).
Raj Rao,

0

È possibile stabilire una regola che dice che una classe può avere un codice che si blocca su "this" o su qualsiasi oggetto che viene istanziato dal codice nella classe. Quindi è solo un problema se il modello non viene seguito.

Se vuoi proteggerti dal codice che non segue questo schema, la risposta accettata è corretta. Ma se lo schema viene seguito, non è un problema.

Il vantaggio di lock (questo) è l'efficienza. Che cosa succede se si dispone di un semplice "oggetto valore" che contiene un singolo valore. È solo un wrapper e viene istanziato milioni di volte. Richiedendo la creazione di un oggetto di sincronizzazione privato solo per il blocco, hai sostanzialmente raddoppiato la dimensione dell'oggetto e raddoppiato il numero di allocazioni. Quando le prestazioni sono importanti, questo è un vantaggio.

Quando non ti interessa il numero di allocazioni o il footprint di memoria, evitare il blocco (questo) è preferibile per i motivi indicati in altre risposte.


-1

Si verificherà un problema se è possibile accedere all'istanza pubblicamente perché potrebbero esserci altre richieste che potrebbero utilizzare la stessa istanza di oggetto. È meglio usare una variabile privata / statica.


5
Non sono sicuro di ciò che si aggiunge all'uomo, risposte dettagliate già esistenti che dicono la stessa cosa.
Andrew Barber,
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.