[obj-c] Les lazy loaders ne sont pas thread safe !
colas_
Membre
Hello !
Un message pour partager ce que j'ai découvert hier (!) : les lazy loaders ne sont pas thread safe !
- (CBDWelcomeViewController *)welcomeVC
{
if (!_welcomeVC)
{
_welcomeVC = [[CBDWelcomeViewController alloc] init] ;
}
return _welcomeVC ;
}
J'ai eu un cas hier. Le welcomeWC était appelé par deux threads différents. L'un des threads venait d'un appel de - viewDidLoad. L'autre du thread principal.
Bon à savoir.
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Dans ton code, on voit clairement que _welcomeVC peut être écrit par:
alors qu'on la lu dans le:
C'est un cas typique et facile à cerner !
Il vaut mieux utiliser dispatch_once() (voir le snippet dans Xcode).
@Céroce : je n'ai pas compris ta première remarque. En fait mon objet lazy loadé est appelé par deux méthodes en même temps, sur deux threads différents.
Si j'utilise le dispatch_once, que va-t-il se passer ? Je pense qu'un des deux appels va renvoyer nil.
C'est d'ailleurs pour cela que dispatch_once est aussi utilisé pour créer les sharedInstance de façon thread-safe
Voici un scénario:
- le thread 0 lit _welcomeVC. Comme la variable est nil, la condition du if est remplie.
- le thread 1 lit _welcomeVC. Comme la variable est nil, la condition du if est remplie.
- le thread 0 charge le VC et écrit dans _welcomeVC.
- le thread 1 charge le VC et écrit dans _welcomeVC.
Résultat: _welcomeVC pointe maintenant sur la deuxième instance du VC. Le thread 0 pointe sur la première instance, qui n'existe plus en mémoire puisqu'aucun objet ne la retient. Il y aura donc un plantage dès que le thread 0 essaiera d'utiliser le VC.
@ali @ceroce,
je voulais parler de
<<
Dans ton code, on voit clairement que _welcomeVC peut être écrit par:
_welcomeVC = [[CBDWelcomeViewController alloc] init] ;alors qu'on la lu dans le:
if (!_welcomeVC)C'est un cas typique et facile à cerner !
>>
Le code que j'ai mis dans le message #3 vous paraà®t-il un bon code pour les lazy loaders, en règle générale ? Il ne couvre pas tous les cas mais peut-il être considéré comme un bon code si l'objet ne doit être créé qu'une seule fois ?
Dans ce cas, j'utiliserai ce code de manière plus systématique.
Merci !
@Céroce, je ne suis pas sûr de te comprendre, _welcomeVC est une @property de ma classe.
Du coup, je vois le problème : le token est global à ma classe, alors que je ne souhaiterais avoir qu'un token par instance.
N'est-ce pas ?
@Ali : je n'ai jamais utilisé @synchronize. Peux-tu m'indiquer ce que je devrai faire dans ce cas-là ? Merci !
Ou mettre le "onceToken" en membre de la classe au lieu de variable statique dans la méthode.
Du coup est-ce que ce pattern est toujours aussi intéressant ?
Est-ce que ce n'est pas mieux de créer toutes les variables dans une seule méthode du controller qui sera appelée une fois dans awakeFromNib ou autre ? Finalement cela ne fait pas plus de code à écrire.
En terme de mémoire, je trouve qu'il vaut mieux tout allouer d'un coup, comme ça s'il y a risque de grosse consommation de mémoire on le saura. Plutôt que d'attendre une certaine combinaison d'appels à des getters pour constater qu'on déclenche un memory warning fatal.
Après c'est une question de vitesse, quand on travaille avec la watch, il peut être intéressant de bénéficier de l'étalement dans le temps des allocations procuré par le lazy loading, mais sur l'iPhone/iPad, je ne suis pas certain que cela fasse encore une différence.
Par contre ça a quand même du sens, et le pattern est intéressant, si l'objet est potentiellement volumineux et a quand même des chances de ne pas forcément être utilisé.
En pratique, je ne code quasiment jamais de lazy loaders (surtout en Objective-C). Ca ne vaut pas le coup de faire un lazy loader pour une bête NSArray de NSString ou autre, par exemple.
Par contre, ça peut commencer à avoir du sens si c'est par exemple une propriété "errorView" qui charge une vue d'un XIB dans le but d'afficher un message d'erreur à l'utilisateur. Peut-être que ce XIB est un peu costaud et long à charger, alors que si tout se passe bien et qu'il n'y a pas d'erreur dans le parcours utilisateur, elle ne sera jamais affichée à l'écran et jamais utilisée. Et du coup là ça a du sens de faire du lazy loading, pour ne pas charger le XIB pour rien, surtout si ce XIB est volumineux, avec beaucoup de vues dedans, chargement d'images qui ne sont utilisées que là , etc.
Le lazy loading est intéressant, mais il faut savoir ne pas en abuser inutilement non plus. Pour la plupart des propriétés, genre une NSString, un NSArray, un NSDictionary... ça reste bien souvent inutile de s'embêter. Ce n'est à faire que si c'est vraiment intéressant (et en évitant les optimisations prématurées !!) parce que la création de l'objet exposé par cette propriété est vraiment long à créer et volumineux en mémoire alors qu'il y a de fortes chances qu'il ne soit en fait jamais utilisé.
Mais par pitié, ne mettez pas du lazy loading partout à tord et à travers juste parce que vous avez appris le pattern et donc que vous avez tout à coup une folle envie de l'appliquer partout juste par principe pour montrer que vous connaissez ce pattern. Il n'y a rien de pire qu'un Design Pattern mal appliqué ou sur-appliqué pour rien, un bon Design Pattern ne s'applique QUE quand ça a du sens et est utile, pas à toutes les sauces pour montrer qu'on le connait.
Par exemple dans le cas de Colas, ça a du sens si son welcomeVC est son ViewController affichant un tutoriel au premier lancement, et ne s'affichant plus jamais ensuite, sauf si l'utilisateur tap sur un bouton "?" pour le réaffirmer à la demande mais ça n'arrivera sans doute pas tous les 4 matins. Du coup le welcomeVC risque dans 99% des cas de ne pas être utilisé ni affiché à l'écran car on ne consulte pas un tutoriel tous les 4 matins, et ça serait donc du coup un peu dommage d'instancier le WelcomeVC pour rien, surtout que potentiellement dans son "initWithNibName:bundle:" il fait peut-être plein de choses comme aller lire un fichier PLIST sur le disque pour charger les étapes du tutoriel et faire plein de calculs pour construire son tutoriel etc, donc autant les éviter si finalement c'est pas utilisé dans 99% des cas d'usage.
@FKDEV
je suis d'accord avec toi mais en même le lazy loading permet d'avoir un code plus structuré et plus simple à débuguer parfois. Mais, je suis tombé sur les fesses quand j'ai vu que ce n'était pas thread safe...
Zut, pourquoi on le dit pas ???
@Ali
En fait, n'ayant pas vu le talon d'Achille de ce pattern, j'ai eu tendance à l'appliquer trop souvent...
Quand on développe à haut-niveau d'abstraction, on a souvent tendance à ne penser qu'à la structure du code statique et à oublier la structure "dynamique", que se passe-t-il à l'exécution pour la mémoire et le temps cpu.
Un code où tu alloues tout au début et où tu libères tout à la fin a aussi une forme de structure. Si tu dois ajouter une variable, tu sais quoi faire: tu l'alloues au début et la libère à la fin. Après il y a les exceptions, comme décrit par Ali où ça n'a pas de sens d'allouer un truc lourd et lent au début si on sait qu'il ne sera surement pas utile.
C'est surtout que le lazy loading, ça passe bien dans les tutos, les samples et les démos car la complexité est délivrée au fur à mesure où on introduit les objets, comme dans la stack CoreData d'Apple par exemple.
En debug je ne trouve pas ça si pratique, si tu fais du step-in, tu tombes dans des accesseurs qui ne font rien que tester un truc qui a déjà été alloué et donc tu perds du temps.