2015-10-01 11 views
5

Aktualizacja: Przygotowałem próbkę, która jest odtworzyć problem bez magicznego record.Please pobrać projekt testowy stosując następujący adres URL: https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreDataWithoutMR.zipXcode 7.0 IOS9 SDK: zakleszczenie podczas wykonywania żądania pobierania z performBlockAndWait

Dostarczony projekt ma następujący problem: zakleszczenie przy pobieraniu w performBlockAndWait wywołanym z głównego wątku.

Problem zostanie odtworzony, jeśli kod zostanie skompilowany przy użyciu wersji XCode> 6.4. Problem nie jest powielany, jeśli kod został skompilowany przy użyciu xCode == 6.4.

Old pytanie:

Pracuję na wsparcie IOS aplikacji mobilnej. Po ostatniej aktualizacji Xcode IDE od wersji 6.4 do wersji 7.0 (z obsługą IOS 9) natknąłem się na krytyczny problem - zawieszenie aplikacji. Ta sama wersja aplikacji (wyprodukowana z tego samego źródła) z kodem XCode 6.4 działa poprawnie. Tak więc, jeśli aplikacja jest zbudowana przy użyciu xCode> 6.4 - aplikacja zawiesza się w niektórych przypadkach. , jeśli aplikacja została zbudowana przy użyciu xCode 6.4 - aplikacja działa poprawnie.

Spędziłem trochę czasu, aby zbadać problem, w wyniku czego przygotowałem aplikację testową z podobnym przypadkiem jak w mojej aplikacji, która odtwarza problem. odwieszenie zastosowanie testu na Xcode> = 7,0, ale działa poprawnie na Xcode 6.4

odnośnik Pobierz źródeł Test: https://www.sendspace.com/file/r07cln

wymagania dla aplikacji testowej wynosi: 1. kakao strąki menedżer musi być zainstalowany w systemie. 2. Framework MagicalRecord w wersji 2.2.

Aplikacja testowa działa w następujący sposób: 1. Na początku aplikacji tworzy bazę testową z 10000 rekordami prostych obiektów i zapisuje je w magazynie trwałym. 2. Na pierwszym ekranie aplikacji w metodzie viewWillAppear: uruchamia test powodujący zakleszczenie. następujący algorytm jest stosowany:

-(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); 
        } 
       }); 

dwa konteksty zarządzanych obiektów są tworzone z rodzaju współbieżności == NSPrivateQueueConcurrencyType (proszę sprawdzić kod MR_context magicznych ramach rekordu). Oba konteksty mają kontekst nadrzędny z typem współbieżności = NSMainQueueConcurrencyType. Z głównej aplikacji wątku wykonuje pobieranie w trybie synchronizacji (MR_findByAttribute i MR_findAllWithPredicate są używane performBlockAndWait z żądaniem pobierania wewnątrz). Po pierwszym pobraniu drugie pobranie jest zaplanowane w głównym wątku za pomocą metody dispatch_async().

W rezultacie aplikacja rozłącza się. Wygląda na to, że doszło do impasu, sprawdź zrzut ekranu stosu:

 Oto link, moja reputacja jest zbyt niska, aby opublikować zdjęcia. https://cdn.img42.com/34a8869bd8a5587222f9903e50b762f9.png)

Jeśli skomentować linię
NSLog (@ "E z fetchRequest% @ nazwą = '% @'" d, d.name); /// ta linia jest powodem zawieszania się (która jest linią 39 w ViewController).m projektu testowego) aplikacja działa poprawnie. Uważam, że dzieje się tak dlatego, że nie ma pola odczytu nazwy jednostki testowej.

Tak więc z komentarzem wiersza NSLog (@ "e od fetchRequest% @ z nazwą = '% @'", d, d.nazwa);
nie ma żadnego zawieszenia w plikach binarnych zbudowanych zarówno w Xcode 6.4, jak i Xcode 7.0.

Z nie skomentowaną linią NSLog (@ "e od fetchRequest% @ z nazwą = '% @'", d, d.nazwa);

jest zawieszenie na systemie binarnym zbudowanym za pomocą Xcode 7.0 i nie ma żadnego zawieszenia w systemie binarnym zbudowanym z Xcode 6.4.

Wierzę, że problem występuje z powodu leniwego ładowania danych o jednostce.

Czy masz problem z opisaną sprawą? Będę wdzięczny za każdą pomoc.

+0

Wydaje mi się, że w najnowszych pakietach SDK dla systemu iOS 9 występuje problem lub niekompatybilność, ale aby pomóc użytkownikom w zrozumieniu tego problemu, sprawmy, aby Twój post był bardziej przejrzysty. Po pierwsze, czy mógłbyś przesłać swój przykład do githuba? (Chrome nie pozwolił mi pobrać). Drugi: usuń wszystkie zmiany w strąkach zrobione przez ciebie. Usuń nieużywany kod, załóż przykład wyczyść. Krótka historia problemu polega na tym, że: blok wysyłki z kontekstem podrzędnym zablokuje go, jeśli 1) zostanie wywołany z -viewWillAppear: i 2) użyty obiekt zarządzany został użyty (wywołana usterka) przed wysłaniem (tak jak w NSLog: d. nazwa) Ten sam problem z bezpośrednim CoreDa – MikeR

Odpowiedz

6

Dlatego nie używam frameworków, które abstrakcyjne (to znaczy ukrywają) zbyt wiele szczegółów podstawowych danych. Ma bardzo skomplikowane wzorce korzystania, a czasami trzeba znać szczegóły dotyczące ich współdziałania.

Po pierwsze, nie wiem nic o magicznej płycie, z wyjątkiem tego, że wiele osób używa jej, więc musi być całkiem niezła w tym, co robi.

Jednak od razu zauważyłem kilka całkowicie błędnych zastosowań współbieżności danych w naszych przykładach, więc poszedłem i spojrzałem na pliki nagłówkowe, aby zobaczyć, dlaczego twój kod uczynił założenia, które on robi.

Nie mam zamiaru cię wkurzyć, choć może się to wydawać na pierwszy rzut oka. Chcę ci pomóc w nauce (i wykorzystałem to jako okazję, by rzucić okiem na MR).

Od bardzo szybkiego przyjrzenia się MR, powiedziałbym, że macie pewne nieporozumienia co do MR, a także ogólne zasady współbieżności danych podstawowych.

Po pierwsze, można powiedzieć to ...

dwa konteksty zarządzanych obiektów są tworzone typu współbieżności == NSPrivateQueueConcurrencyType (proszę sprawdzić kod MR_context z magicznym ramach rekordu). Oba konteksty mają kontekst nadrzędny z typem współbieżności = NSMainQueueConcurrencyType.

co nie wydaje się być prawdą. Dwa nowe konteksty są w rzeczywistości kontekstami prywatnej kolejki, ale ich rodzic (zgodnie z kodem, który oglądałem na githubie) jest magiczny MR_rootSavingContext, który sam w sobie jest także kontekstem prywatnej kolejki.

Podzielmy przykład Twojego kodu.

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

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

Tak, masz teraz dwa MOCy prywatnego kolejki (childContext1 i childContext2), zarówno dla dzieci innego anonimowego prywatnego kolejki MOC (będziemy nazywać savingContext).

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

Następnie wykonać pobieranie na childContext1. Ten kod jest w rzeczywistości ...

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

Teraz wiemy, że localContext w tej metodzie jest w tym przypadku inny wskaźnik do childContext2 który jest MOC prywatnego kolejka. Jest 100% przeciwko regułom współbieżności, aby uzyskać dostęp do prywatnego MOC-u poza wywołaniem performBlock. Ponieważ jednak używasz innego interfejsu API, a nazwa metody nie oferuje pomocy w poznaniu sposobu uzyskiwania dostępu do MOC, musimy przejrzeć ten interfejs API i sprawdzić, czy ukrywa on performBlock, aby sprawdzić, czy uzyskujesz do niego prawidłowy dostęp.

Niestety, dokumentacja w pliku nagłówkowym nie zawiera żadnych wskazówek, więc musimy spojrzeć na implementację. To wywołanie kończy się wywoływaniem MR_executeFetchRequest..., co nie wskazuje w dokumentacji, jak obsługuje on również współbieżność. Więc przyjrzymy się jego realizacji.

Teraz coś docieramy. Ta funkcja próbuje bezpiecznie uzyskać dostęp do MOC, ale używa performBlockAndWait, która blokuje, gdy zostanie wywołana.

Jest to niezwykle ważna informacja, ponieważ wywołanie jej z niewłaściwego miejsca może rzeczywiście spowodować impas. Dlatego musisz mieć świadomość, że performBlockAndWait jest wywoływane za każdym razem, gdy wykonasz żądanie pobierania. Moja własna zasada brzmi: nigdy używać performBlockAndWait, chyba że nie ma absolutnie żadnej innej opcji.

Jednak to wezwanie powinno być całkowicie bezpieczne ... zakładając, że nie jest on wywoływany z kontekstu macierzystego MOC.

Spójrzmy więc na następny fragment kodu.

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

Nie jest to już wina MagicalRecord, ponieważ MR nie jest nawet używany bezpośrednio tutaj. Jednak zostali Państwo przeszkoleni w zakresie stosowania tych metod, które nie wymagają znajomości modelu współbieżności, więc można albo zapomnieć, albo nigdy nie nauczyć się reguł współbieżności.

Wszystkie obiekty w tablicy results są obiektami zarządzanymi, które znajdują się w kontekście prywatnej kolejki childContext2. W związku z tym użytkownik nie może uzyskać do nich dostępu bez obowiązku hołdowania regułom współbieżności. Jest to wyraźne naruszenie zasad współbieżności. Podczas rozwijania aplikacji należy włączyć debugowanie współbieżności za pomocą argumentu -com.apple.CoreData.ConcurrencyDebug 1.

Ten fragment kodu musi być zawinięty w: performBlock lub performBlockAndWait. Rzadko kiedy używam performBlockAndWait, ponieważ ma tak wiele wad - jednym z nich są zakleszczenia. W rzeczywistości, po prostu zobaczenie użycia performBlockAndWait jest bardzo silnym wskazaniem, że twój impas dzieje się tam, a nie na linii kodu, który wskażesz. Jednak w tym przypadku, to jest co najmniej tak samo bezpieczna jak poprzednie sprowadzić, więc zróbmy to nieco bezpieczniejsze ...

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

Następnie należy przesyłać do głównego wątku. Czy to dlatego, że chcesz po prostu coś zrobić w kolejnym cyklu pętli zdarzeń, czy też dlatego, że ten kod jest już uruchomiony w innym wątku? Kto wie. Masz jednak ten sam problem (sformatowałem Twój kod pod kątem czytelności jako postu).

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); 
    } 
}); 

Teraz wiemy, że kod rozpoczyna się działa na głównym wątku, a wyszukiwarka zwróci performBlockAndWait ale twój kolejny dostęp w pętli for ponownie narusza podstawowe zasady współbieżności danych.

podstawie, że jedyne prawdziwe problemy widzę to ...

  1. MR wydaje się honorować zasady współbieżności podstawowe dane na ich API, ale nadal musisz przestrzegać zasad współbieżności podstawowe dane podczas uzyskiwania dostępu Twoje zarządzane obiekty.

  2. Naprawdę nie podoba mi się użycie performBlockAndWait, ponieważ jest to tylko problem czekający na zdarzenie.

Teraz rzućmy okiem na zrzut ekranu twojego zawieszenia. Hmmm ... to klasyczny impas, ale nie ma sensu, ponieważ impas dochodzi między głównym wątkiem a wątkiem MOC. Może się to zdarzyć tylko wtedy, gdy główna kolejka MOC jest rodzicem tej MOC prywatnej kolejki, ale kod pokazuje, że tak nie jest.

Hmmm ... to nie miało sensu, więc pobrałem twój projekt i spojrzałem na kod źródłowy w przesłanym przez ciebie poduście. Ta wersja kodu używa MR_defaultContext jako elementu nadrzędnego wszystkich MOC-ów utworzonych za pomocą MR_context. Tak więc domyślny MOC jest rzeczywiście główną kolejką MOC, a teraz wszystko ma sens.

Masz MOC jako dziecko głównej kolejki MOC. Po wysłaniu tego bloku do głównej kolejki, jest on teraz uruchomiony jako blok w głównej kolejce. Kod następnie wywołuje performBlockAndWait w kontekście, który jest dzieckiem MOC dla tej kolejki, co jest ogromnym nie-nie, a twojemu prawie gwarantowane jest uzyskanie zakleszczenia.

Wygląda więc na to, że MR od tego czasu zmieniło swój kod z używania kolejki głównej jako rodzica nowych kontekstów na użycie kolejki prywatnej jako elementu nadrzędnego dla nowych kontekstów (najprawdopodobniej z powodu tego problemu). Tak więc, jeśli uaktualnisz do najnowszej wersji MR, powinieneś być w porządku.

Jednak nadal będę Cię ostrzegał, że jeśli chcesz używać MR w wielowątkowych metodach, musisz dokładnie wiedzieć, w jaki sposób radzą sobie z regułami współbieżności, i musisz także upewnić się, że ich przestrzegasz, gdy tylko uzyskasz dostęp do jakichkolwiek danych podstawowych obiekty, które nie przechodzą przez MR API.

Na koniec powiem, że zrobiłem mnóstwo ton danych podstawowych i nigdy nie korzystałem z interfejsu API, który próbuje ukryć problemy ze współbieżnością. Powodem jest to, że jest zbyt wiele drobnych spraw, a ja wolałbym zajmować się nimi w sposób pragmatyczny z góry.

Wreszcie, prawie nigdy nie należy używać performBlockAndWait, chyba że wiesz dokładnie, dlaczego jest to jedyna opcja. Mając go jako część API pod Tobą jest jeszcze bardziej przerażająca ... przynajmniej dla mnie.

Mam nadzieję, że ta mała wyprawa oświeciła Cię i pomogła (i możliwe, że inni). Z pewnością rzuciło mi to trochę światła i pomogło mi odzyskać część mojej poprzedniej bezpodstawnej płochliwości.

Edit

To w odpowiedzi na "nie-magiczne-record" przykład, który podałeś.

Problem z tym kodem jest dokładnie tym samym problemem, który opisałem powyżej, w odniesieniu do tego, co działo się z MR.

Masz kontekst prywatnej kolejki, jako element potomny do kontekstu głównej kolejki.

Uruchamiasz kod w głównej kolejce i wywołujesz performBlockAndWait w kontekście podrzędnym, który musi następnie zablokować kontekst nadrzędny podczas próby pobrania.

Nazywa się impasem, ale bardziej opisowy (i uwodzicielski) termin jest śmiertelnie objąć.

Oryginalny kod działa na głównym wątku. Wzywa dziecko do kontekstu dziecka, aby coś zrobić, i nie robi nic innego, dopóki to dziecko nie ukończy.

To dziecko, aby dokończyć, potrzebuje głównego wątku, aby coś zrobić. Jednak główny wątek nie może nic zrobić, dopóki dziecko nie zostanie ukończone ... ale dziecko czeka, aż główny wątek coś zrobi ...

Żadne z nich nie może zrobić żadnego postępu.

Problem, przed którym stoisz, jest bardzo dobrze udokumentowany, a w rzeczywistości był wielokrotnie wspomniany w prezentacjach WWDC i wielu dokumentach.

Powinieneś NIGDY zadzwonić pod numer performBlockAndWait w kontekście podrzędnym.

Fakt, że uszło ci to na sucho w przeszłości, jest tylko "przypadkiem", ponieważ w ogóle nie powinien działać w ten sposób.

W rzeczywistości prawie nie trzeba dzwonić pod numer performBlockAndWait.

Powinieneś naprawdę przyzwyczaić się do programowania asynchronicznego. Oto, jak zaleciłbym przepisanie tego testu, a cokolwiek to jest, wywołało ten problem.

Najpierw przepisać sprowadzić tak to działa asynchronicznie ...

- (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); 
     } 
    }]; 
} 

Wtedy również zmienić kod, który wywołuje sprowadzić do czegoś takiego ...

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

Oto adres URL próbki, która odtwarza problem bez magicznego zapisu. https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreData.zip Podany projekt ma następujący problem: zakleszczenie przy pobieraniu w performBlockAndWait wywołanym z głównego wątku. Problem zostanie odtworzony, jeśli kod zostanie skompilowany przy użyciu wersji XCode> 6.4. Problem nie jest powielany, jeśli kod został skompilowany przy użyciu xCode == 6.4. Czy ktoś ma jakieś pomysły? –

+0

@KirillNeznamov - Ten kod wykonuje dokładnie to samo, co opisałem, że robi to wersja MR, której używasz. Zobacz edycję po więcej szczegółów. –

+0

Dzięki Jody, postaram się znaleźć notatki w oficjalnej dokumentacji jabłek dotyczące odpowiedzi. Dziękuję Ci bardzo! –

1

Zmieniliśmy do XCode7 i właśnie wpadłem na podobny problem zakleszczenia z performBlockAndWait w kodzie, który działa dobrze w XCode6.

Wydaje się, że problemem jest wcześniejsze użycie parametru dispatch_async(mainQueue, ^{ ... w celu przekazania wyniku operacji sieciowej. To wywołanie nie było już potrzebne po dodaniu obsługi współbieżności dla CoreData, ale jakoś pozostało i nigdy dotąd nie sprawiało problemu.

Możliwe, że firma Apple zmieniła coś za kulisami, aby potencjalne zakleszczenia stały się bardziej wyraźne.

+1

Zgłosiłem problem do wsparcia Apple, powiedzieli, że to błąd. Wysłałem im zmodyfikowane źródła, które odtwarzają problem bez magicznych ram zapisu. Powinniśmy więc poczekać, aż Apple to naprawi. Przy okazji stwierdzili, że problem można rozwiązać za pomocą metody reset() NSManagedContext –

Powiązane problemy