2015-10-01 19 views
5

Aggiornamento: Ho preparato il campione che è riprodurre il problema senza record.Please magica scaricare il progetto di test utilizzando seguente URL: https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreDataWithoutMR.zipxCode 7.0 IOS9 SDK: situazione di stallo durante l'esecuzione di prendere richiesta con performBlockAndWait

Il progetto prevedeva ha seguente problema: deadlock on fetch in performBlockAndWait chiamato dal thread principale.

Il problema viene riprodotto se il codice viene compilato utilizzando la versione XCode> 6.4. Il problema non viene riprodotto se il codice viene compilato utilizzando xCode == 6.4.

Vecchio domanda era:

sto lavorando sul supporto di applicazione mobile IOS. Dopo il recente aggiornamento di Xcode IDE dalla versione 6.4 alla versione 7.0 (con supporto IOS 9) ho dovuto affrontare problemi critici: l'hangup dell'applicazione. La stessa build dell'applicazione (prodotta dalle stesse sorgenti) con xCode 6.4 funziona correttamente. Quindi, se l'applicazione è costruita usando xCode> 6.4 - l'applicazione si blocca in alcuni casi. se l'applicazione è costruita usando xCode 6.4 - l'applicazione funziona correttamente.

Ho dedicato un po 'di tempo alla ricerca del problema e, come risultato, ho preparato l'applicazione di test con un caso simile come nella mia applicazione che riproduce il problema. Il hangup applicazione di test sul Xcode> = 7.0, ma funziona correttamente sul Xcode 6.4

Link per il download di fonti di prova: https://www.sendspace.com/file/r07cln

I requisiti per l'applicazione di test è: 1. cacao baccelli manager deve essere installato nel sistema 2. Framework MagicalRecord della versione 2.2.

L'applicazione di prova funziona nel modo seguente: 1. All'avvio dell'applicazione crea un database di test con 10000 record di entità semplici e li salva nell'archivio permanente. 2. Alla prima schermata dell'applicazione nel metodo viewWillAppear: esegue il test che provoca il deadlock. seguente algoritmo è usato:

-(NSArray *) entityWithId: (int) entityId inContext:(NSManagedObjectContext *)localContext 
{ 
    NSArray * results = [TestEntity MR_findByAttribute:@"id" withValue:[ NSNumber numberWithInt: entityId ] inContext:localContext]; 
    return results; 
} 

….. 
int entityId = 88; 
NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; 
childContext1.name = @"childContext1"; 

NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; 
childContext2.name = @"childContext2"; 

NSArray *results = [self entityWithId:entityId inContext: childContext2]; 

for(TestEntity *d in results) 
{ 
    NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup 
} 

dispatch_async(dispatch_get_main_queue(),^
       { 
        int entityId2 = 11; 
        NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"id=%d", entityId2]; 
        NSArray *a = [ TestEntity MR_findAllWithPredicate: predicate2 inContext: childContext2]; 
        for(TestEntity *d in a) 
        { 
         NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); 
        } 
       }); 

due contesti oggetto gestito sono creati con il tipo di concorrenza == NSPrivateQueueConcurrencyType (si prega di controllare il codice di MR_context del quadro diario magico). Entrambi i contesti hanno un contesto padre con il tipo di concorrenza = NSMainQueueConcurrencyType. Dall'applicazione thread principale viene eseguito il recupero in modo sincrono (MR_findByAttribute e MR_findAllWithPredicate vengono utilizzati performBlockAndWait con la richiesta di recupero all'interno). Dopo il primo recupero, il secondo recupero è pianificato sul thread principale utilizzando dispatch_async().

Come risultato l'applicazione riaggancia. Sembra che si sia verificato un deadlock, per favore controlla lo screenshot dello stack:

 ecco il link, la mia reputazione è troppo bassa per pubblicare immagini. https://cdn.img42.com/34a8869bd8a5587222f9903e50b762f9.png)

Se per commentare la linea
NSLog (@ "e dal fetchRequest% @ con il nome = '% @'", d, d.name); /// questa linea è la ragione del hangup

(che è la linea 39 in ViewController.m del progetto di test) l'applicazione funziona correttamente. Credo che ciò sia dovuto al fatto che non esiste una lettura del campo nome dell'entità di test.

Quindi con la riga commentata NSLog (@ "e da fetchRequest% @ con nome = '% @'", d, d.name);
non ci sono problemi con i binari costruiti con Xcode 6.4 e Xcode 7.0.

Con la riga non commentata NSLog (@ "e da fetchRequest% @ con nome = '% @'", d, d.name);

c'è hangup su binario costruito con Xcode 7.0 e non c'è riagganciamento su binario costruito con Xcode 6.4.

Credo che il problema si verifichi a causa del caricamento lento dei dati delle entità.

Qualcuno ha problemi con il caso descritto? Sarò grato per qualsiasi aiuto.

+0

Credo che ci sia un problema o un'incompatibilità nei recenti SDK di iOS 9, ma per aiutare le persone a capirlo rendiamo il tuo post più chiaro. Prima di tutto, potresti caricare il tuo esempio sul github? (Chrome non mi ha permesso di scaricare). Il secondo: rimuovi tutti i cambiamenti nei baccelli fatti da te. Rimuovi il codice inutilizzato, fai in modo che sia chiaro. La breve storia del problema è: il blocco di invio con un contesto figlio si bloccherà su di esso se 1) inviato da -viewWillAppear: e 2) è stato utilizzato un oggetto gestito (errore licenziato) prima della spedizione (come avete fatto in NSLog: d. nome) Lo stesso problema con CoreDa diretto – MikeR

risposta

6

Questo è il motivo per cui non utilizzo framework che astraggono (ovvero nascondono) troppi dettagli dei dati di base. Ha schemi di utilizzo molto complessi e talvolta è necessario conoscere i dettagli di come interagiscono.

In primo luogo, non so nulla di record magico tranne che molte persone lo usano, quindi deve essere abbastanza buono in quello che fa.

Tuttavia, ho visto immediatamente diversi usi errati della concorrenza dei dati di base nei tuoi esempi, quindi sono andato a guardare i file di intestazione per capire perché il tuo codice ha fatto supposizioni.

Non ho intenzione di prenderti per il culo, anche se a prima vista potrebbe sembrare come prima cosa. Voglio aiutarti ad educarti (e l'ho usato come un'opportunità per dare un'occhiata al MR).

Da una rapida occhiata a MR, direi che hai qualche malinteso su ciò che fa MR, e anche le regole generali di concorrenza dei dati di base.

In primo luogo, si dice questo ...

due contesti oggetto gestito sono creati con il tipo di concorrenza == NSPrivateQueueConcurrencyType (si prega di controllare il codice di MR_context di quadro diario magico). Entrambi i contesti hanno un contesto padre con il tipo di concorrenza = NSMainQueueConcurrencyType.

che non sembra essere vero. I due nuovi contesti sono, infatti, contesti di coda privata, ma il loro genitore (secondo il codice che ho dato un'occhiata a github) è il magico MR_rootSavingContext, che a sua volta è anche un contesto di coda privata.

Analizziamo l'esempio di codice.

NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; 
childContext1.name = @"childContext1"; 

NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; 
childContext2.name = @"childContext2"; 

Quindi, ora avete due MOCS privato-coda (childContext1 e childContext2), entrambi figli di un altro anonimo MOC privato-coda (che chiameremo savingContext).

NSArray *results = [self entityWithId:entityId inContext: childContext2]; 

Si esegue quindi un recupero su childContext1. Quel codice è in realtà ...

-(NSArray *) entityWithId:(int)entityId 
       inContext:(NSManagedObjectContext *)localContext 
{ 
    NSArray * results = [TestEntity MR_findByAttribute:@"id" 
              withValue:[NSNumber numberWithInt:entityId] 
              inContext:localContext]; 
    return results; 
} 

Ora, noi sappiamo che la localContext in questo metodo è, in questo caso, un altro puntatore a childContext2 che è una MOC privato-coda. È al 100% contro le regole di concorrenza per accedere a un MOC di coda privata al di fuori di una chiamata a performBlock. Tuttavia, dal momento che stai utilizzando un'altra API e il nome del metodo non offre assistenza per sapere come si accede al MOC, dobbiamo dare un'occhiata a quell'API e vedere se nasconde lo performBlock per vedere se stai accedendo correttamente.

Sfortunatamente, la documentazione nel file di intestazione non offre alcuna indicazione, quindi dobbiamo esaminare l'implementazione. Quella chiamata finisce per chiamare MR_executeFetchRequest... che non indica nella documentazione come gestisce la concorrenza. Quindi, andiamo a dare un'occhiata alla sua implementazione.

Ora, stiamo arrivando da qualche parte. Questa funzione tenta di accedere in sicurezza al MOC, ma usa performBlockAndWait che bloccherà quando viene chiamato.

Questa è un'informazione estremamente importante, perché chiamare questo dal posto sbagliato può effettivamente causare un deadlock. Pertanto, è necessario essere consapevoli che performBlockAndWait viene chiamato ogni volta che si esegue una richiesta di recupero. La mia regola personale è di mai usare performBlockAndWait a meno che non ci sia assolutamente altra opzione.

Tuttavia, questa chiamata qui dovrebbe essere completamente sicura ... presumendo che non venga chiamata dal contesto del MOC padre.

Quindi, diamo un'occhiata al prossimo pezzo di codice.

for(TestEntity *d in results) 
{ 
    NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup 
} 

Ora, questo non è colpa di MagicalRecord, perché MR non viene nemmeno utilizzato direttamente qui. Tuttavia, sei stato addestrato all'uso dei metodi MR_, che non richiedono alcuna conoscenza del modello di concorrenza, quindi dimentichi o non impari mai le regole di concorrenza.

Gli oggetti nell'array results sono tutti gli oggetti gestiti che vivono nel contesto di coda privata childContext2. Pertanto, potresti non accedervi mai senza rendere omaggio alle regole di concorrenza. Questa è una chiara violazione delle regole di concorrenza. Durante lo sviluppo dell'applicazione, è necessario abilitare il debug della concorrenza con l'argomento -com.apple.CoreData.ConcurrencyDebug 1.

Questo snippet di codice deve essere compreso tra performBlock o performBlockAndWait. Non uso quasi mai lo performBlockAndWait perché ha così tanti svantaggi: i deadlock sono uno di questi. In effetti, vedere l'uso di performBlockAndWait è un'indicazione molto forte che il deadlock sta accadendo lì e non sulla linea di codice che si indica. Tuttavia, in questo caso, è almeno altrettanto sicuro come il precedente fetch quindi cerchiamo di renderlo un po 'più sicuro ...

[childContext2 performBlockAndWait:^{ 
    for (TestEntity *d in results) { 
     NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); 
    } 
}]; 

Successivamente, la spedizione verso il thread principale. È perché vuoi solo che qualcosa si verifichi in un successivo ciclo del ciclo degli eventi, o perché questo codice è già in esecuzione su qualche altro thread? Chissà. Tuttavia, hai lo stesso problema qui (ho riformattato il tuo codice per la leggibilità come post).

dispatch_async(dispatch_get_main_queue(), ^{ 
    int entityId2 = 11; 
    NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"id=%d", entityId2]; 
    NSArray *a = [TestEntity MR_findAllWithPredicate:predicate2 
              inContext:childContext2]; 
    for (TestEntity *d in a) { 
     NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); 
    } 
}); 

Ora, sappiamo che il codice inizia in esecuzione sul thread principale, e la ricerca chiamerà performBlockAndWait ma il vostro accesso successivo nel ciclo for viola di nuovo le regole di base di dati di concorrenza.

Sulla base di ciò, gli unici veri problemi che vedo sono ...

  1. MR sembra di onorare le regole di base di dati di concorrenza all'interno del loro API, ma è comunque necessario seguire le regole di base di dati di concorrenza quando si accede i tuoi oggetti gestiti.

  2. Non mi piace davvero l'uso di performBlockAndWait in quanto è solo un problema in attesa di accadere.

Ora, diamo un'occhiata allo screenshot del blocco. Hmmm ... è un deadlock classico, ma non ha senso perché il deadlock si verifica tra il thread principale e il thread MOC. Questo può accadere solo se il MOC della coda principale è un genitore di questo MOC della coda privata, ma il codice mostra che non è il caso.

Hmmm ... non aveva senso, quindi ho scaricato il tuo progetto e ho guardato il codice sorgente nel pod che hai caricato. Ora, quella versione del codice utilizza MR_defaultContext come padre di tutti i MOC creati con MR_context. Quindi, il MOC predefinito è, in effetti, un MOC della coda principale, e ora tutto ha perfettamente senso.

Si dispone di un MOC come figlio di un MOC di coda principale. Quando si invia quel blocco alla coda principale, è ora in esecuzione come blocco sulla coda principale. Il codice chiama quindi performBlockAndWait in un contesto che è figlio di un MOC per quella coda, che è un enorme no-no, e si è quasi garantito che si verifichi un deadlock.

Quindi, sembra che MR abbia modificato il proprio codice dall'utilizzo di una coda principale come principale di nuovi contesti per l'utilizzo di una coda privata come padre di nuovi contesti (molto probabilmente a causa di questo problema esatto). Quindi, se esegui l'aggiornamento all'ultima versione di MR, dovresti stare bene.

Tuttavia, vorrei ancora avvertire che se si desidera utilizzare MR in modalità multithreading, è necessario sapere esattamente come gestiscono le regole di concorrenza, e si deve anche assicurarsi di obbedire a loro ogni volta che si accede a qualsiasi core-data oggetti che non stanno attraversando l'API MR.

Infine, dirò semplicemente che ho fatto tonnellate e tonnellate di dati di base e non ho mai utilizzato un'API che tenta di nascondere i problemi di concorrenza da parte mia. Il motivo è che ci sono troppi piccoli casi d'angolo, e preferirei semplicemente affrontarli in modo pragmatico.

Infine, non si dovrebbe quasi mai usare performBlockAndWait a meno che non si sappia esattamente perché è l'unica opzione. Averlo usato come parte di un'API sotto di te è ancora più spaventoso ... almeno per me.

Spero che questa piccola escursione si sia illuminata e ti abbia aiutato (e possibili altri). Sicuramente mi ha dato un po 'di luce e ho contribuito a ristabilire alcune delle mie precedenti sciocchezze infondate.

Modifica

Questo è in risposta alla esempio "non-magico-record" che hai fornito.

Il problema con questo codice è lo stesso problema che ho descritto sopra, relativo a ciò che stava accadendo con MR.

Si dispone di un contesto di coda privata, come un bambino in un contesto di coda principale.

Si sta eseguendo il codice sulla coda principale e si chiama performBlockAndWait nel contesto figlio, che deve quindi bloccare il relativo contesto genitore mentre tenta di eseguire il recupero.

Si chiama deadlock, ma il termine più descrittivo (e seducente) è mortale abbraccio.

Il codice originale è in esecuzione sul thread principale. Invita in un contesto figlio a fare qualcosa, e non fa nient'altro fino a quando il bambino non completa.

Quel bambino quindi, per completare, ha bisogno del thread principale per fare qualcosa. Tuttavia, il thread principale non può fare nulla finché il bambino non ha finito ... ma il bambino sta aspettando che il thread principale faccia qualcosa ...

Nessuno dei due può fare progressi.

Il problema che si sta affrontando è molto ben documentato e, infatti, è stato menzionato più volte nelle presentazioni WWDC e in più parti di documentazione.

È necessario MAI chiamare performBlockAndWait in un contesto figlio.

Il fatto che te ne sei scappato in passato è solo una "casualità" perché non dovrebbe funzionare in questo modo.

In realtà, non dovreste chiamare tutti performBlockAndWait.

Si dovrebbe davvero abituarsi a fare la programmazione asincrona. Ecco come suggerirei di riscrivere questo test e qualunque sia la cosa che ha provocato questo problema.

In primo luogo, riscrivere il recuperare in modo che funziona in modo asincrono ...

- (void)executeFetchRequest:(NSFetchRequest *)request 
        inContext:(NSManagedObjectContext *)context 
       completion:(void(^)(NSArray *results, NSError *error))completion 
{ 
    [context performBlock:^{ 
     NSError *error = nil; 
     NSArray *results = [context executeFetchRequest:request error:&error]; 
     if (completion) { 
      completion(results, error); 
     } 
    }]; 
} 

Poi, si cambia codice che chiama l'operazione di recupero di fare qualcosa di simile ...

NSFetchRequest *request = [[NSFetchRequest alloc] init]; 
[request setEntity: testEntityDescription ]; 
[request setPredicate: predicate2 ]; 
[self executeFetchRequest:request 
       inContext:childContext2 
       completion:^(NSArray *results, NSError *error) { 
    if (results) { 
     for (TestEntity *d in results) { 
      NSLog(@"++++++++++ e from fetchRequest %@ with name = '%@'", d, d.name); 
     } 
    } else { 
     NSLog(@"Handle this error: %@", error); 
    } 
}]; 
+0

Ecco l'URL per l'esempio che riproduce il problema senza record magico. https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreData.zip Il progetto fornito presenta il seguente problema: deadlock on fetch in performBlockAndWait chiamato dal thread principale. Il problema viene riprodotto se il codice viene compilato utilizzando la versione XCode> 6.4. Il problema non viene riprodotto se il codice viene compilato utilizzando xCode == 6.4. Qualcuno ha qualche idea? –

+0

@KirillNeznamov - Quel codice sta facendo esattamente la stessa cosa che ho descritto che sta facendo la versione di MR che stavi usando. Vedi la modifica per maggiori dettagli. –

+0

Grazie Jody, cercherò di trovare le note nella documentazione ufficiale Apple riguardo alla risposta. Grazie mille! –

1

siamo passati su XCode7 e mi sono imbattuto in un problema di deadlock simile con performBlockAndWait nel codice che funziona bene in XCode6.

Il problema sembra essere un utilizzo a monte di dispatch_async(mainQueue, ^{ ... per restituire il risultato da un'operazione di rete. Quella chiamata non era più necessaria dopo aver aggiunto il supporto della concorrenza per CoreData, ma in qualche modo è stato lasciato e non sembrava mai causare un problema fino ad ora.

È possibile che Apple abbia cambiato qualcosa dietro le quinte per rendere i potenziali deadlock più espliciti.

+1

Ho segnalato il problema al supporto Apple, hanno detto che si tratta di un bug. Ho inviato a loro le fonti modificate che riproducono il problema senza un quadro di record magico. Quindi dovremmo aspettare che Apple lo risolva. A proposito, hanno detto che il problema potrebbe essere risolto usando il metodo reset() di NSManagedContext –