web-dev-qa-db-fra.com

Est-ce que @synchronized garantit ou non la sécurité des threads?

En référence à cela réponse , je me demande si c'est correct?

@synchronized ne fait aucun code "thread-safe"

Comme j'ai essayé de trouver une documentation ou un lien pour soutenir cette déclaration, sans succès.

Tout commentaire et/ou réponse sera apprécié à ce sujet.

Pour une meilleure sécurité du fil, nous pouvons opter pour d'autres outils, cela m'est connu.

29
Anoop Vaidya

@synchronized Rend le thread de code sûr s'il est utilisé correctement.

Par exemple:

Disons que j'ai une classe qui accède à une base de données non thread-safe. Je ne veux pas lire et écrire dans la base de données en même temps car cela entraînerait probablement un crash.

Disons donc que j'ai deux méthodes. storeData: et readData sur une classe singleton appelée LocalStore.

- (void)storeData:(NSData *)data
 {
      [self writeDataToDisk:data];
 }

 - (NSData *)readData
 {
     return [self readDataFromDisk];
 }

Maintenant, si je devais envoyer chacune de ces méthodes sur leur propre fil comme ceci:

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [[LocalStore sharedStore] storeData:data];
 });
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [[LocalStore sharedStore] readData];
 });

Il y a de fortes chances que nous obtenions un crash. Cependant, si nous modifions nos méthodes storeData et readData pour utiliser @synchronized

 - (void)storeData:(NSData *)data
 {
     @synchronized(self) {
       [self writeDataToDisk:data];
     }
 }

 - (NSData *)readData
 { 
     @synchronized(self) {
      return [self readDataFromDisk];
     }
 }

Maintenant, ce code serait thread-safe. Il est important de noter que si je supprime l'une des instructions @synchronized, Le code ne sera plus thread-safe. Ou si je devais synchroniser différents objets au lieu de self.

@synchronized Crée un verrou mutex sur l'objet que vous synchronisez. En d'autres termes, si un code veut accéder au code dans un bloc @synchronized(self) { }, il devra se mettre en ligne derrière tout le code précédent exécuté dans ce même bloc.

Si nous devions créer différents objets localStore, la @synchronized(self) ne verrouillerait chaque objet individuellement. Cela a-t-il du sens?

Pensez-y comme ça. Vous avez tout un tas de gens qui attendent sur des lignes distinctes, chaque ligne est numérotée de 1 à 10. Vous pouvez choisir dans quelle ligne vous voulez que chaque personne attende (en synchronisant sur une base par ligne), ou si vous n'utilisez pas @synchronized Vous pouvez sauter directement vers l'avant et sauter toutes les lignes. Une personne en ligne 1 n'a pas à attendre qu'une personne en ligne 2 termine, mais la personne en ligne 1 doit attendre que tout le monde devant elle dans sa ligne termine.

39
Jack Freeman

Je pense que l'essence de la question est:

l'utilisation appropriée de la synchronisation est-elle en mesure de résoudre tout problème de thread-safe?

Techniquement oui, mais dans la pratique, il est conseillé d'apprendre et d'utiliser d'autres outils.


Je répondrai sans supposer de connaissances antérieures.

Code correct est un code conforme à ses spécifications. Une bonne spécification définit

  • invariants contraignant l'état,
  • conditions préalables et postconditions décrivant les effets des opérations.

Thread-safe code est un code qui reste correct lorsqu'il est exécuté par plusieurs threads. Donc,

  • Aucune séquence d'opérations ne peut enfreindre la spécification.1
  • Les invariants et les conditions se maintiendront pendant l'exécution multithread sans nécessiter de synchronisation supplémentaire par le client2.

Le point à retenir de haut niveau est: thread-safe nécessite que la spécification soit vraie pendant l'exécution multithread. Pour réellement coder cela, nous devons faire une seule chose: réguler l'accès à un état partagé mutable3. Et il y a trois façons de le faire:

  • Empêchez l'accès.
  • Rendre l'État immuable.
  • Synchronisez l'accès.

Les deux premiers sont simples. Le troisième nécessite la prévention des problèmes de sécurité des threads suivants:

  • vivacité
    • deadlock: deux threads bloquent en attente l'un de l'autre pour libérer une ressource nécessaire.
    • livelock: un thread est occupé à travailler mais il est incapable de progresser.
    • famine: un thread se voit toujours refuser l'accès aux ressources dont il a besoin pour progresser.
  • publication sécurisée: la référence et l'état de l'objet publié doivent être rendus visibles aux autres threads en même temps.
  • conditions de concurrence Une condition de concurrence est un défaut où la sortie dépend du moment des événements incontrôlables. En d'autres termes, une condition de concurrence se produit lorsque l'obtention de la bonne réponse repose sur un timing chanceux. Toute opération composée peut subir une condition de concurrence, par exemple: "check-then-act", "put-if-absent". Un exemple de problème serait if (counter) counter--;, et l'une des solutions serait @synchronize(self){ if (counter) counter--;}.

Pour résoudre ces problèmes, nous utilisons des outils tels que @synchronize, Volatile, barrières mémoire, opérations atomiques, verrous spécifiques, files d'attente et synchroniseurs (sémaphores, barrières).

Et revenons à la question:

l'utilisation appropriée de @synchronize est-elle en mesure de résoudre tout problème de thread-safe?

Techniquement oui, car tout outil mentionné ci-dessus peut être émulé avec @synchronize. Mais cela entraînerait de mauvaises performances et augmenterait les risques de problèmes liés à la vivacité. Au lieu de cela, vous devez utiliser l'outil approprié pour chaque situation. Exemple:

counter++;                       // wrong, compound operation (fetch,++,set)
@synchronize(self){ counter++; } // correct but slow, thread contention
OSAtomicIncrement32(&count);     // correct and fast, lockless atomic hw op

Dans le cas de la question liée, vous pouvez en effet utiliser @synchronize, Ou un verrou en lecture-écriture GCD, ou créer une collection avec suppression de verrouillage, ou tout ce que la situation requiert. La bonne réponse dépend du modèle d'utilisation. Dans tous les cas, vous devez documenter dans votre classe les garanties thread-safe que vous offrez.


1Autrement dit, voir l'objet dans un état non valide ou violer les conditions de pré/post.

2Par exemple, si le thread A itère une collection X et que le thread B supprime un élément, l'exécution se bloque. Ceci n'est pas thread-safe car le client devra se synchroniser sur le verrou intrinsèque de X (synchronize(X)) pour avoir un accès exclusif. Cependant, si l'itérateur renvoie une copie de la collection, la collection devient thread-safe.

3L'état partagé immuable ou les objets non partagés mutables sont toujours thread-safe.

22
Jano

En général, @synchronized Garantit la sécurité des threads, mais uniquement lorsqu'il est utilisé correctement. Il est également sûr d'acquérir le verrou de manière récursive, mais avec des limitations que je détaille dans ma réponse ici .

Il existe plusieurs façons courantes d’utiliser @synchronized De manière incorrecte. Ce sont les plus courants:

Utiliser @synchronized Pour assurer la création d'objets atomiques.

- (NSObject *)foo {
    @synchronized(_foo) {
        if (!_foo) {
            _foo = [[NSObject alloc] init];
        }
        return _foo;
    }
}

Étant donné que _foo Sera nul lors de la première acquisition du verrou, aucun verrouillage ne se produira et plusieurs threads peuvent potentiellement créer leur propre _foo Avant la fin du premier.

Utilisez @synchronized Pour verrouiller un nouvel objet à chaque fois.

- (void)foo {
    @synchronized([[NSObject alloc] init]) {
        [self bar];
    }
}

J'ai vu ce code un peu, ainsi que l'équivalent C # lock(new object()) {..}. Puisqu'il essaie de verrouiller un nouvel objet à chaque fois, il sera toujours autorisé dans la section critique du code. Ce n'est pas une sorte de magie de code. Il ne fait absolument rien pour assurer la sécurité des fils.

Enfin, verrouillage sur self.

- (void)foo {
    @synchronized(self) {
        [self bar];
    }
}

Bien que cela ne soit pas en soi un problème, si votre code utilise un code externe ou est lui-même une bibliothèque, cela peut être un problème. Alors qu'en interne l'objet est connu sous le nom de self, il a en externe un nom de variable. Si le code externe appelle @synchronized(_yourObject) {...} et que vous appelez @synchronized(self) {...}, vous pouvez vous retrouver dans une impasse. Il est préférable de créer un objet interne à verrouiller qui ne soit pas exposé à l'extérieur de votre objet. Ajouter _lockObject = [[NSObject alloc] init]; À l'intérieur de votre fonction init est bon marché, facile et sûr.

ÉDITER:

On me pose toujours des questions sur ce post, voici donc un exemple de pourquoi c'est une mauvaise idée d'utiliser @synchronized(self) dans la pratique.

@interface Foo : NSObject
- (void)doSomething;
@end

@implementation Foo
- (void)doSomething {
    sleep(1);
    @synchronized(self) {
        NSLog(@"Critical Section.");
    }
}

// Elsewhere in your code
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
Foo *foo = [[Foo alloc] init];
NSObject *lock = [[NSObject alloc] init];

dispatch_async(queue, ^{
    for (int i=0; i<100; i++) {
        @synchronized(lock) {
            [foo doSomething];
        }
        NSLog(@"Background pass %d complete.", i);
    }
});

for (int i=0; i<100; i++) {
    @synchronized(foo) {
        @synchronized(lock) {
            [foo doSomething];
        }
    }
    NSLog(@"Foreground pass %d complete.", i);
}

Il devrait être évident de voir pourquoi cela se produit. Le verrouillage sur foo et lock est appelé dans des ordres différents sur les threads d'arrière-plan VS de premier plan. Il est facile de dire que c'est une mauvaise pratique, mais si Foo est une bibliothèque, il est peu probable que l'utilisateur sache que le code contient un verrou.

9
Holly

@synchronized seul ne rend pas le thread de code sûr, mais c'est l'un des outils utilisés pour écrire du code thread-safe.

Avec les programmes multithreads, c'est souvent le cas d'une structure complexe que vous souhaitez conserver dans un état cohérent et que vous souhaitez qu'un seul thread ait accès à la fois. Le modèle courant consiste à utiliser un mutex pour protéger une section critique de code où la structure est accessible et/ou modifiée.

4
progrmr

@synchronized est thread safe mécanisme. Un morceau de code écrit à l'intérieur de cette fonction devient la partie de critical section, sur lequel un seul thread peut s'exécuter à la fois.

@synchronize applique le verrou implicitement tandis que NSLock l'applique explicitement.

Cela garantit seulement la sécurité du fil, pas la garantie. Ce que je veux dire, c'est que vous embauchez un chauffeur expert pour votre voiture, mais cela ne garantit pas que la voiture ne rencontrera pas d'accident. Cependant, la probabilité reste la plus faible.

Son compagnon dans GCD (grande répartition centrale) est dispatch_once. dispatch_once fait le même travail que pour @synchronized.

3
user3693546

Le @synchronized directive est un moyen pratique de créer des verrous mutex à la volée dans le code Objective-C.

effets secondaires des verrous mutex:

  1. impasses
  2. famine

La sécurité des threads dépendra de l'utilisation de @synchronized bloquer.

1
Parag Bafna