CoreData整理(二)——多線程方案

CoreData整理(二)——多線程方案


目錄

  • 爲什麼使用多線程
  • 如何使用多線程
  • 多線程方案

爲什麼使用多線程

    到了這裏你必定會問,增刪改查功能已經實現了,用的好好的爲何要使用多線程呢?其實想想,Core Data畢竟是數據持久化技術,若是數據量大的話,使用主線程操做一定會產生線程擁塞。而UI的更新就是在主線程中進行的,這將會致使你的app界面「卡住」。此外當你須要同時執行多個操做時也須要使用多線程。ios


如何使用多線程

最初想法:
    對於如何去實現,你首先可能會想到的是以下圖的方案:實例化一個MOC對象,當有須要執行的操做時就開闢一個線程去執行。可是這樣是不行的,因爲MOC和MO不是線程安全的,對MO進行的操做和使用MOC進行的操做並不會上鎖去保證操做的原子性。若是多線程共用MOC的話會出現數據混亂,甚至更嚴重的會致使程序崩潰。git

clipboard.png

例如以下代碼,先Add20條數據,再執行Update操做。下面的代碼在屢次頻繁執行時會crash。
咱們可以簡單分析出來,因爲MOC是同一個,因此在線程A中的for循環中執行時,有可能線程B已經執行完畢。在這種狀況下,線程A中新增的一部分由MOC監聽的MO對象會在線程B中被提早Save。這樣的狀況下兩個操做混雜在了一塊兒,嚴重的會產生crash。github

// 線程A執行Add操做
NSMutableArray *arr = [NSMutableArray array];
for (int i = 0; i < 20; i++) {
    [arr addObject:@{@"id": @"111", @"name": @"aaa"}];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSManagedObjectContext *context = self.manager.moc;
    int i = 1;
    for (NSDictionary *params in arr) {
        User *user = [NSEntityDescription insertNewObjectForEntityForName:EntityName inManagedObjectContext:context];
        user.userID = params[@"id"];
        user.name   = params[@"name"];
        // 模擬在添加了5條數據以後,線程B執行完成Update操做
        if (i == 5) {
            sleep(2);
        }
        i++;
    }
    [self.manager saveContext];
});
// 線程B執行Update操做
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSManagedObjectContext *context = self.manager.moc;
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:EntityName];
    NSArray *resultArray = [context executeFetchRequest:fetchRequest error:nil];
    for (User *user in resultArray) {
        user.name = @"newName";
    }
    [self.manager saveContext];
});

正確的作法:macos

CoreData不是線程安全的(例子如上),對於ManagedObject以及ManagedObjectContext的訪問都只能在對應的線程上進行,而不能跨線程。蘋果推薦的作法是,一個線程使用一個NSManagedObjectContext對象。因爲在每一個線程中的context是不一樣的,並且它只管理本身監聽的MO,context之間互不影響,因此不會出現context保存前它所監聽的MO被其餘context篡改或者提早提交的狀況。segmentfault

clipboard.png

API中提供的方法:安全

NSManagedObjectContext的類型:
實例化時提供了3種類型來方便進行多線程管理:多線程

NSConfinementConcurrencyType(iOS 9廢棄)
NSPrivateQueueConcurrencyType
NSMainQueueConcurrencyType

NSManagedObjectContext提供的多線程執行方法:
API中提供了多線程執行方法,使得咱們不須要去本身維護線程隊列或開啓線程。併發

- (void)performBlock:(void (^)())block NS_AVAILABLE(10_7,  5_0);
- (void)performBlockAndWait:(void (^)())block NS_AVAILABLE(10_7,  5_0);

1.對於NSConfinementConcurrencyType類型,iOS 9以後過時,context在實例化時並不會自動建立隊列,須要本身管理多線程實現併發。當該類型的context使用上述的兩個方法時會出現以下的crash。app

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Can only use -performBlock: on an NSManagedObjectContext that was created with a queue.

2.對於NSPrivateQueueConcurrencyType類型,該上下文會建立並管理一個私有隊列(串行隊列)。當你想要異步執行某個操做時,能夠在performBlock方法的block中執行。異步

NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

// 私有類型上下文執行performBlock方法
[privateContext performBlock:^{
    NSLog(@"privateContext block: %@", [NSThread currentThread]);
}];
// 至關於:串行隊列 異步 執行block
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);  // 只建立一個queue與context綁定,每次都使用這一個queue
dispatch_async(queue, ^{
    NSLog(@"privateContext block: %@", [NSThread currentThread]);
});
 
// 私有類型上下文執行performBlockAndWait方法
[privateContext performBlockAndWait:^{
    NSLog(@"privateContext blockAndWait: %@", [NSThread currentThread]);
}];
// 至關於:無隊列線程操做,在當前線程中直接執行block
NSLog(@"privateContext blockAndWait: %@", [NSThread currentThread]);

3.對於NSMainQueueConcurrencyType類型,該上下文會關聯主隊列。若是有UI對象執行的操做或者是須要在主線程中執行的操做,可使用該類型。

NSManagedObjectContext *mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

// 主類型上下文執行performBlock方法
[mainContext performBlock:^{
    NSLog(@"mainContext block: %@", [NSThread currentThread]);
 }];
// 至關於:主隊列 異步 執行block
 dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"mainContext block: %@", [NSThread currentThread]);
 });
 
// 主類型上下文執行performBlockAndWait方法
[mainContext performBlockAndWait:^{
    NSLog(@"mainContext blockAndWait: %@", [NSThread currentThread]);
}];
// 至關於在主線程中直接執行block
if ([NSThread isMainThread]) {
    NSLog(@"mainContext blockAndWait: %@", [NSThread currentThread]);
} else {
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"mainContext blockAndWait: %@", [NSThread currentThread]);
    });
}

官方文檔


多線程方案

tip:demo在最後

方案一

使用兩個MOC,一個負責在後臺處理各類耗時的操做,一個負責與UI進行協做。

clipboard.png

存在的問題

咱們知道MOC和MO不是線程安全的,爲了解決這個問題咱們在一個線程中僅使用一個MOC,不能跨線程訪問同一個MOC和MO。可是這會存在問題。好比:使用一個context異步執行刪除操做,首先查詢,在查詢出結果時恰好另外一個context更新了這些數據,刪除操做在以後保存時是不知道數據被修改了,最終會致使刪除失敗。(該問題的研究,詳見Demo中UserDao類的testMergeChanges方法)

爲了解決這個問題,咱們須要使用通知來監聽私有上下文的保存動做,並將更改的信息合併到其餘上下文中:

// 上下文提交保存後的通知name
NSManagedObjectContextDidSaveNotification
// 將通知中上下文提交的信息合併到執行該方法的上下文中
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification NS_AVAILABLE(10_5, 3_0);

方案二

經過創建上下文間的父子關係,避免上下文的合併操做。

clipboard.png

iOS5.0以後新增了MOC之間的父子關係,子上下文的改動保存時會提交給父上下文,最後由根部的上下文提交全部改動給PSC。所以創建關係以後,上下文的改動就不須要用通知去告知其餘上下文了。咱們能夠經過設置以下屬性來設置父上下文。

@property (nullable, strong) NSManagedObjectContext *parentContext API_AVAILABLE(macosx(10.7),ios(5.0));

方案二將使用三層的MOC去實現多線程Core Data,privateContext -> mainContext -> rootContext

clipboard.png

其中privateContext用於執行操做,mainContext用於與UI協做,rootContext用於在後臺保存全部子上下文的提交。

存在的問題
MO都有惟一的MOID與之對應,爲了不實例化MO時消耗大量資源來確保ID的惟一性,因此MO在實例化時會被給予一個臨時的ID,這個ID在MOC範圍內惟一。當MOC進行提交時,須要將臨時ID轉化爲全局ID,因此咱們須要監聽MOC將要保存的通知來處理MOID的轉換:

// 上下文將要提交保存的通知name
NSManagedObjectContextWillSaveNotification
// MOID轉換方法
- (BOOL)obtainPermanentIDsForObjects:(NSArray<NSManagedObject *> *)objects error:(NSError **)error NS_AVAILABLE(10_5, 3_0);

代碼

方案一初始化:

CoreDataManager.m:
clipboard.png

方案二初始化:

CoreDataManager.m:
clipboard.png
clipboard.png

公共輔助方法:

UserDao.m:
clipboard.png

增:

clipboard.png

刪:

clipboard.png

改:

clipboard.png

查:

clipboard.png
clipboard.png


後續

CoreData整理(一)——基本概念與簡單使用
CoreData整理(三)——MagicalRecord的使用
CoreData整理(四)——數據遷移和其餘問題
Demo地址


參考文章

Core Data 線程大揭祕
iOSCoreData詳解(五)多線程

相關文章
相關標籤/搜索