Blocage dispatch_group

Hello,


 


J'ai un crash avec aucune trace de debug avec l'utilisation de dispatch_group et peut être que quelqu'un aurait une idée :)


 


Le code :



@interface PFMAccountSelectionViewController

@property (nonatomic, strong) NSMutableArray *arrayOfAccountsManagerParser;
...
@end

@implementation PFMAccountSelectionViewController

...




@end


- (void)applyChange{
    dispatch_group_t group = dispatch_group_create();
    
    // to handle the WS completion
    __block BOOL errorDetected = NO;
    
    for (NSInteger i = 0; i < self.arrayOfAccounts.count; i ++)
    {
        CSPFMAccount * newAccount = self.arrayOfAccounts[i];
        CSPFMAccount * oldAccount = [ApplicationData sharedInstance].arrayOfPFMPRCAccounts[i];
        
        if (newAccount.selected != oldAccount.selected)
        {
            [self lanceSpinnerMiniActivityViewer];

            dispatch_group_enter(group);
            
            NSLog(@BEFORE BLOCK : %ld, (long)i);

            weakifySelf(weakSelf);
            CSPFMAccountsManagerXMLParser * parserAccountsManager = [[CSPFMAccountsManagerXMLParser alloc] initWithCompletion:^(EIXMLDOMParser *parser, NSError *error) {

                NSLog(@IN GROUP : %ld, (long)i);
                strongifySelfAndReturnIfNil(strongSelf, weakSelf);

                if (error != nil)
                {
                    errorDetected = YES;
                    [EIAlertView presentAlertViewWithError:error];
                    // We revert the selection
                    newAccount.selected = !newAccount.selected;
                    [strongSelf->currentTableView reloadRowsAtIndexPaths:@[;[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
                }
                
                dispatch_group_leave(group);
            }];
            
            [self.arrayOfAccountsManagerParser addObject:parserAccountsManager];
            
            [parserAccountsManager
                start:newAccount.webid
                action:CSPFMAccountsManagerActionTypeUpdate
                isSelected:newAccount.selected
            ];
        }
    }
    
    // When you cannot make any more forward progress,
    // wait on the group to block the current thread.
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@OH I'M HERE);
}

En gros :


 


Si je lance un seul appel : ca passe tout le temps (le NSLog "OH I'M HERE").


Si j'en lance 2, ca passe rarement.


Si j'en lance 3 ou plus je vois dans les traces de log que le web service est bien appelé mais on arrive jamais au "OH I'M HERE".


 


J'imagine que je ne dois pas retainer correctement un objet, mais j'ai du mal à  voir.


 


Je pourrais faire autrement (avec un compteur ou un truc du genre, mais je trouve le GCD plus propre pour les appels concurrents.


 


Merci d'avance 


 


Mots clés:

Réponses

  • A noter que j'avais au début à  la place du dispatch_group_wait :



        weakifySelf(weakSelf);
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            
            NSLog(@IN GROUP NOTIFY);

            strongifySelfAndReturnIfNil(strongSelf, weakSelf);


            [strongSelf hideMiniActivityViewer];
            
            if (!errorDetected)
            {            
                [ApplicationData sharedInstance].pfmHomeToRefresh = YES;
                [strongSelf.navigationController popViewControllerAnimated:YES];
            }
        });

    Et que dans ce cas j'avais un EXC_BAD_ACCESS mais sans aucune trace dans la console.

  • AliGatorAliGator Membre, Modérateur
    J'ai pas trop le temps de regarder le code en détail, mais tu dis que tu n'arrives jamais au "OH I'M HERE", mais est-ce que tu as quand même ta balance entre "BEFORE BLOCK" et "IN GROUP" ?

    En particulier si tu mets aussi un log juste avant le dispatch_group_enter et juste après le dispatch_group_leave, est-ce que tu as bien les 2 logs, qui montreraient que tu balances les entrées et sorties du groupe ?

    Sinon, un petit coup de Static Analyzer (Product -> Analyze) pourrais te donner des indices.

    Sinon pour ta solution précédente (2ème message), comme dispatch_group_notify retourne tout de suite (et continue avec le reste du code), pour n'exécuter le block de notification que plus tard (comme pour un completionBlock, quoi), ça veut dire que dans ce cas il faut que ton "dispatch_group_t group" soit retenu quelque part dans une @property(strong). Je pense que c'est pour ça que tu avais un EXC_BADACCESS, ton group ne devait pas être retenu ?
  • GeoffreyGeoffrey Membre
    février 2015 modifié #4

    Alors j'ai allégé le code :


     


        



    @property (nonatomic, strong) dispatch_group_t group;
        self.group = dispatch_group_create();
        
        
        for (NSInteger i = 0; i < self.arrayOfAccounts.count; i ++)
        {
            CSPFMAccount * newAccount = self.arrayOfAccounts[i];
            CSPFMAccount * oldAccount = [ApplicationData sharedInstance].arrayOfPFMPRCAccounts[i];
            
            if (newAccount.selected != oldAccount.selected)
            {
                NSLog(@BEFORE BLOCK : %ld, (long)i);

                weakifySelf(weakSelf);
                CSPFMAccountsManagerXMLParser * parserAccountsManager = [[CSPFMAccountsManagerXMLParser alloc] initWithCompletion:^(EIXMLDOMParser *parser, NSError *error) {
                    
                    NSLog(@IN GROUP : %ld, (long)i);
                    strongifySelfAndReturnIfNil(strongSelf, weakSelf);
                    
                    NSLog(@BEFORE LEAVE : %ld, (long)i);
                    dispatch_group_leave(strongSelf.group);
                    NSLog(@AFTER LEAVE : %ld, (long)i);
                }];
                
                [self.arrayOfAccountsManagerParser addObject:@[;parserAccountsManager, newAccount]];
            }
        }
        
        for (NSArray * array in self.arrayOfAccountsManagerParser)
        {
            NSLog(@Before Enter);
            dispatch_group_enter(self.group);
            NSLog(@After Enter);
            
            CSPFMAccountsManagerXMLParser * parser = array[0];
            CSPFMAccount * account = array[1];
            [parser
                start:account.webid
                action:CSPFMAccountsManagerActionTypeUpdate
                isSelected:account.selected
            ];
        }
        
    //     When you cannot make any more forward progress,
    //     wait on the group to block the current thread.
        for (NSArray * array in self.arrayOfAccountsManagerParser)
        {
            NSLog(@Still alive %@", array);
        }
        
        dispatch_group_wait(self.group, DISPATCH_TIME_FOREVER);    
        NSLog(@OH I'M HERE);

    En gros on est bien avant et après le Enter, mais on rentre jamais dans les completions block ! 


     


    Je vais regarder le static analyser.


  • AliGatorAliGator Membre, Modérateur

    En gros on est bien avant et après le Enter, mais on rentre jamais dans les completions block !

    Bon bah tu l'as ta réponse. Si le completionBlock n'est jamais appelé, le dispatch_group_leave qui se trouve dedans n'est jamais appelé non plus.

    Tu es sûr que ton completionBlock est bien appelé sur un thread secondaire ?
    Parce que si ton completionBlock de ton CSPFMAccountsManagerXMLParser est appelé sur la dispatch_get_main_queue() c'est un peu logique que tu aies un deadlock : ton main thread est bloqué par ton dispatch_group_wait (qui attend que tous les groupes soient finis), et si ton completionBlock essaye de s'exécuter sur ce main thread, comme il est bloqué par le dispatch_group_wait, tu as un deadlock.

    En gros le main thread attend que le dispatch_group_wait ait redonné la main, ce qui n'arrivera que quand tes completionBlock seront appelés, mais tes completionBlock s'ils sont exécutés sur le main thread vont à  leur tour attendre que le main thread soit libre pour pouvoir s'exécuter... c'est le serpent qui se mord la queue !

    Si tu veux que ça marche il ne faut pas que ton completionBlock s'exécute sur le même thread que le thread que tu as bloqué avec ton dispatch_group_wait, sinon ça n'a pas de sens.
  • GeoffreyGeoffrey Membre
    février 2015 modifié #6

    Ok, merci pour tout Ali, du coup la j'ai enlevé le _wait et remis un _notify avec le group qui est strong et c'est bon. Je vais quand meme demander à  ceux qui ont fait la librairie d'appel des WS (avec les completions block) dans quels threads sont exécutés les completionBlock. 


     


    Fausse joie, ca a fonctionné une fois that's all.


  • GeoffreyGeoffrey Membre
    février 2015 modifié #7

    Bon j'ai toujours un crash -_- mais après le notify. Je vais creuser.


  • samirsamir Membre
    février 2015 modifié #8

    Je pense que le code est compliqué pour rien ( ou bien j'ai mal compris ?) et c'est un peu normal d'avoir des comportments qui te font crasher.


     


    Pourquoi stocker tous les objets parser dans une liste ? La première chose à  mon avis est d'alléger ce code :



    @interface CSPFMAccountsManagerXMLParser : NSObject

    + (void)parseAccount:(NSString *)accountid action:(ActionType)type selected:(BOOL)selected completionHandler:(....)

    @end

    Ce n'est pas vraiment une solution à  ton problème mais je pense que tu verras plus claire comme ça et et ça t'évitera probablement les crash.


     


     


     


  • Je les stockes dans un array (strong) sinon ils risquent de ne plus exister et le completion block pourrait ne pas s'executer.


     


    Je vais essayer quand meme de l'appeler directement comme tu le proposes.


  • Bon c'est pas trop possible. Je dépend d'une lib interne ou il y a :



    typedef void(^EmptyCompletionXMLDOM)(EIXMLDOMParser *parser, NSError *error);

    @interface EIXMLDOMBlockParser : EIXMLDOMParser <EIXMLDOMParserDelegate>
    {
        
    @private
        // Simple parser vars
        NSString *rootNodeName;
        Class contentClass;
        BOOL isArray;
        
        // Multiple parser vars
        NSArray *rootNodeNames;
        NSArray *contentClasses;
    }

    @property (nonatomic, copy, readonly) EmptyCompletionXMLDOM emptyCompletion;

    - (id)initWithCompletion:(EmptyCompletionXMLDOM)completion;

    Et le parser hérite de cette classe.



    @interface CSPFMAccountsManagerXMLParser : EIXMLDOMBlockParser

    -(void)start:(NSString *)pWebid action:(CSPFMAccountsManagerActionType *)pAction isSelected:(BOOL)pIs_selected;

    @end
  • AliGatorAliGator Membre, Modérateur
    Ouch, une classe qui utilise des variables d'instance... qu'il expose en + dans le .h et son @interface...
    Et aucun signe de dispatch_queue pour le completionBlock (ni en variable d'instance, ni avec un moyen d'en fournir une lors de l'appel)...

    C'est mauvais signe cette lib... Ca sent le completionBlock exécuté sur le mainThread avec aucun moyen de demander de l'exécuter sur une autre queue que la main_queue... du coup tu vas pas pouvoir à  la fois faire un dispatch_group_wait dans le mainThread d'un côté et faire tes dispatch_group_leave dans les completion si ces dernières sont dans le main thread...

    Soit tu changes de lib, soit tu la fais évoluer pour qu'elle accepte en paramètre la dispatch_queue sur laquelle exécuter le completionBlock, soit tu passes par un autre moyen... comme dispatch_group_notify si jamais il marche pour ce cas.
  • Oui après discussion avec le mec qui a fait la lib ici, tu as tout juste Ali !


  • Alors j'ai des infos supplémentaire :


     


    On "laisse" iOS gérer les threads, les completions block sont exécuté dans le meme thread que la NSURLConnexion créée pour l'appel du WS.


  • AliGatorAliGator Membre, Modérateur
    Bon bah il te reste plus qu'à  lancer tes appels à  ton CSPFMAccountsManagerXMLParser sur une dispatch_queue créée pour l'occasion (et tant qu'à  faire créée en mode CONCURRENT et non SERIAL pour que chaque requête/parsing se fasse en parallèle sinon ça n'a pas trop d'intérêt) avec un dispatch_async.


    Vite fait en pseudo-code (non testé) ça devrait donner un truc du genre :

    dispatch_queue_t parsingQueue = dispatch_queue_create("com.me.parsingQ",DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t parsingGroup = dispatch_group_create();

    for(...)
    {
    CSPFMAccountsManagerXMLParser* parser = [[CSPFMAccountsManagerXMLParser alloc] initWithCompletion:^(...) {
    ...
    dispatch_group_leave(self.parsingGroup);
    }];
    [self.parsersArray addObject:parser];
    dispatch_group_enter(self.parsingGroup);
    dispatch_async(self.parsingQueue, ^{
    [parser start:... ...];
    }
    }

    dispatch_group_wait(self.parsingGroup);
  • J'ai testé ca :



    self.arrayOfAccountsManagerParser = [NSMutableArray array];

        self.parsingQueue = dispatch_queue_create("com.me.parsingQ", DISPATCH_QUEUE_CONCURRENT);
        self.parsingGroup = dispatch_group_create();
        
        for (NSInteger i = 0; i < self.arrayOfAccounts.count; i ++)
        {
            CSPFMAccount * newAccount = self.arrayOfAccounts[i];
            CSPFMAccount * oldAccount = [ApplicationData sharedInstance].arrayOfPFMPRCAccounts[i];

            if (newAccount.selected != oldAccount.selected)
            {
                weakifySelf(weakSelf);
                CSPFMAccountsManagerXMLParser * parserAccountsManager = [[CSPFMAccountsManagerXMLParser alloc] initWithCompletion:^(EIXMLDOMParser *parser, NSError *error) {
                    
                    strongifySelfAndReturnIfNil(strongSelf, weakSelf);
                    
                    NSLog(@Before leave);
                    dispatch_group_leave(strongSelf.parsingGroup);
                    NSLog(@After leave);
                }];

                [self.arrayOfAccountsManagerParser addObject:parserAccountsManager];
                
                NSLog(@Before enter);
                dispatch_group_enter(self.parsingGroup);
                NSLog(@After enter);

                dispatch_async(self.parsingQueue, ^{
                    
                    NSLog(@Before start);
                    [parserAccountsManager
                        start:newAccount.webid
                        action:CSPFMAccountsManagerActionTypeUpdate
                        isSelected:newAccount.selected
                    ];
                    NSLog(@After start);
                });
            }
        }
                           
        dispatch_group_wait(self.parsingGroup, DISPATCH_TIME_FOREVER);
        NSLog(@AFTER);

    Et dans le log :



    2015-02-24 14:35:49.809 Crédit Mutuelenterprise-rct[13980:2561006] Before enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561006] After enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561006] Before enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561195] Before start
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561006] After enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561006] Before enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561006] After enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2562537] Before start
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561006] Before enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2561006] After enter
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2562539] Before start
    2015-02-24 14:35:49.810 Crédit Mutuelenterprise-rct[13980:2562540] Before start

    Et je n'ai pas de trace de l'appel du WS dans les logs.


  • Et avec 



        dispatch_group_notify(self.parsingGroup, self.parsingQueue, ^{

            NSLog(@IN GROUP NOTIFY);

        });

    A la place du wait, j'ai bien les After Start et les traces de l'appel du WS, par contre je rentre jamais dans le completion block.


  • AliGatorAliGator Membre, Modérateur
    Faut revoir avec les auteurs de ta lib, à  tous les coups la completionBlock est appelée non pas sur le thread appelant comme ils t'ont dit (donc sur la queue dans laquelle tu as appelé ton "start"), mais sur la mainQueue...

    A la limite il suffit dans leur code, juste avant la ligne qui est sensée appeler ton completionBlock, d'afficher la queue voire le NSThread sur lequel il est appelé pour voir s'il s'agit du mainThread ou pas.

    Ce qui est bizarre c'est qu'ils te disent "les completions block sont exécuté dans le meme thread que la NSURLConnexion", sauf que justement s'ils utilisent NSURLConnection correctement, et utilisent donc sa méthode "sendAsynchronousRequest:queue:completionHandler:", bah tu passes justement une queue en paramètre de cette méthode... donc ils devraient être capable de te dire quelle queue ils passent !

    Et s'ils n'utilisent pas cette méthode mais utilisent encore la méthode avec les delegate, déjà  il serait temps qu'ils se mettent au goût du jour (le changement est préconisé depuis iOS5 quand même !), où effectivement il est dit "Delegate methods are called on the same thread that called this method.", encore faut-il vérifier comment ils l'appellent en interne (sur quel thread et quelle queue) et vérifier que leur code est thread-safe.
  • AliGatorAliGator Membre, Modérateur
    Bon après, dans tous les cas, en relisant tes messages, je suis en train de me demander pourquoi tu veux faire un dispatch_group_wait() (et donc une attente bloquante) si tu es dans le main thread... Ca va donc bloquer toute ton application le temps que toutes tes requêtes et leur parsing soit terminé, non ? Donc ça peut bloquer longtemps...

    L'idéal serait d'avoir pour ta méthode globale elle aussi un completionBlock t'indiquant quand toutes tes requêtes sont finies. Effectivement normalement le dispatch_group_notify devrait pouvoir répondre à  ce besoin...

  • sendAsynchronousRequest:queue:completionHandler

    C'est nulle part dans leur code, j'ai déjà  vérifié ce point avant -_-


     


    C'est effectivement la méthode avec des delegate.

  • Et oui à  la base je voulais utiliser le notify plutôt que le wait (le wait je l'avais testé un peu en desespoir de cause).


  • Bon finalement j'ai fait sans, c'est moins élégant mais bon, pas le choix 


     


     



      self.arrayOfAccountsManagerParser = [NSMutableArray array];
        
        __block NSInteger modificationCounter = 0;
        __block BOOL errorDetected = NO;
        BOOL activityViewerIsRunning = NO;

        for (NSInteger i = 0; i < self.arrayOfAccounts.count; i ++)
        {
            CSPFMAccount * newAccount = self.arrayOfAccounts[i];
            CSPFMAccount * oldAccount = [ApplicationData sharedInstance].arrayOfPFMPRCAccounts[i];
            
            if (newAccount.selected != oldAccount.selected)
            {
                modificationCounter ++;
                
                if (!activityViewerIsRunning)
                {
                    [self lanceSpinnerMiniActivityViewer];
                    activityViewerIsRunning = YES;
                }
                
                weakifySelf(weakSelf);
                CSPFMAccountsManagerXMLParser * parserAccountsManager = [[CSPFMAccountsManagerXMLParser alloc] initWithCompletion:^(EIXMLDOMParser *parser, NSError *error) {
                    
                    modificationCounter --;
                    
                    strongifySelfAndReturnIfNil(strongSelf, weakSelf);
                    
                    if (error != nil)
                    {
                        errorDetected = YES;
                        [EIAlertView presentAlertViewWithError:error];
                        // We revert the selection
                        newAccount.selected = !newAccount.selected;
                        [strongSelf->currentTableView reloadRowsAtIndexPaths:@[;[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
                    }
                    
                    if (!errorDetected && modificationCounter == 0)
                    {
                        [ApplicationData sharedInstance].pfmHomeToRefresh = YES;
                        [strongSelf.navigationController popViewControllerAnimated:YES];
                    }
                }];

                [self.arrayOfAccountsManagerParser addObject:parserAccountsManager];

                [parserAccountsManager
                    start:newAccount.webid
                    action:CSPFMAccountsManagerActionTypeUpdate
                    isSelected:newAccount.selected
                ];
            }
        }
Connectez-vous ou Inscrivez-vous pour répondre.