35 votes

Verrouiller correctement une List<T> dans les scénarios multithreads ?

Ok, je n'arrive pas à me faire une idée correcte des scénarios de multithreading. Désolé de poser à nouveau une question similaire, mais je vois beaucoup de "faits" différents sur Internet.

public static class MyClass {
    private static List<string> _myList = new List<string>;
    private static bool _record;

    public static void StartRecording()
    {
        _myList.Clear();
        _record = true;
    }

    public static IEnumerable<string> StopRecording()
    {
        _record = false;
        // Return a Read-Only copy of the list data
        var result = new List<string>(_myList).AsReadOnly();
        _myList.Clear();
        return result;
    }

    public static void DoSomething()
    {
        if(_record) _myList.Add("Test");
        // More, but unrelated actions
    }
}

L'idée est que si l'enregistrement est activé, les appels à DoSomething() sont enregistrés dans une liste interne, et retournés lorsque StopRecording() est appelé.

Mon cahier des charges est le suivant :

  • StartRecording n'est pas considéré comme Thread-Safe. L'utilisateur doit l'appeler pendant qu'aucun autre Thread n'appelle DoSomething(). Mais si cela pouvait être le cas, ce serait formidable.
  • StopRecording n'est pas non plus officiellement thread-safe. Encore une fois, ce serait formidable s'il pouvait l'être, mais ce n'est pas une obligation.
  • DoSomething doit être à l'abri des threads.

La méthode habituelle semble être :

    public static void DoSomething()
    {
        object _lock = new object();
        lock(_lock){
            if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }

Une autre solution consiste à déclarer une variable statique :

    private static object _lock;

    public static void DoSomething()
    {
        lock(_lock){
            if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }

Cependant, cette réponse précise que cela n'empêche pas un autre code d'y accéder.

Je me demande donc

  • Comment verrouiller correctement une liste ?
  • Dois-je créer l'objet de verrouillage dans ma fonction ou comme une variable de classe statique ?
  • Puis-je également intégrer la fonctionnalité de Start et StopRecording dans un lock-block ?
  • StopRecording() fait deux choses : Mettre une variable booléenne à false (pour empêcher DoSomething() d'ajouter plus de choses) et ensuite copier la liste pour retourner une copie des données à l'appelant). Je suppose que _record = false ; est atomique et prend effet immédiatement ? Donc, normalement, je n'aurais pas à m'inquiéter du Multi-Threading ici du tout, à moins qu'un autre Thread appelle StartRecording() à nouveau ?

En fin de compte, je cherche un moyen d'exprimer "Ok, cette liste est la mienne maintenant, tous les autres fils doivent attendre jusqu'à ce que j'en aie fini avec elle".

42voto

Henk Holterman Points 153608

Je vais verrouiller sur la _myList elle-même ici puisqu'elle est privée, mais l'utilisation d'une variable séparée est plus courante. Pour améliorer quelques points :

public static class MyClass 
{
    private static List<string> _myList = new List<string>;
    private static bool _record; 

    public static void StartRecording()
    {
        lock(_myList)   // lock on the list
        {
           _myList.Clear();
           _record = true;
        }
    }

    public static IEnumerable<string> StopRecording()
    {
        lock(_myList)
        {
          _record = false;
          // Return a Read-Only copy of the list data
          var result = new List<string>(_myList).AsReadOnly();
          _myList.Clear();
          return result;
        }
    }

    public static void DoSomething()
    {
        lock(_myList)
        {
          if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }
}

Notez que ce code utilise lock(_myList) pour synchroniser l'accès à la fois à _myList y _enregistrement. Et vous devez synchroniser toutes les actions sur ces deux-là.

Et je suis d'accord avec les autres réponses ici, lock(_myList) ne fait rien à _myList, elle utilise simplement _myList comme jeton (vraisemblablement comme clé dans un HashSet). Toutes les méthodes doivent jouer franc jeu en demandant la permission en utilisant le même jeton. Une méthode sur un autre thread peut toujours utiliser _myList sans verrouillage préalable, mais avec des résultats imprévisibles.

Nous pouvons utiliser n'importe quel jeton, c'est pourquoi nous en créons souvent un spécialement :

private static object _listLock = new object();

Et ensuite utiliser lock(_listLock) au lieu de lock(_myList) partout.

Cette technique aurait été conseillée si maListe avait été publique, et elle aurait été absolument nécessaire si vous aviez recréé maListe au lieu d'appeler Clear().

16voto

Jon Skeet Points 692016

Création d'une nouvelle serrure dans DoSomething() serait certainement serait erroné - il serait inutile, car chaque appel à DoSomething() utiliserait une autre serrure. Vous devriez utiliser la deuxième forme, mais avec un initialisateur :

private static object _lock = new object();

Il est vrai que le verrouillage n'empêche rien d'autre d'accéder à votre liste, mais à moins que vous n'exposiez la liste directement, cela n'a pas d'importance : rien d'autre n'accédera à la liste de toute façon.

Oui, vous pouvez envelopper Start/StopRecording dans des verrous de la même manière.

Oui, la définition d'une variable booléenne est atomique, mais cela ne la rend pas thread-safe. Si vous n'accédez à la variable qu'à l'intérieur d'un même verrou, vous n'avez aucun problème en termes d'atomicité. y volatilité cependant. Sinon, vous risquez de voir des valeurs "périmées" - par exemple, vous avez fixé la valeur à true dans un thread, et un autre thread pourrait utiliser une valeur en cache lors de sa lecture.

8voto

Matt Davis Points 22019

Il y a plusieurs façons de verrouiller la liste. Vous pouvez verrouiller directement sur _myList à condition que _myList ne soit jamais modifié pour référencer une nouvelle liste.

lock (_myList)
{
    // do something with the list...
}

Vous pouvez créer un objet de verrouillage spécifiquement à cette fin.

private static object _syncLock = new object();
lock (_syncLock)
{
    // do something with the list...
}

Si la collection statique implémente l'interface System.Collections.ICollection (c'est le cas de List(T)), vous pouvez également synchroniser en utilisant la propriété SyncRoot.

lock (((ICollection)_myList).SyncRoot)
{
    // do something with the list...
}

Le point principal à comprendre est que vous voulez un y seulement un à utiliser comme sentinelle de verrouillage, c'est pourquoi la création de la sentinelle de verrouillage dans la fonction DoSomething() ne fonctionnera pas. Comme Jon l'a dit, chaque thread qui appelle DoSomething() obtiendra son propre objet, donc le verrouillage sur cet objet réussira à chaque fois et accordera un accès immédiat à la liste. En rendant l'objet de verrouillage statique (via la liste elle-même, un objet de verrouillage dédié ou la propriété ICollection.SyncRoot), il devient partagé entre tous les threads et peut effectivement sérialiser l'accès à votre liste.

3voto

Scott Weinstein Points 11404

Le premier moyen est mauvais car chaque appelant se verrouillera sur un objet différent. Vous pourriez simplement verrouiller sur la liste.

lock(_myList)
{
   _myList.Add(...)
}

1voto

Quintin Robinson Points 41988

Vous pouvez mal interpréter le cette réponse Ce que l'on dit en réalité, c'est qu'ils lock ne verrouille pas réellement la modification de l'objet en question, mais empêche l'exécution de tout autre code utilisant cet objet comme source de verrouillage.

Cela signifie que lorsque vous utilisez la même instance que l'objet de verrouillage, le code à l'intérieur du bloc de verrouillage ne doit pas être exécuté.

En fait, vous n'essayez pas vraiment de "verrouiller" votre liste, vous essayez d'avoir une instance commune qui peut être utilisée comme point de référence lorsque vous voulez modifier votre liste. Lorsque cette instance est utilisée ou "verrouillée", vous voulez empêcher l'exécution d'un autre code qui pourrait potentiellement modifier la liste.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X