web-dev-qa-db-fra.com

Pourquoi ne pouvons-nous pas verrouiller sur un type de valeur?

J'essayais de lock une variable Boolean lorsque j'ai rencontré l'erreur suivante:

'bool' n'est pas un type de référence comme requis par l'instruction lock

Il semble que seuls les types de référence soient autorisés dans les instructions lock, mais je ne suis pas sûr de comprendre pourquoi.

Andreas déclare dans son commentaire :

Lorsque l'objet [un type de valeur] est passé d'un thread à l'autre, une copie est effectuée. Les threads finissent par travailler sur 2 objets différents, ce qui est sûr.

Est-ce vrai? Cela signifie-t-il que lorsque je fais ce qui suit, je modifie en fait deux x différents dans les méthodes xToTrue et xToFalse?

public static class Program {

    public static Boolean x = false;

    [STAThread]
    static void Main(string[] args) {

        var t = new Thread(() => xToTrue());
        t.Start();
        // ...
        xToFalse();
    }

    private static void xToTrue() {
        Program.x = true;
    }

    private static void xToFalse() {
        Program.x = false;
    }
}

(ce code seul est clairement inutile dans son état, ce n'est que pour l'exemple)


P.S: Je connais cette question sur Comment verrouiller correctement un type de valeur . Ma question ne concerne pas le comment mais le pourquoi.

47
Otiel

Juste une conjecture sauvage ici ...

mais si le compilateur vous permettait de verrouiller un type de valeur, vous ne verrouilleriez absolument rien ... car chaque fois que vous passiez le type de valeur à lock, vous en passiez une copie encadrée; une copie en boîte différente. Ainsi, les serrures seraient comme si elles étaient des objets entièrement différents. (depuis, ils sont réellement)

N'oubliez pas que lorsque vous passez un type de valeur pour un paramètre de type object, il est mis en boîte (encapsulé) dans un type de référence. Cela en fait un nouvel objet chaque fois que cela se produit.

40
Andrew Barber

Vous ne pouvez pas verrouiller un type de valeur car il ne possède pas d'enregistrement sync root.

Le verrouillage est effectué par les mécanismes internes du CLR et du système d'exploitation qui reposent sur un objet ayant un enregistrement accessible uniquement à un thread à la fois - racine du bloc de synchronisation. Tout type de référence aurait:

  • Pointeur vers un type
  • Sync block root
  • Pointeur sur les données d'instance dans le tas
25
oleksii

Il s'étend à:

System.Threading.Monitor.Enter(x);
try {
   ...
}
finally {
   System.Threading.Monitor.Exit(x);
}

Bien qu'ils compileraient, Monitor.Enter/Exit requiert un type de référence car un type de valeur serait encadré à une instance d'objet différente à chaque fois afin que chaque appel à Enter et Exit opère sur des objets différents.

A partir de la méthode MSDN Enter page:

Utilisez Monitor pour verrouiller des objets (c'est-à-dire des types de référence), pas des types de valeur. Lorsque vous transmettez une variable de type valeur à Entrée, elle est encadrée en tant qu'objet. Si vous transmettez à nouveau la même variable à Enter, elle est encadrée en tant qu'objet séparé et le thread ne bloque pas. Dans ce cas, le code que Monitor est censé protéger n'est pas protégé. De plus, lorsque vous transmettez la variable à Exit, un autre objet distinct est créé. Etant donné que l'objet transmis à Exit est différent de celui transmis à Entrée, Monitor lève SynchronizationLockException. Pour plus d'informations, voir la rubrique conceptuelle Moniteurs.

18
George Duckett

Je me demandais pourquoi l’équipe .Net avait décidé de limiter les développeurs et d’autoriser Monitor à fonctionner uniquement avec des références. Tout d’abord, vous pensez qu’il serait bien de verrouiller avec un System.Int32 au lieu de définir une variable d’objet dédié uniquement à des fins de verrouillage, ces casiers ne font habituellement rien d’autre.

Mais il apparaît alors que toute fonctionnalité fournie par le langage doit avoir une sémantique forte et non seulement être utile aux développeurs. Donc, la sémantique avec les types de valeur est que chaque fois qu'un type de valeur apparaît dans le code, son expression est évaluée en tant que valeur. Donc, d’un point de vue sémantique, si nous écrivons `lock (x) 'et que x est un type de valeur primitif, c’est la même chose que nous dirions:" verrouille un bloc de code critique à l’aide de la valeur de la variable x "qui sonne davantage qu'étrange, c'est sûr :). Pendant ce temps, lorsque nous rencontrons des variables de référence dans le code, nous avons l'habitude de penser "Oh, c'est une référence à un objet", ce qui implique que la référence peut être partagée entre des blocs de code, des méthodes, des classes et même des threads et des processus, et peut donc servir de garde.

En deux mots, les variables de type valeur apparaissent dans le code uniquement pour être évaluées à leur valeur réelle dans chaque expression - rien de plus.

Je suppose que c'est l'un des points principaux. 

5
Arman McHitarian

Si vous demandez conceptuellement pourquoi cela n'est pas autorisé, je dirais que la réponse découle du fait que identité d'un type de valeur est exactement équivalent à sa valeur (c'est ce qui le rend un type de valeur).

Ainsi, n'importe qui dans l'univers parlant de la int4 parle de de la même chose - comment pouvez-vous alors revendiquer un accès exclusif pour le verrouiller?

4
AakashM

Les types de valeur n'ayant pas le bloc de synchronisation utilisé par l'instruction lock pour verrouiller un objet. Seuls les types de référence supportent la surcharge du type info, du bloc de synchronisation, etc.

Si vous encadrez votre type de référence, vous avez maintenant un objet contenant le type de valeur et vous pouvez le verrouiller (je l’attends) car il a maintenant la surcharge supplémentaire que les objets ont (un pointeur sur un bloc de synchronisation utilisé pour le verrouillage, une pointeur sur les informations de type, etc.). Comme tout le monde le dit cependant - si vous encadrez un objet, vous obtiendrez un nouvel objet chaque fois que vous l'envelopperez afin que vous puissiez verrouiller différents objets à chaque fois - ce qui annule complètement le but du verrouillage.

Cela fonctionnerait probablement (même si c'est complètement inutile et que je ne l'ai pas essayé)

int x = 7;
object boxed = (object)x;

//thread1:
lock (boxed){
 ...
}
//thread2:
lock(boxed){
...
}

Tant que tout le monde utilise boxed et que l'objet boxed n'est défini qu'une fois, vous obtiendrez probablement un verrouillage correct puisque vous verrouillez l'objet boxed et qu'il n'est créé qu'une seule fois. NE FAITES PAS ceci cependant .. c'est juste un exercice de pensée (et peut même ne pas fonctionner - comme je l'ai dit, je ne l'ai pas testé).

Quant à votre deuxième question - Non, la valeur n'est pas copiée pour chaque thread. Les deux threads utiliseront le même booléen, mais il n’est pas certain qu’ils voient la valeur la plus récente (si un thread définit la valeur, il se peut qu’il ne soit pas réécrit immédiatement dans l’emplacement mémoire, de sorte que tout autre thread qui lit la valeur un "vieux" résultat).

2
Russell Troywest

Ce qui suit est tiré de MSDN:

Les instructions lock (C #) et SyncLock (Visual Basic) peuvent être utilisées pour garantir l'exécution complète d'un bloc de code sans interruption par d'autres threads. Ceci est accompli en obtenant un verrou d'exclusion mutuelle pour un objet donné pendant la durée du bloc de code.

et 

L'argument fourni au mot clé lock doit être un objet basé sur un type de référence et permet de définir la portée du verrou.

Je suppose que cela s’explique en partie par le fait que le mécanisme de verrouillage utilise une instance de cet objet pour créer le verrou d’exclusion mutuelle.

1
ChrisBD

Selon ce Thread MSDN , les modifications apportées à une variable de référence peuvent ne pas être visibles par tous les threads et peuvent finir par utiliser des valeurs périmées. AFAIK, je pense que les types de valeur sont copiés lorsqu'ils sont transmis entre les threads.

Pour citer exactement à partir de MSDN

Il est également important de préciser que le fait que la mission soit atomique n'implique pas que l'écriture est immédiatement observée par un autre les fils. Si la référence n'est pas volatile, c'est possible pour un autre thread pour lire une valeur périmée de la référence quelque temps après que votre fil a mis à jour. Cependant, la mise à jour elle-même est garantie d'être atomique (vous ne verrez pas une partie du pointeur sous-jacent se mettre à jour).

1
Vamsi

Je pense que c’est l’un des cas où la réponse à cette question est "car un ingénieur de Microsoft l’a mis en oeuvre de cette façon".

Pour que le verrouillage fonctionne sous le capot, créez une table de structures de serrures en mémoire, puis utilisez les objets vtable pour mémoriser la position dans la table où se trouve le verrou requis. Cela donne l'impression que tous les objets ont un verrou alors qu'en réalité ils n'en ont pas. Seuls ceux qui ont été verrouillés le font. Comme les types de valeur n'ont pas de référence, il n'y a pas de vtable pour stocker la position des verrous.

Pourquoi Microsoft a-t-il choisi cette façon étrange de faire les choses? Ils auraient pu faire de Monitor une classe que vous deviez instancier. Je suis sûr que j'ai vu un article rédigé par un employé de MS qui disait qu'à la réflexion, ce motif était une erreur, mais je n'arrive pas à le trouver maintenant.

0
Martin Brown