web-dev-qa-db-fra.com

Sécurité des threads C # avec get / set

Il s'agit d'une question détaillée pour C #.

Supposons que j'ai une classe avec un objet et que cet objet soit protégé par un verrou:

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         return property;
    }
    set { 
         property = value; 
    }
}

Je veux qu'un fil d'interrogation puisse interroger cette propriété. Je veux également que le thread mette à jour les propriétés de cet objet de temps en temps, et parfois l'utilisateur peut mettre à jour cette propriété, et l'utilisateur veut pouvoir voir cette propriété.

Le code suivant verrouillera-t-il correctement les données?

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         lock (mLock){
             return property;
         }
    }
    set { 
         lock (mLock){
              property = value; 
         }
    }
}

Par "correctement", je veux dire, si je veux appeler

MyProperty.Field1 = 2;

ou autre chose, le champ sera-t-il verrouillé pendant que je fais la mise à jour? Le paramètre effectué par l'opérateur égal à l'intérieur de la portée de la fonction "get", ou la fonction "get" (et donc le verrou) se terminera-t-elle en premier, puis le paramètre, puis "set" sera appelé, contournant ainsi le verrou?

Edit: Puisque cela ne fera apparemment pas l'affaire, qu'est-ce qui le fera? Dois-je faire quelque chose comme:

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         MyObject tmp = null;
         lock (mLock){
             tmp = property.Clone();
         }
         return tmp;
    }
    set { 
         lock (mLock){
              property = value; 
         }
    }
}

qui s'assure plus ou moins que je n'ai accès qu'à une copie, ce qui signifie que si j'avais deux threads appeler un `` get '' en même temps, ils commenceraient chacun par la même valeur de Field1 (non?). Existe-t-il un moyen de verrouiller en lecture et en écriture sur une propriété logique? Ou devrais-je simplement me contraindre à verrouiller des sections de fonctions plutôt que les données elles-mêmes?

Juste pour que cet exemple soit logique: MyObject est un pilote de périphérique qui renvoie l'état de manière asynchrone. Je lui envoie des commandes via un port série, puis l'appareil répond à ces commandes dans son propre temps doux. En ce moment, j'ai un thread qui l'interroge pour son état ("Êtes-vous toujours là? Pouvez-vous accepter les commandes?"), Un thread qui attend les réponses sur le port série ("Je viens de recevoir la chaîne d'état 2, tout va bien" ), puis le thread d'interface utilisateur qui prend d'autres commandes ("L'utilisateur veut que vous fassiez cette chose.") et publie les réponses du pilote ("Je viens de faire la chose, maintenant mettez à jour l'interface utilisateur avec cela"). C'est pourquoi je veux verrouiller sur l'objet lui-même, plutôt que sur les champs de l'objet; ce serait un grand nombre de verrous, a et b, tous les périphériques de cette classe n'ont pas le même comportement, juste un comportement général, donc je devrais coder beaucoup de dialogues individuels si j'individualisait les verrous.

49
mmr

Non, votre code ne verrouillera pas l'accès aux membres de l'objet renvoyé par MyProperty. Il ne verrouille que MyProperty lui-même.

Votre exemple d'utilisation est en réalité deux opérations regroupées en une seule, à peu près équivalente à ceci:

// object is locked and then immediately released in the MyProperty getter
MyObject o = MyProperty;

// this assignment isn't covered by a lock
o.Field1 = 2;

// the MyProperty setter is never even called in this example

En bref - si deux threads accèdent simultanément à MyProperty, le getter bloquera brièvement le deuxième thread jusqu'à ce qu'il renvoie l'objet au premier thread, mais cela ramènera également l'objet au deuxième thread. Les deux threads auront alors un accès complet et déverrouillé à l'objet.

MODIFIER en réponse à plus de détails dans la question

Je ne suis toujours pas certain à 100% de ce que vous essayez de réaliser, mais si vous voulez juste un accès atomique à l'objet, ne pourriez-vous pas avoir le verrou de code appelant contre l'objet lui-même?

// quick and dirty example
// there's almost certainly a better/cleaner way to do this
lock (MyProperty)
{
    // other threads can't lock the object while you're in here
    MyProperty.Field1 = 2;
    // do more stuff if you like, the object is all yours
}
// now the object is up-for-grabs again

Pas idéal, mais tant que tous les accès à l'objet sont contenus dans les sections lock (MyProperty), cette approche sera thread-safe.

39
LukeH

La programmation simultanée serait assez facile si votre approche pouvait fonctionner. Mais ce n'est pas le cas, l'iceberg qui coule que Titanic est, par exemple, le client de votre classe en train de faire ceci:

objectRef.MyProperty += 1;

La course en lecture-modification-écriture est assez évidente, il y en a de pires. Il n'y a absolument rien que vous puissiez faire pour rendre votre propriété thread-safe, à part la rendre immuable. C'est votre client qui doit faire face aux maux de tête. Être contraint de déléguer ce genre de responsabilité à un programmeur qui est le moins susceptible de bien faire les choses est le talon d'Achille de la programmation simultanée.

14
Hans Passant

Comme d'autres l'ont souligné, une fois que vous retournez l'objet du getter, vous perdez le contrôle sur qui accède à l'objet et quand. Pour faire ce que vous voulez faire, vous devrez mettre un verrou à l'intérieur de l'objet lui-même.

Peut-être que je ne comprends pas l'image complète, mais sur la base de votre description, il ne semble pas que vous auriez nécessairement besoin d'un verrou pour chaque champ individuel. Si vous avez un ensemble de champs qui sont simplement lus et écrits via les getters et setters, vous pourriez probablement vous en sortir avec un seul verrou pour ces champs. Il est évidemment possible que vous sérialisiez inutilement le fonctionnement de vos threads de cette façon. Mais encore une fois, sur la base de votre description, il ne semble pas non plus que vous accédiez agressivement à l'objet.

Je suggère également d'utiliser un événement au lieu d'utiliser un fil pour interroger l'état de l'appareil. Avec le mécanisme d'interrogation, vous allez frapper le verrou à chaque fois que le thread interroge l'appareil. Avec le mécanisme d'événement, une fois que l'état change, l'objet avertit tous les écouteurs. À ce stade, votre thread "d'interrogation" (qui ne serait plus l'interrogation) se réveillerait et obtiendrait le nouveau statut. Ce sera beaucoup plus efficace.

Par exemple...

public class Status
{
    private int _code;
    private DateTime _lastUpdate;
    private object _sync = new object(); // single lock for both fields

    public int Code
    {
        get { lock (_sync) { return _code; } }
        set
        {
            lock (_sync) {
                _code = value;
            }

            // Notify listeners
            EventHandler handler = Changed;
            if (handler != null) {
                handler(this, null);
            }
        }
    }

    public DateTime LastUpdate
    {
        get { lock (_sync) { return _lastUpdate; } }
        set { lock (_sync) { _lastUpdate = value; } }
    }

    public event EventHandler Changed;
}

Votre fil de sondage ressemblerait à ceci.

Status status = new Status();
ManualResetEvent changedEvent = new ManualResetEvent(false);
Thread thread = new Thread(
    delegate() {
        status.Changed += delegate { changedEvent.Set(); };
        while (true) {
            changedEvent.WaitOne(Timeout.Infinite);
            int code = status.Code;
            DateTime lastUpdate = status.LastUpdate;
            changedEvent.Reset();
        }
    }
);
thread.Start();
4
Matt Davis

La portée du verrou dans votre exemple est au mauvais endroit - elle doit être à la portée de la propriété de la classe 'MyObject' plutôt que son conteneur.

Si la classe MyObject my object est simplement utilisée pour contenir des données sur lesquelles un thread souhaite écrire et un autre (le thread d'interface utilisateur) à lire, vous n'aurez peut-être pas besoin d'un setter et construisez-le une fois.

Considérez également si le placement de verrous au niveau de la propriété correspond au niveau d'écriture de la granularité des verrous; si plusieurs propriétés peuvent être écrites afin de représenter l'état d'une transaction (par exemple: commandes totales et poids total), il peut être préférable d'avoir le verrou au niveau MyObject (c.-à-d. verrou (myObject.SyncRoot)). .)

2
headsling

Dans l'exemple de code que vous avez publié, un get n'est jamais préformé.

Dans un exemple plus compliqué:

MyProperty.Field1 = MyProperty.doSomething() + 2;

Et bien sûr, en supposant que vous ayez fait:

lock (mLock) 
{
    // stuff...
}

Dans doSomething() alors tous les appels de verrouillage seraient pas suffisants pour garantir la synchronisation sur tout l'objet. Dès que la fonction doSomething() revient, le verrou est perdu, puis l'ajout est effectué, puis l'affectation se produit, qui se verrouille à nouveau.

Ou, pour l'écrire d'une autre manière, vous pouvez faire comme si les verrous ne sont pas effectués de manière amutomatique, et réécrire cela plus comme "code machine" avec une opération par ligne, et cela devient évident:

lock (mLock) 
{
    val = doSomething()
}
val = val + 2
lock (mLock)
{
    MyProperty.Field1 = val
}
1
SoapBox

La beauté du multithreading est que vous ne savez pas dans quel ordre les choses vont se produire. Si vous définissez quelque chose sur un thread, cela peut arriver en premier, cela peut arriver après le get.

Le code que vous avez publié verrouille le membre pendant sa lecture et son écriture. Si vous souhaitez gérer le cas où la valeur est mise à jour, vous devriez peut-être envisager d'autres formes de synchronisation, telles que événements . (Consultez les versions auto/manuelle). Ensuite, vous pouvez dire à votre thread "d'interrogation" que la valeur a changé et qu'elle est prête à être relue.

1
OJ.

Dans votre version modifiée, vous ne fournissez toujours pas de méthode threadsafe pour mettre à jour MyObject. Toute modification des propriétés de l'objet devra être effectuée à l'intérieur d'un bloc synchronisé/verrouillé.

Vous pouvez écrire des setters individuels pour gérer cela, mais vous avez indiqué que cela sera difficile en raison du grand nombre de champs. Si en effet le cas (et vous n'avez pas encore fourni suffisamment d'informations pour l'évaluer), une alternative est d'écrire un setter qui utilise la réflexion; cela vous permettrait de passer une chaîne représentant le nom du champ, et vous pourriez rechercher dynamiquement le nom du champ et mettre à jour la valeur. Cela vous permettrait d'avoir un seul setter qui fonctionnerait sur n'importe quel nombre de champs. Ce n'est pas aussi facile ni aussi efficace, mais cela vous permettrait de gérer un grand nombre de classes et de champs.

0
jdigital

Vous avez implémenté un verrou pour obtenir/définir l'objet mais vous n'avez pas sécurisé le thread de l'objet, ce qui est une autre histoire.

J'ai écrit un article sur les classes de modèles immuables en C # qui pourrait être intéressant dans ce contexte: http://rickyhelgesson.wordpress.com/2012/07/17/mutable-or-immutable-in-a-parallel -world /

0
Ricky Helgesson