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.
@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.
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
Thread-safe code est un code qui reste correct lorsqu'il est exécuté par plusieurs threads. Donc,
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:
Les deux premiers sont simples. Le troisième nécessite la prévention des problèmes de sécurité des threads suivants:
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.
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.
@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.
@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
.
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:
La sécurité des threads dépendra de l'utilisation de @synchronized
bloquer.