1. 程式人生 > >Objective-C多執行緒詳解(NSThread、NSOperation、GCD)

Objective-C多執行緒詳解(NSThread、NSOperation、GCD)

程序和執行緒

程式:一個由原始碼生成的可執行應用(比如qq,微信…)

程序:程序是指在系統中正在執行的一個應用程式。一個正在執行的程式可以看成一個程序,程序負責去向手機系統申請資源,同時將這些資源排程給我們的執行緒

執行緒:1個程序要想執行任務,必須得有執行緒(每1個程序至少要有1條執行緒),可以看成是可以執行的程式碼段,這些程式碼段需要的資源,比如記憶體,都需要去向程序申請。執行緒是進 程的基本執行單元,一個程序(程式)的所有任務都線上程中執行。

單執行緒:只有一個現成的程式叫做單執行緒程式,如果是單執行緒則肯定是主執行緒。

多執行緒:擁有多個執行緒的程式叫做多執行緒程式,1個程序中可以開啟條多執行緒每條執行緒可以並行(同時)執行不同的任務。比如同時開啟3條執行緒分別下載3個檔案,多執行緒技術可以提高程式的執行效率,但相對佔用資源。其實多執行緒的原理仍是單執行緒,其實是CPU快速地在多條執行緒之間排程(切換),如果CPU排程執行緒的時間足夠快,就造成了多執行緒併發執行的假象。

除了主執行緒之外的執行緒都是子執行緒,程式中只有一個主執行緒。主執行緒是順序執行的,上一個任務執行完,才會執行下一個任務。子執行緒之間是並行執行的,也就是同時執行的。

注意: 我們規定我們的UI新增和重新整理程式碼必須放在我們的主執行緒中去做

    [NSThread mainThread]; //獲取到主執行緒
    [NSThread currentThread]; //獲取到當前程式碼執行的執行緒

1,NSThread

NSThread適合輕量級多執行緒開發,控制執行緒順序比較難,同時執行緒總數無法控制(每次建立並不能重用之前的執行緒,只能建立一個新的執行緒)。

優點:NSThread相對比較輕量級
缺點:需要自己管理執行緒生命週期,執行緒同步,執行緒同步對資料加鎖有一定的系統開銷;

NSThread實現的三種方式:
1.

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadInitDoSomething) object:nil];

    [thread start];

這種方法需要手動開啟執行緒。
2.

[NSThread detachNewThreadSelector:@selector(threadDetachDosomething) toTarget:self withObject:nil];

這種便利構造的方法,不需要手動開啟。
3.

[self performSelectorInBackground:@selector(backGround) withObject:nil]; 

這種方式是NSObject物件自帶的開啟後臺執行緒的方法。

2,NSOperation

優點:不需要手動關係執行緒,可以把精力放在自己要執行的操作上面,NSOperation是一個抽象類,不能被直接初始化,NSOperation我們一般使用它的子類NSInvocationOperation,NSBlockOperation或者繼承NSOperation的自定義任務,我們經常將任務和佇列NSOperationQueue進行搭配使用,一個物件一個任務,更利於任務的管理,還有一個優點在於可以明確的確定依賴關係。
缺點:他是一個OC物件,那麼相對於C函式效率要低,而且基於GCD,那麼GCD將提供比他更加全面的功能。
NSOperation使用:
1.

NSInvocationOperation *incocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationDoSomething) object:nil];
    [incocation start];

這種方式利用的Target-Action的設計模式,讓響應者去執行任務。
2.

NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%d, %@", [NSThread isMainThread], [NSThread currentThread]);
    }];
    [block start];

這種方式利用OC裡面經典的語法block(語法塊)。但是和上者一樣,如果單獨使用NSOperation的子類物件必須手動的開啟任務。
3.

//NSOperationQueue裡面只有序列的時候執行緒優先順序才是可行的
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //最大併發量,如果最大病發量為1時,那麼佇列裡面的任務將序列,也就是執行完一個任務才能執行下一個,如果不為1,那就是併發進行。
    queue.maxConcurrentOperationCount = 1;

    //新增block塊任務
    NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--0--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
    }];
    //設定任務的優先順序,只有佇列為序列的時候優先順序才能起到絕對的作用
    [block setQueuePriority:NSOperationQueuePriorityVeryHigh];
    NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--1--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
    }];
    [block1 setQueuePriority:NSOperationQueuePriorityNormal];
    NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--2--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
    }];
    [block2 setQueuePriority:NSOperationQueuePriorityVeryLow];
    NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--3--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
    }];
    [block3 setQueuePriority:NSOperationQueuePriorityVeryHigh];

    //設定以來關係,只有執行完block之後才會去執行block1,這叫做任務block1依賴於block,在專案開發中經常用到
    [block1 addDependency:block];
    //新增任務,任務為NSOperation物件的子類,新增到任務佇列中之後會自動去執行,不需要start;
    [queue addOperation:block];
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];

NSOperationQueue在開發中經常會使用到,比如我們做多工下載的時候,使用自定義NSOperation子類和NSOperationQueue結合使用,每個NSOperation物件是一個任務,而NSOperationQueue卻完美的擔任了任務關係器的角色。抽時間會把demo上傳到github上面,請及大家及時關注。任務之間的依賴也是NSOperation的一大完美特徵。

3,GCD

Grand Central Dispatch (GCD)是Apple開發的一個多核程式設計的解決方法。是基於 C 的 API,函式級別的多執行緒方法,效率更高。有如下特點:

1.GCD 能通過推遲昂貴計算任務並在後臺執行它們來改善你的應用的響應效能。
2.GCD 提供一個易於使用的併發模型而不僅僅只是鎖和執行緒,以幫助我們避開併發陷阱。
3.GCD 具有在常見模式(例如單例)上用更高效能的原語優化你的程式碼的潛在能力。

4.GCD 使用後不用程式去管理執行緒的開閉,GCD會在系統層面上去動態檢測系統狀態,開閉執行緒。

總的來說就是效率高,更容易的利用多核處理器並行處理任務。

GCD 佇列(dispatch queue)大體上分2種:

serial (序列佇列): 一次只能執行一個任務,必須上一個任務執行完,下一個任務才開始,遵循FIFO 原則->哪個任務先進入這個佇列就先執行(先進先出)
concurrent (並行佇列):佇列會根據當前系統的情況(記憶體…)來建立子執行緒,同時將這些任務進行分發

或者分成以下三種:

1)執行在主執行緒的Main queue,通過dispatch_get_main_queue獲取。

2)並行佇列global dispatch queue,通過dispatch_get_global_queue獲取,由系統建立三個不同優先順序的dispatch queue。並行佇列的執行順序與其加入佇列的順序相同。

3)序列佇列serial queues一般用於按順序同步訪問,可建立任意數量的序列佇列,各個序列佇列之間是併發的。

當想要任務按照某一個特定的順序執行時,序列佇列是很有用的。序列佇列在同一個時間只執行一個任務。我們可以使用序列佇列代替鎖去保護共享的資料。和鎖不同,一個序列佇列可以保證任務在一個可預知的順序下執行。

serial queues通過dispatch_queue_create建立,可以使用函式dispatch_retain和dispatch_release去增加或者減少引用計數。
下面給大家詳細介紹GCD的使用:

主佇列:

dispatch_queue_t queue = dispatch_get_main_queue();

主佇列是序列佇列,任務是從上到下一個一個執行的。

dispatch_async(queue, ^{
    NSLog(@"這是第一個任務,當前執行緒是:%@, 是否主執行緒 :%d ", [NSThread currentThread],  [[NSThread currentThread] isMainThread]);
});

上面這個函式意思是在主佇列裡面非同步執行block裡面的任務;

dispatch_sync(queue, ^{
    NSLog(@"這是第一個任務,當前執行緒是:%@, 是否主執行緒 :%d ", [NSThread currentThread],  [[NSThread currentThread] isMainThread]);
});

上面這一行程式碼意思是在主線成中同步執行block塊裡面的任務,但是這樣做會讓主執行緒假死,無法執行任何操作,且不論你在任何佇列裡面同步執行一系列的任務,都會在主執行緒去執行。但是不會出現主執行緒假死,所以同步我們很少去用。

全域性佇列:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

全域性佇列是並行佇列,任務時並行的,充分利用了現在多核cpu的優勢,第一個引數為佇列的優先順序,第二引數為蘋果預留引數現在沒有實際作用我們一般填寫0。

dispatch_async(queue, ^{
    NSLog(@"這是第一個任務,當前執行緒是:%@, 是否主執行緒 :%d ", [NSThread currentThread],  [[NSThread currentThread] isMainThread]);
});

如果將一系列的任務加到全域性佇列裡面時,任務就併發執行,從而沒發預知那個任務先完成。

自定義佇列:

dispatch_queue_t queue = dispatch_queue_create("com.zouhao", DISPATCH_QUEUE_CONCURRENT);

自定義佇列可以是並行的也可以是序列的,第一個引數是佇列的名稱也可以稱之為標示吧,第二個引數是決定佇列是序列還是並行的DISPATCH_QUEUE_CONCURRENT代表並行DISPATCH_QUEUE_SERIAL代表序列。

dispatch_async(queue, ^{
    NSLog(@"這是第一個任務,當前執行緒是:%@, 是否主執行緒 :%d ", [NSThread currentThread],  [[NSThread currentThread] isMainThread]);
});

下面來看看GCD裡面的一些比較常規的函式:GCD延時執行,只執行一次,重複執行,分組任務,Barrier,函式指標。

GCD延時執行:

/第一個引數是從現在開始,第二個引數是時間,
dispatch_time_t delayInNanoSeconds =dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
//推遲兩納秒執行
dispatch_queue_t concurrentQueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//第一個引數只延遲時間,第二個引數是在那個佇列裡,第三個是任務,
dispatch_after(delayInNanoSeconds, concurrentQueue, ^(void){
    NSLog(@"Grand Center Dispatch!");
});

GCD重複執行:

//有時候專案需求我們重複執行一個方法多次,比如我們常見的專案了面的倒計時的計時器,很多都是用GCD實現的。
dispatch_queue_t queue = dispatch_queue_create("com.zouhao", DISPATCH_QUEUE_SERIAL);
dispatch_apply(3, queue, ^(size_t index) {
    NSLog(@"%zu, %@", index, [NSThread currentThread]);
});

這裡我們看看dispatch_apply函式的幾個引數分別是什麼意思,第一個引數是重複執行多少次,第二個引數是在那個佇列裡面執行任務,第三個是任務,block裡面的引數意思是第幾次執行。
GCD只執行一次:

//只執行一次,單例物件建立的時候經常需要用到它
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"只會執行一次");
});

這裡看看diapatch_once函式後面跟著兩個引數,第一個引數傳了一個地址,第二個引數是任務,當執行到block塊裡面時,就會向第一個引數指標指向的地址寫入資訊,當下一次在執行這個程式碼時發現記憶體已經被寫入過就不會再執行block了。

分組任務(dispatch_group):

在追加到Dispatch Queue中的多個任務處理完畢之後想執行結束處理,這種需求會經常出現。如果只是使用一個Serial Dispatch Queue(序列佇列)時,只要將想執行的處理全部追加到該序列佇列中並在最後追加結束處理即可,但是在使用Concurrent Queue 時,可能會同時使用多個Dispatch Queue時,原始碼就會變得很複雜。在這種情況下,就可以使用Dispatch Group。有時候我們開發的時候或許有需求需要將佇列裡面的一些任務加到一個分組裡面進行管理。當分組裡面的任務執行完之後我們可能需要做一些其他的邏輯。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
    NSLog(@"第一個任務 %d, %@", [NSThread isMainThread], [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
    NSLog(@"第二個任務 %d, %@", [NSThread isMainThread], [NSThread currentThread]);
});

這樣我們就講所有的佇列裡面的任務用分組進行管理了,這個的佇列可以使多個,當分組裡面的任務執行完成之後我們用到了一個新的函式。

//這個通知不能寫在還所有任務之上,必須保證先在佇列裡面加入任務之後他才能夠用,說簡單的就是如果佇列裡面沒有任務的時候他就會預設佇列任務被執行完然後走通知
dispatch_group_notify(group, queue, ^{
    NSLog(@"分組裡面的最後一個任務了!");
});

但是這個任務不能第一個加到分組裡,要不然這個時候分組為空,預設所有任務都執行完,直接對調通知的這個block。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("gcd-group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 1000; i++) {
        if (i == 999) {
            NSLog(@"11111111");
        }
    }
});
dispatch_group_async(group, queue, ^{
    NSLog(@"22222222");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"33333333");
});
dispatch_group_notify(group, queue, ^{
    NSLog(@"done");
});

控制檯的輸出:
這裡寫圖片描述

因為向Concurrent Dispatch Queue 追加處理,多個執行緒並行執行,所以追加處理的執行順序不定。執行順序會發生變化,但是此執行結果的done一定是最後輸出的。
無論向什麼樣的Dispatch Queue中追加處理,使用Dispatch Group都可以監視這些處理執行的結果。一旦檢測到所有處理執行結束,就可以將結束的處理追加到Dispatch Queue中,這就是使用Dispatch Group的原因。

GCD Barrier:
在訪問資料庫或者檔案的時候,我們可以使用Serial Dispatch Queue可避免資料競爭問題。併發中的資料競爭,通常的方法是加鎖和解鎖來實現同步機制。iOS提供了一種加鎖的方式,就是採用內建的synchronization block。這種寫法會根據給定的物件,自動建立一個鎖,並等待塊中的程式碼執行完畢。執行到這段程式碼結尾處,鎖也就釋放了。其實使用GCD可以簡單高效的代替同步塊或者鎖物件,可以使用,串行同步佇列,將讀操作以及寫操作都安排在同一個佇列裡,即可保證資料同步,程式碼如下:

#import <Foundation/Foundation.h>
@interface YXPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
#import "YXPerson.h"
@interface YXPerson ()
@end
static NSString *_name;
static dispatch_queue_t _queue;
@implementation YXPerson
- (instancetype)init
{
    if (self = [super init]) {
        _queue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)setName:(NSString *)name
{
    dispatch_sync(_queue, ^{
        _name = [name copy];
    });
}

- (NSString *)name
{
    __block NSString *tempName;
    dispatch_sync(_queue, ^{
        tempName = _name;
    });
    return tempName;
}
@end

這樣寫的思路是:把寫操作與讀操作都安排在同一個同步序列佇列裡面執行,這樣的話,所有針對屬性的訪問操作就都同步了。

這種方法還不是最優的,它只可以實現單讀、單寫。整體來看,我們最終要解決的問題是,在寫的過程中不能被讀,以免資料不對,但是讀與讀之間並沒有任何的衝突。多個getter方法(也就是讀取)是可以併發執行的,而getter(讀)與setter(寫)方法是不能併發執行的,利用這個特點,還能寫出更快的程式碼來,這次注意,不用序列佇列,而改用並行佇列:

#import <Foundation/Foundation.h>
@interface YXPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

#import "YXPerson.h"
@interface YXPerson ()
@end
static NSString *_name;
static dispatch_queue_t _concurrentQueue;
@implementation YXPerson
- (instancetype)init
{
    if (self = [super init]) {
        _concurrentQueue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}
- (void)setName:(NSString *)name
{
    dispatch_barrier_async(_concurrentQueue, ^{
        _name = [name copy];
    });
}
- (NSString *)name
{
    __block NSString *tempName;
    dispatch_sync(_concurrentQueue, ^{
        tempName = _name;
    });
    return tempName;
}
@end

在這個程式碼中的dispatch_barrier_async,可以翻譯成柵欄(barrier),它可以往佇列裡面傳送任務(塊,也就是block),這個任務有柵欄(barrier)的作用。
在佇列中,barrier塊必須單獨執行,不能與其他block並行。這隻對併發佇列有意義,併發佇列如果發現接下來要執行的block是個barrier block,那麼就一直要等到當前所有併發的block都執行完畢,才會單獨執行這個barrier block程式碼塊,等到這個barrier block執行完畢,再繼續正常處理其他併發block。在上面的程式碼中,setter方法中使用了barrier block以後,物件的讀取操作依然是可以併發執行的,但是寫入操作就必須單獨執行了。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    NSLog(@"這是第一個讀取資料的任務。。。執行緒是:%@, 是否主執行緒:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
    NSLog(@"這是第二個讀取資料的任務。。。執行緒是:%@, 是否主執行緒:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
    NSLog(@"這是第三個任務讀取資料的任務。。。執行緒是:%@, 是否主執行緒:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_barrier_async(queue, ^{
    NSLog(@"正在給資料庫裡面寫東西,不要打擾我");
});
dispatch_async(queue, ^{
    NSLog(@"這是第四個讀取資料的任務。。。執行緒是:%@, 是否主執行緒:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
    NSLog(@"這是第五個讀取資料的任務。。。執行緒是:%@, 是否主執行緒:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
    NSLog(@"這是第六個任務讀取資料的任務。。。執行緒是:%@, 是否主執行緒:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});

很明顯的可以看出來雖然是非同步執行,但是在dispatch_barrierh函式上面的方法執行之後dispatch_barrierh下面的的任務將處於等候狀態,直到dispatch_barrierh函式裡面的任務完成之後再去執行。

GCD函式指標:
我們在呼叫GCD函式的時候發現很多方法很相似,只是函式名多了_f,這就是我們的函式指標。為什麼我們明明有了直接的函式呼叫還要出現函式指標實現任務體呢?很明顯block是OC裡面的語法塊,但是函式確是C語言裡面的語法,這樣來看函式指標這種模式必然比較低層,那麼效率必然會比block這種模式要高,但是GCD本生封裝已經很完美,如果想單單通過講block換成函式指標提高很多的效率是辦不到的,只是在一些要求精細的專案裡面會起到細微的提高效率的作用。下面這些就是GCD函式指標:

 dispatch_async_f(dispatch_get_main_queue(), "haha", func);
    dispatch_sync_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
    dispatch_after_f(<#dispatch_time_t when#>, <#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
    dispatch_apply_f(<#size_t iterations#>, <#dispatch_queue_t queue#>, <#void *context#>, <#void (*work)(void *, size_t)#>)
    dispatch_barrier_async_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
    dispatch_barrier_sync_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
    dispatch_group_async_f(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
    dispatch_group_notify_f(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
    dispatch_once_f(<#dispatch_once_t *predicate#>, <#void *context#>, <#dispatch_function_t function#>)
    dispatch_set_finalizer_f(<#dispatch_object_t object#>, <#dispatch_function_t finalizer#>)

4,Pthreads

其實這個框架專職做iOS的人很人知道也很少人用到,這裡也只是提一下拿來充個數,為了讓大家瞭解一下就好了。這是一套在很多作業系統上都通用的多執行緒API,當然在 iOS 中也是可以的。不過這是基於C語言的框架,使用起來你懂得,感興趣的可以瞭解一下。