iOS多線程開發技術

進程與線程的聯繫與區別

  • 進程(process)是一塊包含了某些資源的內存區域,操作系統利用進程把它的工作劃分爲一些功能單元。進程是操作系統的基礎,是一次程序的執行;它是操作系統動態執行的基本單元,在傳統的操作系統中,進程是基本的分配單元,也是基本的執行單元。即正在進行中的程序被稱爲進程,負責程序運行的內存分配,每一個進程都有自己獨立的虛擬內存空間。

  • 線程(thread):一個進程要想執行任務,必須得有線程(每一個進程至少要有一條線程)。線程是進程的基本執行單元,一個進程(程序)的所有任務都在線程中執行。主線程最大佔1M的棧區空間,每條子線程最大佔512K的棧區空間 。

  • 進程和線程都是由操作系統所體現的程序運行的基本單元,系統利用該基本單元實現系統對應用的併發性(併發是指兩個或多個任務在同一時間間隔內發生,但是在任意一個時間點CPU只會處理一個任務),進程是線程的容器,真正完成代碼執行的過程,而進程則作爲線程的執行環境。

  • 兩者的主要區別在於它們是不同的操作系統資源管理方式。進程有獨立的地址空間,一個進程崩潰後,在保護模式的影響下不會對其他進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等同於整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變量的併發操作,只能用線程,不能用進程。

多線程的實現

  • 創建線程的目的:開啓一條新的執行路徑,運行指定的代碼,與主線程中的代碼實現同時運行。

  • 多線程的優勢:

    1、充分發揮多核處理器優勢,將不同線程任務分配給不同的處理器,真正進入”並行運算”狀態;

    2、將耗時的任務分配到其他線程執行,由主線程負責統一更新界面會使應用程序更加流暢,用戶體驗更好;

    3、當硬件處理器的數量增加,程序會運行更快,而程序無需做任何調整。

  • 多線程的劣勢:新建線程會消耗內存空間和CPU時間,線程太多會降低系統的運行性能。

  • 使用多線程的情況:

    1、大量運算,比如for循環計算量特別大的時候;

    2、數據讀取(本地),數據庫查詢所有的東西;

    3、網絡請求的時候(同步)。

  • 線程安全:當多個線程同時訪問一個資源時,會出現線程安全問題。爲避免出現線程安全問題,需要在代碼中保證數據、變量的線程安全。UI Kit 中的所有對象默認都是非線程安全的,需要在主線程刷新UI界面元素。iOS中保證變量線程安全的方法爲atomic 特性屬性聲明、@synchronized 關鍵字和NSLock 線程鎖。

  • 多線程的實現技術

    iOS有三種多線程編程的技術,分別是NSThread、NSOperation和GCD(Grand Central Dispatch)。

    1、NSThread
    即簡單開闢一個線程處理需要放到後臺的操作。NSThread 比其他兩個輕量級,但缺點是需要自己管理線程的生命週期,線程同步,線程同步對數據的加鎖會有一定的系統開銷。

    兩種創建方法:
    (1)NSThread *thread1 = [[[NSThread alloc] initWithTarget:self selector:@selector(calculator)
    object:nil] autorelease];
    [thread1 start];
    (2)[NSThread detachNewThreadSelector:@selector(calculator) toTarget:self withObject:nil];

    常用的方法:
    // 取消線程
    - (void)cancel NS_AVAILABLE(10_5, 2_0);
    // 執行線程
    - (void)start NS_AVAILABLE(10_5, 2_0);

    2、NSOperation
    即將一個操作封裝成對象,並開闢一個新線程執行。不需要關心線程管理,數據同步的事情,可以把精力放在自己需要執行的操作上。與之相關的類是 NSOperation ,NSOperationQueue。NSOperation是個抽象類,使用它必須用它的子類(並重寫main方法,加入自定義操作),可以實現它或者使用它定義好的兩個子類:NSInvocationOperation和NSBlockOperation。創建NSOperation子類的對象,把對象添加到NSOperationQueue隊列裏執行。
    NSBlockOperation的使用:
    // 執行隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // 開闢線程做什麼?
    }];
    [blockOperation setCompletionBlock:^{
    // 線程執行結束後做什麼?
    }];
    [queue addOperation:blockOperation];

    NSInvocationOperation使用:
    // 執行隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 開闢線程
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc]
    initWithTarget:self selector:@selector(loadWithURL:) object:IMGURL];
    [queue addOperation:invocationOperation];

    3、GCD
    GCD是一種較爲底層的多線程實現方式,其原理是將操作封裝爲block,並加入指定隊列中開闢新線程執行。NSOperation以及NSOperationQueue都是對GCD機制的高層封裝,使用GCD可以實現更加靈活的多線程處理。

    優點:
    (1)通過GCD,開發者不用再直接跟線程打交道,只需要向隊列中添加代碼塊即可;
    (2)GCD在後端管理着一個線程池,GCD不僅決定着代碼塊將在哪個線程被執行,它還根據可用的系統資源對這些線程進行管理,從而讓開發者從線程管理的工作中解放出來;通過集中的管理線程,緩解大量線程被創建的問題;

    調度隊列(dispath queue)
    GCD的核心理念:將長期運行的任務拆分成多個工作單元,並將這些單元添加到dispath queue中,系統會爲我們管理這些dispath queue,爲我們在多個線程上執行工作單元,我們不需要直接啓動和管理後臺線程。GCD的dispath queue嚴格遵循FIFO(先進先出)原則,添加到dispath queue的工作單元將始終按照加入dispath queue的順序啓動。
    dispatch queue按先進先出的順序,串行或併發地執行任務:
    (1) serial dispatch queue一次只能執行一個任務, 當前任務完成纔開始出列並啓動下一個任務;
    (2)concurrent dispatch queue則儘可能多地啓動任務併發執行。

    創建和管理dispatch queue

  • 獲得全局併發Dispatch Queue (concurrent dispatch queue)
    (1)併發dispatch queue可以同時並行地執行多個任務,不過併發queue仍然按先進先出的順序來啓動任務。併發queue會在之前的任務完成之前就出列下一個任務並開始執行。併發queue同時執行的任務數量會根據應用和系統動態變化,各種因素包括:可用核數量、其它進程正在執行的工作數量、其它串行dispatch queue中優先任務的數量等。
    (2)系統給每個應用提供三個併發dispatch queue,整個應用內全局共享,三個queue的區別是優先級。不需要顯式地創建這些queue,使用dispatch_get_global_queue函數便可獲取這三個queue。
    // 獲取默認優先級的全局併發dispatch queue
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    注:第一個參數用於指定優先級,分別使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW兩個常量來獲取高和低優先級的兩個queue;第二個參數目前未使用到,默認0即可。
    (3)雖然dispatch queue是引用計數的對象,但不需要retain和release全局併發queue。因爲這些queue對應用是全局的,retain和release調用會被忽略。也不需要存儲這三個queue的引用,每次都直接調用dispatch_get_global_queue獲得queue就行。

  • 創建串行Dispatch Queue (serial dispatch queue)
    (1) 應用的任務需要按特定順序執行時,就需要使用串行Dispatch Queue,串行queue每次只能執行一個任務。可以使用串行queue來替代鎖,保護共享資源或可變的數據結構。和鎖不一樣的是,串行queue確保任務按可預測的順序執行。而且只要異步地提交任務到串行queue,就永遠不會產生死鎖。
    (2)必須顯式地創建和管理所有使用的串行queue,應用可以創建任意數量的串行queue,但不要爲了同時執行更多任務而創建更多的串行queue。如果需要併發地執行大量任務,應該把任務提交到全局併發queue。
    (3)利用dispatch_queue_create函數創建串行queue,兩個參數分別是queue名和一組queue屬性。
    dispatch_queue_t queue;
    queue = dispatch_queue_create("cn.itcast.queue", NULL);

  • 運行時獲得公共Queue
    (1)使用dispatch_get_current_queue函數作爲調試用途,或者測試當前queue的標識。在block對象中調用這個函數會返回block提交到的queue(這個時候queue應該正在執行中)。在block對象之外調用這個函數會返回應用的默認併發queue。
    (2)使用dispatch_get_main_queue函數獲得應用主線程關聯的串行dispatch queue。
    (3)使用dispatch_get_global_queue來獲得共享的併發queue。

  • Dispatch Queue的內存管理
    (1) Dispatch Queue和其它dispatch對象(還有dispatch source)都是引用計數的數據類型。當創建一個串行dispatch queue時,初始引用計數爲 1,可以使用dispatch_retain和dispatch_release函數來增加和減少引用計數。當引用計數到達 0 時,系統會異步地銷燬這個queue。
    (2)對dispatch對象(如dispatch queue)retain和release 是很重要的,確保它們被使用時能夠保留在內存中。和OC對象一樣,通用的規則是如果使用一個傳遞過來的queue,應該在使用前retain,使用完之後release。
    (3)不需要retain或release全局dispatch queue,包括全局併發dispatch queue和main dispatch queue。
    (4) 即使實現的是自動垃圾收集的應用,也需要retain和release創建的dispatch queue和其它dispatch對象。GCD 不支持垃圾收集模型來回收內存。

    添加任務到queue

    要執行一個任務,需要將它添加到一個適當的dispatch queue,可以單個或按組來添加,也可以同步或異步地執行一個任務。一旦進入到queue,queue會負責儘快地執行任務。一般可以用一個block來封裝任務內容。

  • 添加單個任務到queue
    (1)異步添加任務:異步或同步地添加一個任務到Queue,儘可能地使用dispatch_async或dispatch_async_f函數異步地調度任務。因爲添加任務到Queue中時,無法確定這些代碼什麼時候能夠執行。因此異步地添加block或函數,可以讓你立即調度這些代碼的執行,然後調用線程可以繼續去做其它事情。特別是應用主線程一定要異步地 dispatch 任務,這樣才能及時地響應用戶事件。
    (2)同步添加任務:少數時候可能希望同步地調度任務,以避免競爭條件或其它同步錯誤。使用dispatch_sync和dispatch_sync_f函數同步地添加任務到Queue,這兩個函數會阻塞當前調用線程,直到相應任務完成執行。注意:絕對不要在任務中調用 dispatch_sync或dispatch_sync_f函數,並同步調度新任務到當前正在執行的 queue。對於串行queue這一點特別重要,因爲這樣做肯定會導致死鎖;而併發queue也應該避免這樣做。
    (3)代碼演示
    // 調用前,查看當前線程
    NSLog(@"當前調用線程:%@", [NSThread currentThread]);
    // 創建一個串行queue
    dispatch_queue_t queue = dispatch_queue_create("cn.itcast.queue", NULL);
    dispatch_async(queue, ^{
    NSLog(@"開啓了一個異步任務,當前線程:%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
    NSLog(@"開啓了一個同步任務,當前線程:%@", [NSThread currentThread]);
    });
    // 銷燬隊列
    dispatch_release(queue);

    打印信息:
    2016-03-03 09:03:37.348 thread[6491:c07] 當前調用線程:<NSThread: 0x714fa80>{name = (null),
    num = 1}
    2016-03-03 09:03:37.349 thread[6491:1e03] 開啓了一個異步任務,當前線程:<NSThread: 0x74520a0>
    {name = (null), num = 3}
    2016-03-03 09:03:37.350 thread[6491:c07] 開啓了一個同步任務,當前線程:<NSThread: 0x714fa80>
    {name = (null), num = 1}

  • 併發地執行循環迭代
    使用循環執行固定次數的迭代,併發dispatch queue可以提高性能。
    int i;
    int count = 10;
    for (i = 0; i < count; i++) {
    printf("%d ",i);
    }

    如上面的for循環,如果每次迭代執行的任務與其它迭代獨立無關,而且循環迭代執行順序也無關緊要,可以調用dispatch_apply或dispatch_apply_f函數來替換循環。這兩個函數爲每次循環迭代將指定的block或函數提交到queue。當dispatch到併發 queue時,就有可能同時執行多個循環迭代。用dispatch_apply或dispatch_apply_f時可以指定串行或併發 queue。併發queue允許同時執行多個循環迭代,而串行queue就沒太大必要使用了。
    下面的代碼使用dispatch_apply替換了for循環,傳遞的block必須包含一個size_t類型的參數,用來標識當前循環迭代。第一次迭代這個參數值爲0,最後一次值爲count - 1。
    // 獲得全局併發queue
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    size_t count = 10;
    dispatch_apply(count, queue, ^(size_t i) {
    printf("%zd ", i);
    });
    // 銷燬隊列
    dispatch_release(queue);

    打印信息爲:
    1 2 0 3 4 5 6 7 8 9
    可以看出,這些迭代是併發執行的。和普通for循環一樣,ispatch_apply和dispatch_apply_f函數也是在所有迭代完成之後纔會返回,因此這兩個函數會阻塞當前線程,主線程中調用這兩個函數必須注意,可能會阻止事件處理循環並無法響應用戶事件。所以如果循環代碼需要一定的時間執行,可以考慮在另一個線程中調用這兩個函數。如果傳遞的參數是串行queue,而且正是執行當前代碼的queue,就會產生死鎖。

  • 在主線程中執行任務
    (1) GCD提供一個特殊的dispatch queue,可以在應用的主線程中執行任務。只要應用主線程設置了run loop(由CFRunLoopRef類型或NSRunLoop對象管理),就會自動創建這個queue,並且最後會自動銷燬。非Cocoa應用如果不顯式地設置run loop, 就必須顯式地調用dispatch_main函數來顯式地激活這個dispatch queue,否則雖然可以添加任務到queue,但任務永遠不會被執行。
    (2) 調用dispatch_get_main_queue函數獲得應用主線程的dispatch queue,添加到這個queue的任務由主線程串行化執行
    (3)代碼實現:比如異步下載圖片後,回到主線程顯示圖片。
    // 異步下載圖片
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSURL *url = [NSURL URLWithString:@"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg"];
    UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
    // 回到主線程顯示圖片
    dispatch_async(dispatch_get_main_queue(), ^{
    self.imageView.image = image;
    });
    });

  • 任務中使用Objective-C對象
    GCD支持Cocoa內存管理機制,因此可以在提交到queue的block中自由地使用Objective-C對象。每個dispatch queue維護自己的autorelease pool確保釋放autorelease對象,但是queue不保證這些對象實際釋放的時間。如果應用消耗大量內存,並且創建大量autorelease對象,需要創建自己的autorelease pool,用來及時地釋放不再使用的對象。

    暫停和繼續queue

    可以使用dispatch_suspend函數暫停一個queue以阻止它執行block對象;用dispatch_resume函數繼續dispatch queue。調用dispatch_suspend會增加queue的引用計數,調用dispatch_resume則減少queue的引用計數。當引用計數大於0時,queue就保持掛起狀態。因此你必須對應地調用suspend和resume函數。掛起和繼續是異步的,而且只在執行block之間(比如在執行一個新的block之前或之後)生效。掛起一個queue不會導致正在執行的block停止。

    Dispatch Group的使用

    假設需要從網絡上下載兩張不同的圖片,然後顯示到不同的UIImageView上去,可以這樣:

    // 根據url獲取UIImage  
    - (UIImage *)imageWithURLString:(NSString *)urlString {  
    NSURL *url = [NSURL URLWithString:urlString];  
    NSData *data = [NSData dataWithContentsOfURL:url];  
    return [UIImage imageWithData:data];  
    }  
    - (void)downloadImages {  
    // 異步下載圖片  
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
        // 下載第一張圖片  
        NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
        UIImage *image1 = [self imageWithURLString:url1];  
        // 下載第二張圖片  
        NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
        UIImage *image2 = [self imageWithURLString:url2];  
        // 回到主線程顯示圖片  
        dispatch_async(dispatch_get_main_queue(), ^{  
            self.imageView1.image = image1;  
            self.imageView2.image = image2;  
        });  
    });  
    }

    雖然這種方案可以解決問題,但其實兩張圖片的下載過程並不需要按順序執行,併發執行它們可以提高執行速度。有個注意的地方就是必須等兩張圖片都下載完畢後才能回到主線程顯示圖片。Dispatch Group能夠在這種情況下幫我們提升性能。
    下面先看看Dispatch Group的用處:可以使用dispatch_group_async函數將多個任務關聯到一個Dispatch Group和相應的queue中,group會併發地同時執行這些任務。而且Dispatch Group可以用來阻塞一個線程, 直到group關聯的所有的任務完成執行。有時候必須等待任務完成的結果,然後才能繼續後面的處理。
    用Dispatch Group優化上面的代碼:

// 根據url獲取UIImage  
- (UIImage *)imageWithURLString:(NSString *)urlString {  
    NSURL *url = [NSURL URLWithString:urlString];  
    NSData *data = [NSData dataWithContentsOfURL:url];  
    // 這裏並沒有自動釋放UIImage對象  
    return [[UIImage alloc] initWithData:data];  
}  

- (void)downloadImages {  
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  

    // 異步下載圖片  
    dispatch_async(queue, ^{  
        // 創建一個組  
        dispatch_group_t group = dispatch_group_create();  

        __block UIImage *image1 = nil;  
        __block UIImage *image2 = nil;  

        // 關聯一個任務到group  
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
            // 下載第一張圖片  
            NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
            image1 = [self imageWithURLString:url1];  
        });  

        // 關聯一個任務到group  
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
            // 下載第一張圖片  
            NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
            image2 = [self imageWithURLString:url2];  
        });  

        // 等待組中的任務執行完畢,回到主線程執行block回調  
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{  
            self.imageView1.image = image1;  
            self.imageView2.image = image2;  

            // 千萬不要在異步線程中自動釋放UIImage,因爲當異步線程結束,異步線程的自動釋放池也會被銷燬,那麼UIImage也會被銷燬  

            // 在這裏釋放圖片資源  
            [image1 release];  
            [image2 release];  
        });  

        // 釋放group  
        dispatch_release(group);  
    });  
}  

dispatch_group_notify函數用來指定一個額外的block,該block將在group中所有任務完成後執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章