iOS并發編程Tips(二)

WolfgangSpr 8年前發布 | 10K 次閱讀 并發 iOS開發 移動開發

iOS并發編程Tips(一) 中,我們提到了三點,分別是線程、原子屬性和并發同步。在本文中,你將會看到以下幾點:

  • 線程安全

  • 使用主線程

  • GCD 還是 NSOperationQueue

線程安全

線程安全是編程中的術語,指某個函數、函數庫在多線程環境中被調用時,能夠正確地處理多個線程之間的共享變量,使程序功能正確完成。 — 維基百科

舉個例子。

我們定義一個 NSInteger 型的全局變量 count ,我們使用三個異步線程將它自增100000,那么,我們希望的輸出結果是300000。但是,它的真實結果是多少呢?

#import "ViewController.h"

@interface ViewController ()
@property (assign, nonatomic) NSInteger count;
@end

@implementation ViewController

-(void)viewDidLoad
{
    [super viewDidLoad];

    for (int i = 0; i < 3; i++)
    {
        [self startThread];
    }
}
-(void)startThread
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self addCount];
    });
}
-(void)addCount
{
    for (int i = 0; i < 100000; i++)
    {
        self.count++;
    }
    NSLog(@"count = %ld", self.count);
}
-(void)addCountWithLock
{
    @synchronized (self)
    {
        for (int i = 0; i < 100000; i++)
        {
            self.count++;
        }
        NSLog(@"lock count = %ld", self.count);
    }
}
@end

運行結果顯然不是我們想要的,而且,每次的結果都不一定一致,這就是我們所要說的線程安全。

很多時候,我們為了效率,會編寫多線程的代碼。多線程除了會帶來效率的提升之外,也會提高控制的復雜程度。我們有很多解決辦法,比如說,使用鎖、不可變變量、盡量使用主線程(單線程)等等。

在上述例子中,我們如果加一個最簡單的互斥鎖( addCountWithLock 方法),就可以達到線程安全的目的。

運行結果正是我們想要的。

還有一點想提及一下的是, 蘋果有個文檔 列出了部分框架的部分安全和非安全的類和函數,可以適當看一下。

上面提到了鎖,我們常用的鎖有很多,比如,互斥鎖、條件鎖、遞歸鎖、信號量、自旋鎖等等。網上有很多關于這方面的資料,我就不再贅述了,畢竟篇幅很大,而我這篇只是Tips。

網上也有很多關于這些鎖性能對比的文章,比如說 ibireme的文章 等等。

這么多鎖,除了比較特殊的遞歸鎖等,如果你想要一個高性能的鎖的話,可以使用 pthread_mutex 或者 dispatch_semaphore ,如果想使用比較方便的話,以直接使用 @synchronized 和 NSLock 。

使用主線程

在性能優化的時候,我們很容易陷入過度優化的誤區。現在的設備性能越來越好,我們可以在主線程中做越來越多的事情。

如果某個函數或者方法只有主線程去訪問,那它必然是多線程安全的,因為只有單線程訪問,不存在多線程的情況。

我們知道 NSMutableArray 、 NSMutableDictionary 這種的是非線程安全的類,在我的使用過程中,我一般不會對這些東西加鎖,因為我基本只用主線程去訪問,而如果涉及到多線程的話,我會使用不可變的數組和字典。

在大多數情況下,使用多線程只存在于某一個部分,比如網絡等,那么在多線程執行完成之后,一定要交由主線程回調。比如,我們常用的 AFNetworking 中,在回調 success 和 failure 的block塊的過程中,就會回調到主線程上:

dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
                            success(self, responseObject);
                        });

除了我們自己設計的庫需要這么做以外,也有一些系統上的方法需要我們注意。比如, NSNotification 。

NSNotification 是哪個線程去post就是哪個線程去調用 selector 。我們來測試一下:

[[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(test)
                                                 name:kTestNotification
                                               object:nil];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:kTestNotification object:nil];
});

我們在 test 方法上打個斷點,我們會看到:

這樣就會有問題,如果 test 方法內是執行UI操作或者某些需要主線程的操作的話,那么有可能會造成UI無響應,或者很長時間才變化,甚至是崩潰。

所以,我建議一定要在主線程上post,因為你不知道你所發出的 NSNotification 誰會去接收,它又要去干什么,但是你知道,主線程是肯定沒錯的。

實現這個的方法有很多,比如繼承、category、hook等。

前段時間在寫指紋解鎖的時候碰到一個問題。在我的App中需要驗證指紋或者手勢密碼才可以進入主頁,而驗證指紋需要用到這么一個方法:

-(void)evaluatePolicy:(LAPolicy)policy
       localizedReason:(NSString *)localizedReason
                 reply:(void(^)(BOOL success, NSError * __nullable error))reply;

測試的時候,我發現一個問題,在用戶驗證通過之后, alertView 消失之后,頁面并沒有跳到主頁。有時候需要過好久才會跳到主頁,但是頁面并沒有卡死,手勢解鎖依舊可用。這就奇了怪了,我找了一圈才發現,這個方法是在子線程上回調回來的,而我并不知道。所以我用這個子線程去初始化頁面的時候,就會出現長時間無響應的問題。

所以,系統異步回調的接口一定要去檢查一下是不是主線程的。

GCD 還是 NSOperationQueue

我們知道,在 iOS 4 以上, NSOperationQueue 是在GCD上封裝上來的,相比起GCD, NSOperationQueue 具有如下一些優點:

  • 提供cancel操作。

  • 更細粒度的優先級控制。

  • 支持繼承,方便封裝。

  • 支持KVO。

而GCD相比起 NSOperationQueue 的優點是:

  • 使用方便、簡單。

  • 速度可能更快一點。

我相信,對于大部分好的封裝來說,會優先選擇 NSOperationQueue 。而如果你只是一個很小的項目,以使用方便為主,那么,使用GCD也是一種不錯的選擇。

博文鏈接: http://ifujun.com/iosbing-fa-bian-cheng-tips-er/

 本文由用戶 WolfgangSpr 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!