NSArray et NSThread

FloFlo Membre
05:54 modifié dans API AppKit #1
Bonjour,

J'ai entendu ici et là  que la classe NSArray était thread safe...
Cela signifie-t-il que par exemple si j'ai un premier thread qui parcours un NSArray (via les méthodes du NSFastEnumeration protocol) et qu'un deuxième tente d'insérer un objet en même temps, ce dernier sera bloqué jusqu'à  la fin du parcours opéré par le premier ?

Ou dois-je moi même gérer ce genre de synchronisme via NSLock ?
«1

Réponses

  • Philippe49Philippe49 Membre
    avril 2009 modifié #2
    Voilà  ce que je lis dans la doc :

    There are several advantages to using fast enumeration:

    The enumeration is considerably more efficient than, for example, using NSEnumerator directly.
    The syntax is concise.
    Enumeration is “safe”"the enumerator has a mutation guard so that if you attempt to modify the collection during enumeration, an exception is raised.

    Since mutation of the object during iteration is forbidden, you can perform multiple enumerations concurrently.


    Je lis donc que l'on peut lire la même collection en concurrence éventuelle dans plusieurs threads puisque changer la collection dans la boucle provoque une exception.

  • FloFlo Membre
    05:54 modifié #3
    Merci ! Ce que je peux me sentir bigleux par fois...

    Néanmoins ça m'arrange pas des masses cette affaire. Ce que je voudrais moi c'est que si TH2 essaye d'ajouter dans le NSArray pendant que TH1 le parcours et ben TH2 attente la fin du parcours de TH1.

    J'ai lu des choses sur NSLock, serait-il possible de l'utiliser pour que quand TH2 fasse un lock il soit mis en attente jusqu'à  ce que TH1 ai fait un unlock ?

    Si ça marche ce n'est possible que dans ce sens il me semble :

    When sending an unlock message to an NSLock object, you must be sure that message is sent from the same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior

  • schlumschlum Membre
    05:54 modifié #4
    Une exception, c'est pas difficile à  catcher sinon...
  • AliGatorAliGator Membre, Modérateur
    05:54 modifié #5
    Ouais mais le plus simple en effet à  mon avis c'est d'utiliser des mutex/locks/...
    Plutôt que NSLock moi je verrais plutôt une directive [tt]@synchronized(...) { ... }[/tt] (qui a grosso modo le même but que NSLock, protéger un bout de code contre les accès concurrenciels par plusieurs threads, mais est je trouve pour ce genre de situation plus simple à  utiliser.

    En l'occurence, comme c'est l'objet passé à  @synchronized(...) qui sert pour identifier/différencier le lock ou mutex sous-jacent créé, autrement dit @synchronized( a ) va créer un mutex associé à  l'objet a, donc tous les codes entourés de @synchronized( a ) ne pourront être accédés que chacun son tour et pas concurrenciellement. Donc comme objet, tu peux par exemple mettre justement le NSArray auquel tu veux accéder.

    // soit tab un NSArray* accedé par plusieurs threads<br /><br />// thread 1 :<br />@synchronized(tab)<br />{<br />&nbsp; foreach(id obj in tab)<br />&nbsp; { ... boucle qui fait ce que tu veux, lecture et/ou modification de ton tableau<br />&nbsp; }<br />}<br /><br />// thread 2 :<br />@synchronized(tab)<br />{<br />&nbsp; NSUInteger n = [tab count];<br />}
    
    Avec ce genre de truc ça devrait faire les choses propres je pense, en encadrant tous tes accès (en lecture et/ou écriture) à  ton tableau tab par @synchronized(tab)... enfin si j'ai bonne mémoire, faudrait relire la doc pour confirmer, mais bon, l'idée est là .
  • FloFlo Membre
    avril 2009 modifié #6

    Une exception, c'est pas difficile à  catcher sinon...


    Ouais mais l'exception, elle est envoyée dans le thread1 qui parcours le NSArray ou dans le thread2 qui essaye d'y accéder pendant qu'il est parcouru par thread1 ?

    L'idéal ce serait dans thread2...


    Ouais mais le plus simple en effet à  mon avis c'est d'utiliser des mutex/locks/...


    Pas bête , j'avais oublié @synchronised... mais ces mécanismes ne sont pas mis de base dans NSArray ?

    [EDIT] Enfin sauf quand on est dans les méthodes du NSFastEnumeration protocol biensur...
  • schlumschlum Membre
    05:54 modifié #7
    Euh... ben l'exception est envoyée au moment de l'insertion, ou alors j'ai des problèmes de logique  :P
  • FloFlo Membre
    05:54 modifié #8
    C'est pas mal ta solution Ali, le truc c'est qu'en fait le thread qui parcours le tableau est un background update à  intervalle régulier.

    Si je comprends bien et que j'entoure toutes les sections critiques de code de modifications/accès du tableau ça va bloquer les autres threads (genre le principal qui accède au tableau pour l'affichage des éléments du tableau (GUI))...

    A chaque mise à  jour du tableau, l'affichage sera bloqué, l'utilisateur pourra plus rien faire dessus jusqu'à  la fin de la maj... pour peu que ça connexion internet soit pas tip top il peut aller se prendre un café  :-\\

    Comme en plus c'est des mises à  jour environ toutes les deux minutes...

    Yaurait pas moyen d'optimiser tout ça ?
  • Philippe49Philippe49 Membre
    05:54 modifié #9
    Dans l'adversité, soyons courageux : fuyons ...
    Une solution en dédoublant les tableaux, et en faisant la mise à  jour une fois les threads terminés, cela ne te va pas ?
  • FloFlo Membre
    05:54 modifié #10

    Dans l'adversité, soyons courageux : fuyons ...

    :o


    Une solution en dédoublant les tableaux, et en faisant la mise à  jour une fois les threads terminés, cela ne te va pas ?


    Ben c'est que l'est pas tout p'tit le bazar ! J'me vois mal le copier et le bazarder ensuite...

    Sinon je peux décider de ne pas mettre les fonctions de lectures du tableau en section critique pour ne pas bloquer les méthode du NSOutlineView data source protocol et donc l'affichage des éléments du tableau...

    Qu'en pensez-vous ?
  • schlumschlum Membre
    avril 2009 modifié #11
    Je te conseille de faire les insertion dans ce tableau dans le thread principal en utilisant "performSelectorOnMainThread", comme ça tu ne prends aucun risque de télescopage.

    ça doit être un réflexe de faire tout ce qui peut causer une modification dans la GUI au niveau du thread principal.

    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
    


    Tu mets "waitUntilDone" à  false, comme ça tu n'as même pas à  attendre (je ne sais pas comment ça fonctionne... ça passe sans doute par les events).
  • FloFlo Membre
    05:54 modifié #12
    En fait j'ai fait un mix des solutions, j'ai divisé mon gros tableau en plusieurs petits... Mon thread de "background update" prends les petits tableaux un par un en en faisant une copie à  chaque fois et met les éléments à  jour.

    Vous en pensez quoi ?

    Sinon avec cette solution, dois-je toujours utiliser @synchronize sur chacun des petit tableaux ?

    Le seul cas que je vois de problématique c'est quand le thread de "background update" de demande une copie d'un petit tableau et qu'en même temps le thread principal y accède/modifie... au pire si la copie se fait avant, l'éléments ne sera mis à  jour que la prochaine fois qu'on recopie ce même petit tableau...

    [EDIT] Désolé pour le retard, j'étais en Normandie pour le wee de pâques... (voilà , vous savez tout...;-))
  • schlumschlum Membre
    05:54 modifié #13
    Et si c'est modifié au moment où tu copies ?  :o
    Tu crois pas t'en sortir à  si bon compte quand même  :P

    Le truc que je t'ai donné avec le "performSelectorOnMainThread" c'est un grand classique... c'est ce qui est fait partout (pour les graphiques en particulier...).
  • FloFlo Membre
    05:54 modifié #14

    Le truc que je t'ai donné avec le "performSelectorOnMainThread" c'est un grand classique... c'est ce qui est fait partout (pour les graphiques en particulier...)


    Ok, donc si je comprends bien il faudrait que je lance la mise à  jour dans le thread principal même si on doit attendre que cette dernière soit terminée pour l'affichage et les modifs ? Si la maj prends un peu de temps ça risque pas de figer l'appli et de faire penser à  un bug ?
  • schlumschlum Membre
    avril 2009 modifié #15
    Ce genre de modifs n'est pas censé prendre du temps... Même s'il y a 50 objets à  insérer, ça prend même pas 1/10e de seconde.
    S'il y a des calculs à  faire, il faut qu'ils soient fait avant dans le thread de calcul avec éventuellement gestion d'un tableau à  merger ensuite au principal.

    Si vraiment il y a beaucoup de changements, que ça prend beaucoup de temps etc., travailler sur une copie, puis demander au thread principal de faire le switch entre les deux.
  • FloFlo Membre
    05:54 modifié #16
    Le problème c'est pas la mise à  jour en elle-même, c'est le fait qu'il faille récupérer les infos à  partir d'internet...

    Honnêtement, ça me gène beaucoup de monopoliser le thread principal pour faire ça, il me semble plus judicieux de faire travailler un thread concurrent sur une copie... comme ça le thread principal peut toujours ajouter des éléments/afficher les éléments du tableau pendant que le thread concurrent les met à  jour toujours en travaillant sur une nouvelle copie à  chaque fois qu'il se déclenche (environs toutes les 2 minutes).

    Le seul truc à  faire c'est de protéger le tableau au moment où le thread concurrent en fait une copie avec @synchronized...
  • schlumschlum Membre
    05:54 modifié #17
    Mais la récupération sur Internet ne doit se faire en aucun cas dans le thread principal... C'est juste l'insertion des nouveaux éléments qui doit.
  • FloFlo Membre
    05:54 modifié #18
    Ok, je vois ce que tu veux dire...

    en gros là  où moi je vois une tache monolithique(télécharger + mettre à  jour dans un thread concurrent) toi tu proposes plutôt de fragmenter le processus pour ne laisser que la mise à  jour des éléments au thread principal.

    En fait ma mise à  jour ce passe en trois étapes :
    - construction de l'URL à  partir des éléments du tableau (thread principal ?)
    - téléchargement des données sur internet (thread concurrent)
    - mise à  jour des éléments concernés (thread principal)

    Je commence à  y voir un peu plus clair...
  • FloFlo Membre
    avril 2009 modifié #19
    Du coup j'aurai une autre petite question si votre patience peut encore en supporter... comment je synchronise le tout ?

    Ce qu'il me semble bon de faire :
    - 1: dans le thread principal construire l'URL à  partir des éléments du tableau puis lancer un thread concurrent.
    - 2: dans un thread concurrent lancer le téléchargement des données depuis internet
    - 3: une fois le thread concurrent terminée mettre à  jour les éléments du tableau concernés.

    Mais comment faire en sorte de démarrer l'étape 3 quand l'étape 2 est terminée ? Une notification ?
    Comment transférer les données récupérées depuis le thread concurrent au thread principal ? var globale ?

    Bon daccord ça fait plus d'une question mais que voulez vous... j'en profite !
  • schlumschlum Membre
    05:54 modifié #20
    dans 1239812746:

    Mais comment faire en sorte de démarrer l'étape 3 quand l'étape 2 est terminée ? Une notification ?


    J'ai pas dû le dire assez de fois...  :P "performSelectorOnMainThread"...

    Comment transférer les données récupérées depuis le thread concurrent au thread principal ? var globale ?


    Un objet auto-released en mettant YES pour "waitUntilDone"
  • FloFlo Membre
    05:54 modifié #21
    J'essayais de t'arracher un petit exemple mais bon sans grand succès apparemment  :P

    J'ai pensé à  ça :

    PHASE 1
    <br />- (void) backUpdate<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; // url pointant sur les maj <br />	NSURL *url = [ITSymbol URLForSymbols: [dataController symbols]];<br />	<br />	[NSThread detachNewThreadSelector: @selector(download:) toTarget: self withObject: url];<br />}<br />
    


    PHASE 2
    <br />- (void) download: (NSURL *)anURL<br />{<br />	NSString *updates = [NSString stringWithContentsOfURL: anURL];<br />	<br />	[self performSelectorOnMainThread: @selector(update:) withObject: updates waitUntilDone: NO];<br />}<br />
    


    PHASE 3
    <br />- (void) update: (NSString *)updates<br />{<br />	//mise à  jour du tableau<br />}<br />
    


    C'est à  peu près ça que tu évoquais ou alors pas du tout ?
  • schlumschlum Membre
    05:54 modifié #22
    Oui, c'est bien ça, sauf que "waitUntilDone" doit être à  "YES", sinon "updates" va disparaà®tre avec la fin du thread, et il y a des chances (malchances !) pour que ça arrive pendant qu'il est utilisé pour l'update !
  • FloFlo Membre
    05:54 modifié #23
    C'est vrai que cette solution à  l'avantage de m'éviter de gérer les mécanismes tels que les mutex/NSLock et autres... En plus la partie potentiellement problématique est exécuté dans un thread concurrent ce qui à  l'avantage de ne pas bloquer le thread principal quand la connexion internet est lente par exemple...

    Le seul problème que je vois c'est que l'utilisateur peut ajouter des nouveaux éléments dans le tableau pendant la phase 2 et donc les updates fournis à  la phase 3 ne prennent pas en compte ces nouveaux éléments. La méthode update() va parcourir le tableau qui contient potentiellement des éléments non concernés par la chaà®ne (NSString *)updates.

    Il faut que je prévois un test de contrôle pour savoir si oui ou non un élément peut être mis à  jour par la chaà®ne updates (ou si ce même élément à  été pris en compte lors de la précédente construction d'url).

    ça va alourdir un peu, mais c'est peut-être rien à  côté du gain réalisé grace à  l'économie des outils de synchronisation.

    Merci en tous cas !
  • schlumschlum Membre
    avril 2009 modifié #24
    dans 1239874399:

    Le seul problème que je vois c'est que l'utilisateur peut ajouter des nouveaux éléments dans le tableau pendant la phase 2 et donc les updates fournis à  la phase 3 ne prennent pas en compte ces nouveaux éléments. La méthode update() va parcourir le tableau qui contient potentiellement des éléments non concernés par la chaà®ne (NSString *)updates.


    Ce que je fais dans ce cas (utilisé pour SudokuX), c'est que chaque modification utilisateur relance le thread... Une i-var pointeur conserve le pointeur du dernier thread lancé (affectation atomique, donc pas besoin de mutex), et chaque thread teste à  des endroits stratégiques s'il est le dernier thread et stoppe s'il ne l'est pas...
    ça permet aussi de mettre une temporisation au début de la méthode du thread avant le test pour bufferiser les modifications utilisateur s'il y en a plusieurs rapprochées.

    "[NSThread currentThread]" permet de récupérer le pointeur du thread courant, pour à  la fois l'update du pointeur " dernier thread " et pour les tests.

    J'ai mis longtemps à  concevoir cet algo de fonctionnement, mais j'en suis plutôt satisfait  :P
  • FloFlo Membre
    05:54 modifié #25

    J'ai mis longtemps à  concevoir cet algo de fonctionnement, mais j'en suis plutôt satisfait 


    Tu peux ! c'est très intéressant !
    Je suppose, à  la lecture de ce que tu as écris, que tu fais les ajouts utilisateur dans un nouveau thread ?
  • schlumschlum Membre
    05:54 modifié #26
    Non, les ajouts utilisateurs proviennent d'événements UI, donc ce sont des events dans le thread principal (c'est pas plus mal comme ça, on évite à  nouveau les collisions avec l'affichage...)
    Par contre, si les ajouts utilisateurs nécessitent des calculs longs, il vaut mieux passer par un thread pour ne pas coincer l'interface oui (tout en faisant le merge final dans le thread principal).
  • FloFlo Membre
    05:54 modifié #27
    Ok, alors voyons si j'ai à  peu près compris :

    j'ai déclaré une property pour mon controller :
    <br />@property(assign) NSThread *lastThread;<br />
    


    PHASE 1
    <br />- (void) backUpdate<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; self.lastThread = [NSThread currentThread];<br /><br />&nbsp; &nbsp; &nbsp; &nbsp; // url pointant sur les maj <br />	NSURL *url = [ITSymbol URLForSymbols: [dataController symbols]];<br />	<br />	[NSThread detachNewThreadSelector: @selector(download:) toTarget: self withObject: url];<br />}<br />
    


    PHASE 2
    <br />- (void) download: (NSURL *)anURL<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; self.lastThread = [NSThread currentThread];<br /><br />	NSString *updates = [NSString stringWithContentsOfURL: anURL];<br />	<br />	[self performSelectorOnMainThread: @selector(update:) withObject: updates waitUntilDone: YES];<br />}<br />
    


    PHASE 3
    <br />- (void) update: (NSString *)updates<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; if (lastThread != [NSThread currentThread])<br />	{<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.lastThread = [NSThread currentThread];<br /><br />		// mise à  jour du tableau<br />	}<br />	else [self backUpdate];<br />}<br />
    


    AJOUT UTILISATEUR (Dans le thread principal)
    <br />- (void) add<br />{<br />&nbsp; &nbsp;  self.lastThread = [NSThread currentThread];<br /><br />&nbsp; &nbsp;  // ajout d&#39;un objet dans le tableau<br />}<br />
    


    En gros s'il y a eu un ajout pendant le téléchargement, la variable lastThread pointe sur le Thread principal et la méthode update ne mettra pas à  jour le tableau mais relancera la méthode backUpdate.

    C'est ça l'idée ou pas du tout ?
  • schlumschlum Membre
    05:54 modifié #28
    "lastThread" doit pointer sur nil au début, puis elle est mise à  jour à  chaque début du thread uniquement...

    - (void) download: (NSURL *)anURL<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; self.lastThread = [NSThread currentThread];<br /><br />	NSString *updates = [NSString stringWithContentsOfURL: anURL];<br />	<br />	if([NSThread currentThread]==self.lastThread)<br />		[self performSelectorOnMainThread: @selector(update:) withObject: updates waitUntilDone: YES];<br />	[NSThread exit];<br />}
    


    - (void) backUpdate<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; // url pointant sur les maj <br />	NSURL *url = [ITSymbol URLForSymbols: [dataController symbols]];<br />	<br />	[NSThread detachNewThreadSelector: @selector(download:) toTarget: self withObject: url];<br />}
    


    - (void) update: (NSString *)updates<br />{<br />	// mise à  jour du tableau<br />}
    


    - (void) add<br />{<br />&nbsp; &nbsp;  // ajout d&#39;un objet dans le tableau<br />&nbsp; &nbsp;  [self backUpdate];<br />}
    


    Mais ça n'est valable que si une modification utilisateur invalide la mise à  jour en cours... Sinon il faut un mécanisme de cumulation.
  • FloFlo Membre
    05:54 modifié #29

    Mais ça n'est valable que si une modification utilisateur invalide la mise à  jour en cours...


    En réalité si un élément est inséré pendant la mise à  jour en cours, il faut stopper cette dernière et la relancer pour qu'elle le prenne en compte.

    Mais l'ajout d'un élément ne doit pas déclencher de mise à  jour :
    <br />- (void) add<br />{<br />&nbsp; &nbsp;  // ajout d&#39;un objet dans le tableau<br />&nbsp; &nbsp;  [s][self backUpdate];[/s]<br />}<br />
    


    Que penses-tu de ça ? :

    <br />- (void) download: (NSURL *)anURL<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; self.lastThread = [NSThread currentThread];<br /><br />	NSString *updates = [NSString stringWithContentsOfURL: anURL];<br />	<br />	if([NSThread currentThread]==self.lastThread)<br />		[self performSelectorOnMainThread: @selector(update:) withObject: updates waitUntilDone: YES];<br />&nbsp; &nbsp; &nbsp; &nbsp; else<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [self performSelectorOnMainThread: @selector(backUpdate:) withObject: nil waitUntilDone: NO];<br /><br />	[NSThread exit];<br />}<br />
    


  • schlumschlum Membre
    avril 2009 modifié #30
    Donc si je comprends bien, l'ajout d'un élément ne doit remettre à  jour que s'il y a une mise à  jour en cours...

    Auquel cas, c'est à  peu de choses près le code que j'avais mis :

    - (void) download: (NSURL *)anURL<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; self.lastThread = [NSThread currentThread];<br /><br />	NSString *updates = [NSString stringWithContentsOfURL: anURL];<br />	<br />	if([NSThread currentThread]==self.lastThread) {<br />		[self performSelectorOnMainThread: @selector(update:) withObject: updates waitUntilDone: YES];<br />		self.lastThread = nil;<br />	}<br />	[NSThread exit];<br />}
    


    - (void) add<br />{<br />&nbsp; &nbsp;  // ajout d&#39;un objet dans le tableau<br />&nbsp; &nbsp;  if(self.lastThread!=nil)<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [self backUpdate];<br />}
    


    Le code que tu as mis au message précédent ne relance jamais le thread en cas de modification utilisateur.
  • schlumschlum Membre
    avril 2009 modifié #31
    Par contre, là  ça introduit un risque de concurrence sur "self.lastThread"
    Le thread peut le mettre à  "nil" alors qu'il a été setté par un autre thread déjà , ce qui est problématique (ça flanque à  l'eau la mise à  jour du thread qui aurait alors été lancé)...
    Pour pallier à  ça, faire la mise à  jour aussi si self.lastThread==nil pourrait être la solution. À y réfléchir si ça n'introduit pas d'effet de bord désagréable...

    - (void) download: (NSURL *)anURL<br />{<br />&nbsp; &nbsp; &nbsp; &nbsp; self.lastThread = [NSThread currentThread];<br /><br />	NSString *updates = [NSString stringWithContentsOfURL: anURL];<br />	<br />	if([NSThread currentThread]==self.lastThread||self.lastThread==nil) {<br />		[self performSelectorOnMainThread: @selector(update:) withObject: updates waitUntilDone: YES];<br />		self.lastThread = nil;<br />	}<br />	[NSThread exit];<br />}
    
Connectez-vous ou Inscrivez-vous pour répondre.