[obj-c] Les lazy loaders ne sont pas thread safe !

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.


Réponses

  • CéroceCéroce Membre, Modérateur
    Disons que les lazy loaders implémentés ainsi ne sont pas thread-safe.
    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 !

    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.



    - (CBDWelcomeViewController *)welcomeVC
    {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    _welcomeVC = [[CBDWelcomeViewController alloc] init] ;
    });

    return _welcomeVC ;
    }

  • AliGatorAliGator Membre, Modérateur
    Non car si je ne dis pas de bêtises dispatch_once est bloquant (si un thread est déjà  dans le code à  l'intérieur du dispatch_once et qu'un autre thread arrive, il va attendre que l'autre soit sorti pour faire le test), c'est tout son intérêt.


    C'est d'ailleurs pour cela que dispatch_once est aussi utilisé pour créer les sharedInstance de façon thread-safe
  • AliGatorAliGator Membre, Modérateur


    @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.

    Il voulait dire que ton titre n'est pas tout à  fait juste car ce n'est pas vraiment "les lazy loaders" qui ne sont pas thread-safe, mais seulement "cette façon d'implémenter les lazy loaders" qui ne l'est pas. Car on peut tout à  fait écrire des lazy loaders qui sont thread-safe ... du moment qu'on les écrit correctement (avec dispatch-once par exemple). C'est d'ailleurs le cas en Swift par exemple où les "lazy var toto = { ...code...}()" sont thread-safe.
  • CéroceCéroce Membre, Modérateur
    juin 2015 modifié #6

    @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.

    Oui, ce que je te dis est que je vois immédiatement que ton code n'est pas 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éroceCéroce Membre, Modérateur
    Oui, la deuxième version est thread-safe. Personnellement, je mettrais _welcomeVC en variable statique pour éviter de polluer le haut du .m avec les déclaration de globales, mais il me semble que c'est techniquement correct ainsi.
  • @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 ?


  • AliGatorAliGator Membre, Modérateur
    Effectivement du coup il y a un problème. Il faudra sans doute utiliser un lock (genre OSSpinLock ou un @synchronize) pour protéger l'accès (ou se mettre à  Swift :P)
  • @Ali : je n'ai jamais utilisé @synchronize. Peux-tu m'indiquer ce que je devrai faire dans ce cas-là  ? Merci !


  • zoczoc Membre


    Effectivement du coup il y a un problème. Il faudra sans doute utiliser un lock (genre OSSpinLock ou un @synchronize) pour protéger l'accès (ou se mettre à  Swift :P)




     


    Ou mettre le "onceToken" en membre de la classe au lieu de variable statique dans la méthode.

  • FKDEVFKDEV Membre
    juin 2015 modifié #13

    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.


  • AliGatorAliGator Membre, Modérateur
    Je suis d'accord avec FKDEV sur le fait que les lazy loaders ne sont pas si intéressants que ça en général, surtout pour les propriétés qui ne sont finalement pas gourmandes en mémoire.
    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... 


  • FKDEVFKDEV Membre
    juin 2015 modifié #17


    @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...


     




     


     


    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.


Connectez-vous ou Inscrivez-vous pour répondre.