Probleme NSThread et accesseurs

Bonjour à  tous,



Je poste ce message parce que je rencontre un problème sur lequel je sèche complètement Voila le contexte : je suis en train de faire mes armes sur le développement Cocoa en créant un mini lecteur MP3 très basique. J'ai donc un fenêtre qui s'ouvre avec un bouton qui me permet de sélectionner un fichier audio et un bouton qui me permet de lire ce fichier. Tout ceci fonctionne plutôt bien.

Pendant la lecture, je souhaite mettre à  jour un NSSlider ainsi qu'un champ texte sur ma fenêtre indiquant le temps. Comme cette mise à  jour doit se faire en continue j'ai mis en place, non sans mal, un Thread qui exécute une méthode pour faire avancer le slider et changer le valeur du texte.

Maintenant, et c'est la que les ennuis commencent, je voudrais que lorsque je change la valeur de mon slider en cliquant directement dessus mon instance de NSSound utilise cette valeur pour se mettre à  jour et joue ma musique à  partir du temps t (une sorte d'avance rapide en somme). J'en arrive donc a mon problème : mon thread s'exécutant en tâche de fond met à  jour la valeur du slider avec le temps courant de mon instance NSSound avant ma méthode "avance rapide". Ainsi quand elle s'exécute la valeur du slider n'est pas celle que j'ai indiquée en cliquant dessus mais celle mis à  jour par le thread, celle du temps avant le clique donc (je l'admets c'est un peu confus).

Ce que je cherche à  faire c'est qu'au moment ou je clique sur mon slider, mon thread s'arrête, laisse ma méthode "avance rapide" s'exécuter et reprenne son travail après J'ai bien essayé de le stopper au début de la méthode avance rapide mais la mise a jour se fait avant, j'ai aussi réglé la priorité du thread sans grand résultat.

Voila je fais donc appel à  vos lumières. J'en profite aussi pour vous demander si le thread est une bonne idée ou s'il existe d'autre moyen d'exécuter des méthode en tâche de fond sans bloquer l'application



Merci pour aide,



Vincent

Réponses

  • AliGatorAliGator Membre, Modérateur
    'vincentf213' a écrit:


    Pendant la lecture, je souhaite mettre à  jour un NSSlider ainsi qu'un champ texte sur ma fenêtre indiquant le temps. Comme cette mise à  jour doit se faire en continue j'ai mis en place, non sans mal, un Thread qui exécute une méthode pour faire avancer le slider et changer le valeur du texte.
    C'est à  mon avis là  ton erreur.



    En pratique, c'est déconseillé d'utiliser les NSThread. Il existe des techniques bien plus avancées pour faire de l'exécution parallèle. Manipuler les threads directement n'est pas simple car il y a plein de choses à  prendre en considérations, protection de la mémoire, accès concurrents au resources, etc. Il y a d'autres outils bien plus adaptés pour ton cas qui t'éviterons bien des déboires.



    Déjà , pour ton cas, un NSTimer suffit amplement. Il va te permettre de demander d'exécuter un bout de code toutes les N secondes, pour mettre à  jour ton interface. Dans ton cas c'est exactement ce qu'il te faut. Et c'est 1000x plus simple qu'un NSThread (ça n'a même rien à  voir, bien plus simple à  manipuler). Tu peux demander au NSTimer de se lancer, l'arrêter quand tu as besoin, le recréer quand tu veux redémarrer ton timer, etc...

    Et ça ne crée pas de thread dédié qui prendrait des ressources inutiles (swapping de contexte mémoire, etc, beaucoup d'overhead pour rien si tu utilisais un NSThread), car ça s'installe tout seul automatiquement sur la RunLoop.



    Après, pour ta culture, si un jour tu avais vraiment à  exécuter du code en tâche de fond, façon thread, de toute façon ne pars pas direct sur NSThread. Car ce n'est pas la solution la plus simple et l'API la plus adaptée la plupart du temps. Il y a d'autres possibilités, comme bien souvent déjà  des APIs asynchrones existant pour ce que tu veux faire, ou des NSOperation, ou GCD... qui sont toutes plus faciles à  utiliser qu'un NSThread et évitent bien des problèmes que tu peux avoir avec ceux-ci vu les considérations que tu dois prendre en compte avec NSThread.



    Apple a une doc dédiée sur le sujet, et entre autres un paragraphe dédié "Migrating away from Threads" qui explique qu'il y a bien d'autres façons de faire maintenant avec les API modernes pour utiliser autre chose que les threads, du moins de façon directe avec NSThread
  • Ha oui cela semble en effet bien plus approprié. Je n'étais pas au fait de l'utilisation, j'avais compris que c'étais compliqué mais là  j'ai un bon exemple de cette complexité. Je vais donc revoir tout ça et passer au NSTimer. Si je comprends bien, j'initialise puis démarre un NSTimer au début de la lecture pour une exécution disons toutes les 0.5 secondes. Quand je mets à  jour mon slider directement, la première chose à  faire c'est d'arrêter le timer, faire ce que j'ai a faire puis reprendre mon timer.

    Merci pour l'aide et pour le lien je vais lire ça un peu plus en détail. Je ne peux pas tester la solution maintenant mais je reviendrai vous tenir au courant.
  • CéroceCéroce Membre, Modérateur
    octobre 2012 modifié #4
    Et une précision qu'Ali a oublié: l'interface utilisateur ne doit être manipulée que par le thread principal. (NSTimer tourne d'ailleurs dans le thread principal par défaut).
  • AliGatorAliGator Membre, Modérateur
    octobre 2012 modifié #5
    'vincentf213' a écrit:
    Si je comprends bien, j'initialise puis démarre un NSTimer au début de la lecture pour une exécution disons toutes les 0.5 secondes. Quand je mets à  jour mon slider directement, la première chose à  faire c'est d'arrêter le timer, faire ce que j'ai a faire puis reprendre mon timer.
    Même pas, c'est encore plus simple que ça. Au début tu crées un timer qui a une période de 0.5s (et qui a son mode "repeat" activé pour qu'il tourne non-stop), qui va déclencher une méthode à  chaque appel. Et dans cette méthode tu mets à  jour ton slider. C'est tout.



    Pas besoin d'arrêter le timer avant et de le relancer après. Pas de risque de "Race Condition" comme tu peux avoir sur les NSThreads, car un NSTimer ne génère pas un thread séparé, il est exécuté par la RunLoop (par défaut, comme l'a mentionné Céroce, sur la RunLoop du thread principal, ou plus exactement du thread sur lequel tu as installé ton timer mais en général tu le fais dans le Main Thread de toute façon)


    -(void)viewDidLoad {<br />
      [super viewDidLoad];<br />
      // Créer un timer, immédiatement installé (scheduled) sur la RunLoop (= immédiatement lancé)<br />
      // Une fois le timer créé par cette méthode, il est &quot;scheduled&quot; sur la RunLoop (qui va le retenir -- &quot;retain&quot;) et sera donc exécuté dès la prochaine itération<br />
      self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgression:) userInfo:nil repeats:YES];<br />
    }<br />
    <br />
    // En cas de memory warning où la vue serait relâchée (pour être recréée plus tard), en profiter pour arrêter le timer aussi<br />
    -(void)viewDidUnload {<br />
      [super viewDidUnload]<br />
      [self.updateTimer invalidate]; // Enlève le Timer de la RunLoop, donc arrête de le déclencher en fait<br />
      self.updateTimer = nil; // Et on le relâche puisqu&#39;il sera recréé au prochain viewDidLoad<br />
    }<br />
    -(void)dealloc {<br />
      [self.updateTimer invalidate]; // Enlève le Timer de la RunLoop, donc arrête de le déclencher en fait<br />
      self.updateTimer = nil; // Et on le relâche pour libérer la mémoire<br />
      [super dealloc];<br />
    }<br />
    <br />
    -(void)updateProgression:(NSTimer*)timer {<br />
       // Cette méthode va donc être appelée automatiquement toutes les 0.5 secondes<br />
       self.slider.value = ...<br />
    }
    
    Et c'est tout ! Pas de threads, pas de gros code à  gérer ou de start/stop ou quoi, juste un NSTimer qui déclenche une méthode régulièrement, que tu crées au début et que tu arrêtes à  la fin.
  • Ok ça me semble assez clair merci pour les précisions. Cependant un petit truc me turlupine : les méthodes viewDidLoad et viewDidUnload sont disponnibles pour quelles classes ? Sont elles disponibles pour chaque fenêtre créée ? Je suis un peu perdu sur ce point image/sad.png' class='bbc_emoticon' alt=':(' />
  • CéroceCéroce Membre, Modérateur
    octobre 2012 modifié #7
    Aligator a été un peu vite, et ne s'est pas rendu compte que nous étions sous OS X, où viewDidLoad et viewDidUnload (qui sont des méthodes de UIViewController) n'existent pas.



    En gros, sous OS X, l'équivalent de viewDidLoad est -awakeFromNib. Je dis, en gros, parce que c'est équivalent fonctionnellement, mais pas dans le principe.



    (La méthode awakeFromNib d'un objet est appelée lorsqu'un objet est instancié depuis un fichier .nib. Mais elle est également appelée sur le File's Owner, c'est à  dire l'objet à  l'origine du chargement du fichier nib. Le File's Owner sera donc le NSWindowController ou le NSViewController).
  • AliGatorAliGator Membre, Modérateur
    octobre 2012 modifié #8
    Ah oui pardon c'est ma faute. Les méthodes viewDidLoad et viewDidUnload sont des méthodes de UIViewController, le pendant de NSViewController mais sous iOS. Et je suis tellement habitué à  bosser sous iOS...



    Mais bon, le principe de mon exemple c'était juste pour te montrer :
    • comment créer un NSTimer (ce que j'ai fait dans viewDidLoad, mais que toi tu feras peut-être dans le init de ta classe qui gère la mise à  jour de ton slider, ou quand tu lancera ton lecteur audio typiquement)
    • comment l'invalider ce qu'il faut absolument penser à  faire dans le dealloc (sinon ton timer continuera de tourner mais va appeler une méthode "updateProgression" sur un object self qui aura été désalloué depuis, donc ça va planter), et que toi tu feras dans toute méthode où tu aurais besoin d'arrêter ton timer (par exemple pourquoi pas quand ton lecteur audio est stoppé ou mis en pause)


    Voilà  j'espère que c'est plus clair image/wink.png' class='bbc_emoticon' alt=';)' />



    Donc en gros tu as juste à  utiliser la méthode "scheduleTimer..." pour créer ton timer (et définir sa période + la méthode à  appeler à  chaque itération) et la méthode "invalidate" (+ remise à  nil) pour l'arrêter.
  • Je connaissais le awakeFromNib (qui m'est revenu pendant que j'étais en train de déjeuner) mais le viewDidLoad ne me disais rien. Je pense qu'avec toutes ces explications je suis paré.



    Un grand merci pour toutes ces précisions.
  • Bon ben voila ça fonctionne image/smile.png' class='bbc_emoticon' alt=':)' /> mais parce que bien sur il y a un mais lorsque je regarde dans le moniteur d'activité mon application me prend 64 mega de mémoire ce que je trouve quand même assez énorme... Avez vous des pistes pour optimiser ?
  • AliGatorAliGator Membre, Modérateur
    Le mieux est :
    1. De lancer une analyse statique de ton code (menu Product -> Analyze) qui va te remonter tout plein de warnings sur des mauvaises gestions mémoire potentiels par exemple, etc. le Clang Static Analyzer qui analyse ton code ne trouve pas forcément tout (quoique plus les versions de Xcode avant plus il est puissant quand même !) mais par contre tout ce qu'il trouve (enfin disons 98%) est en général justifié, donc je te conseille fortement de corriger les problèmes qu'il te signale avant d'aller plus loin, que ce soit des problèmes de gestion mémoire ou non
    2. Tu peux aussi passer à  ARC, si ce n'est pas déjà  le cas, ce qui te permettra de te simplifier la vie au niveau de la gestion de la mémoire. Ca n'empêche pas qu'il est bon et utile de comprendre les mécanismes de gestion mémoire, mais ça va faire les retain/release tout seul pour toi donc tu risques pas d'en oublier.
    3. Utilise Instruments (Menu Product -> Profile) pour analyser la consommation mémoire de ton application pendant que tu l'utilises, comprendre ce qui bouffe autant de mémoire, etc.
  • CéroceCéroce Membre, Modérateur
    Oui, regarde sous Instruments (menu Product > Profile) pour avoir une valeur réelle.

    Sache tout de même que les applis sous OS X prennent leurs aises...
Connectez-vous ou Inscrivez-vous pour répondre.