[Obj-c] Lazy-loading et thread-safe mutable dictionary

colas_colas_ Membre
juin 2015 modifié dans Objective-C, Swift, C, C++ #1

Bonjour,


est-ce que ce code est bon?


 


Il est censé créer des entrées dans un dictionnaire de façon lazy ET thread-safe. Je n'ai jamais utiliser @synchronized donc c'est pour savoir.


 


Merci!



@property (nonatomic, readwrite, strong) NSMutableDictionary * mutableDictionary;

[...]

- (id)objectForKey:(NSString *)key
{
id result = self.mutableDictionary[key];

if (!result)
{
@synchronized(self)
{
id result = self.mutableDictionary[key];
if (!result)
{
result = [...] ; // go and fetch the result;

self.mutableDictionary[key] = result;
}
}
}

return result ;
}

Réponses

  • Je dirais oui et non.


     


    Formellement oui.


    Le double accès au dictionnaire est un peu bizarre au premier abord. Je crois comprendre que c'est pour améliorer les performances : ne pas verrouiller de mutex si la clé est trouvée.


     


    Le lazy load à  l'intérieur d'un bloc protégé me paraà®t dangereux. S'il y a lazy load c'est que l'opération est longue, voire un résultat incertain, et du coup je pense que c'est à  éviter dans un bloc protégé.


  • FKDEVFKDEV Membre
    juin 2015 modifié #3
    Pour moi ce n'est pas bon. NSMutableDictionary n'est pas thread-safe donc tout accès doit être protégé, y compris la lecture au début.

    Tu pourrais très bien avoir un thread qui crée la clé pendant qu'un autre est en train de la tester, on ne sait pas trop ce qui va se passer dans le code de NSMutableDictionary à  ce moment là .

    Ton code serait correct si le valueForKey était une opération atomique.


    D'autre part il est dommage de mettre le fetch dans le synchronized car cela va bloquer les autres thread alors qu'il n'y a pas de menace sur les donnees partagees pendant ce fetch.


  • Pour moi ce n'est pas bon. NSMutableDictionary n'est pas thread-safe donc tout accès doit être protégé, y compris la lecture au début.

    Tu pourrais très bien avoir un thread qui crée la clé pendant qu'un autre est en train de la tester, on ne sait pas trop ce qui va se passer dans le code de NSMutableDictionary à  ce moment là .




     


    J'ai supposé que le dictionnaire était forcément mis à  jour dans cette méthode et jamais ailleurs, j'ai peut-être mal supposé.

  • @jpimbert tu as bien supposé !


     


    Pour répondre à  ta question



    @property (nonatomic, readwrite, strong) NSMutableDictionary * mutableDictionary;

    [...]

    - (id)objectForKey:(NSString *)key
    {
    id result = self.mutableDictionary[key];

    if (!result)
    {
    @synchronized(self)
    {
    id result = self.mutableDictionary[key];
    if (!result)
    {
    result = [...] ; // go and fetch the result;

    self.mutableDictionary[key] = result;
    }
    }
    }

    return result ;

    Le double-test c'est pour la raison suivante : si deux threads entrent au même moment dans la méthode et que le result est nil. Un seul rentrera dans le @synchronized (c'est du moins ce que j'imagine que fait @synchronized). Celui qui rentre va créer l'entrée du dico et l'autre attend. Quand finalement l'autre entre dans le @synchronized, il ne faut pas qu'il recrée l'entrée du dico. Donc, je re teste mais cette fois-ci, l'entrée n'est pas nil, car elle vient d'être crée par l'autre thread.


     


     


    @KDEV


     


    Je ne suis pas d'accord avec toi (mais je m'y connais mal, donc si je me trompe dis-moi)


     


    Dans ce pattern, la valeur du dico est fixée une fois pour toutes. Si thread1 lit valeurDico alors que thread2 écrit cette valeur, cela veut dire que thread2 est dans le @synchronized. Deux cas possibles :


     


    -> Si thread1 lit une valeur non-nil de valeurDico, c'est ok, c'est la bonne valeur.


    -> Si thread1 lit nil, il va entrer dans le @synchronized. Mais il doit attendre que thread2 sorte de @synchronized. Or, à  ce moment, la valeurDico sera non-nil et donc thread1 renverra la bonne valeur.


  • FKDEVFKDEV Membre

    Si thread2 est en train d'écrire la valeur du dico, pendant que thread1 essaye de la lire, on a potentiellement le cas suivant :


    thread2 est dans la méthode [NSMutableDictionary setValue:ForKey:]


    thread1 est dans la méthode [NSMutableDictionary value:forKey:]


     


    A partir de là , NSMutableDictionary n'étant pas thread safe (à  vérifier), on ne sait pas ce qui peut arriver aux états internes du NSMutableDictionary.




  • Le double-test c'est pour la raison suivante : si deux threads entrent au même moment dans la méthode et que le result est nil. Un seul rentrera dans le @synchronized (c'est du moins ce que j'imagine que fait @synchronized). Celui qui rentre va créer l'entrée du dico et l'autre attend. Quand finalement l'autre entre dans le @synchronized, il ne faut pas qu'il recrée l'entrée du dico. Donc, je re teste mais cette fois-ci, l'entrée n'est pas nil, car elle vient d'être crée par l'autre thread.




     


    Formellement le double test ne sert à  rien. Le code suivant marche tout aussi bien. 



    @property (nonatomic, readwrite, strong) NSMutableDictionary * mutableDictionary;

    [...]

    - (id)objectForKey:(NSString *)key
    {
    @synchronized(self)
    {
    id result = self.mutableDictionary[key];
    if (!result)
    {
    result = [...] ; // go and fetch the result;
    self.mutableDictionary[key] = result;
    }
    }
    return result ;
    }

    La seule différence est que chaque appel à  objectForKey va verrouiller le mutex, ce qui fait un peu perdre en performance.

  • colas_colas_ Membre
    juin 2015 modifié #8

    @jpimber


     


    Oui, le seul cas où l'on doit gérer le multithread est si on doit créer l'entrée du dico.




  • Si thread2 est en train d'écrire la valeur du dico, pendant que thread1 essaye de la lire, on a potentiellement le cas suivant :


    thread2 est dans la méthode [NSMutableDictionary setValue:ForKey:]


    thread1 est dans la méthode [NSMutableDictionary value:forKey:]


     


    A partir de là , NSMutableDictionary n'étant pas thread safe (à  vérifier), on ne sait pas ce qui peut arriver aux états internes du NSMutableDictionary.




     


    NSMutableDictionary n'est pas thread-safe (avec la déf de thread-safe qui dirait que si thread1 lit une valeur du dico, cette valeur sera la même s'il la relit plus tard).


     


    Mais, IMHO, on peut lire et écrire en même temps dans NSMutableDico (dans ce sens-là , il est thread safe). En fait, je n'en sais rien mais j'imagine que c'est comme ça !



  • Mais, IMHO, on peut lire et écrire en même temps dans NSMutableDico (dans ce sens-là , il est thread safe). En fait, je n'en sais rien mais j'imagine que c'est comme ça !




     


    Je devrais laisser Ali répondre à  ça, mais j'ai pitié des chats.


     


     


    Sur cette page, on peut lire.


     



     


    Mutable objects are generally not thread-safe. To use mutable objects in a threaded application, the application must synchronize access to them using locks. (For more information, see Atomic Operations). In general, the collection classes (for example, NSMutableArray, NSMutableDictionary) are not thread-safe when mutations are concerned. That is, if one or more threads are changing the same array, problems can occur. You must lock around spots where reads and writes occur to assure thread safety.



     


    Bon ça parle surtout de changements simultanés mais à  la fin du paragraphe, il est bien dit de protéger les lectures aussi.


    C'est logique car lors d'une écriture, les listes chaà®nées ou hash table ou autre pointeurs utilisés en interne de la classe mutable peuvent être chamboulés. Si une lecture simultanée utilise ces mêmes pointeurs alors elle risque de se retrouver le bec dans l'eau.

  • Hello,


     


    @Colas j'ai trouvé ça, ça pourrais t'intéresse !


     


    https://gist.github.com/steipete/5928916


  • zoczoc Membre

    J'ai supposé que le dictionnaire était forcément mis à  jour dans cette méthode et jamais ailleurs, j'ai peut-être mal supposé.



     

    Ca ne change rien à  l'affirmation de FKDEV avec laquelle je suis totalement d'accord. Par contre ta solution bien que plus coûteuse, est correcte.


     


    On peut imaginer le cas suivant (avec le code de Colas2) :


    • Thread 1 appelle la méthode de objectForKey, entre dans le bloc protégé par le mutex, puis exécute le code qui va rajouter l'entrée dans le dictionnaire et est interrompu avant d'avoir terminé.
    • Thread 2 appelle la même méthode, et exécute donc "id result = mutableDictionary[key];" alors que l'entrée est "à  moitié créée" (façon de parler).

    Dans cette situation le comportement est totalement imprévisible, car NSDictionnary n'est pas thread safe à  partir du moment où il y a une écriture (2 threads peuvent accéder à  un NSDictionnary en lecture en // car dans ce cas il est thread safe).


     


     


    Bon sinon il existe un excellent article de Mike Ash qui explique comment gérer correctement un cache d'objets à  l'aide de NSMutableDictionnary et GCD pour pallier à  sa "non thread-safitude" : https://mikeash.com/pyblog/friday-qa-2011-10-14-whats-new-in-gcd.html

  • Joanna CarterJoanna Carter Membre, Modérateur

    Bon sinon il existe un excellent article de Mike Ash qui explique comment gérer correctement un cache d'objets à  l'aide de NSMutableDictionnary et GCD pour pallier à  sa "non thread-safitude" : https://mikeash.com/pyblog/friday-qa-2011-10-14-whats-new-in-gcd.html




    Superb article. Le GCD et bien la solution la préférée.
Connectez-vous ou Inscrivez-vous pour répondre.