iOS多執行緒的使用

weixin_33807284發表於2017-09-13

iOS中,只有主執行緒跟Cocoa關聯,也即是在這個執行緒中,更新UI總是有效的,如果在其他執行緒中更新UI有時候會成功,但可能失敗。所以蘋果要求開發者在主執行緒中更新UI。但是如果我們吧所有的操作都放置在主執行緒中執行,當遇到比較耗時的操作的時候,勢必會阻塞執行緒,出現介面卡頓的情況。這時候採取將耗時的操作放入後臺執行緒中操作,且保持主執行緒只更新UI是我們推薦的做法。

在iOS中,要實現多執行緒,一共有四種方式。  它們分別是:

pthreadsPOSIX執行緒(POSIX threads),簡稱Pthreads,是執行緒的POSIX標準。該標準定義了建立和操縱執行緒的一整套API。在類Unix作業系統(Unix、Linux、Mac OS X等)中,都使用Pthreads作為作業系統的執行緒。Windows作業系統也有其移植版pthreads-win32[1]這篇文章不介紹

NSThread需要管理執行緒的生命週期、同步、加鎖問題,這會導致一定的效能開銷

NSOperation & NSOperationQueue

GCDiOS4開始,蘋果釋出了GCD,可以自動對執行緒進行管理。極大的方便了多執行緒的開發使用

一、pthread

pthread是一套基於C的API,它不接受cocoa框架的控制:當手動建立pthread的時候,cocoa框架並不知道。 蘋果不建議在cocoa中使用pthread,但是如果為了方便不得不使用,我們應該小心的使用。

下面這些方法可以建立pthread

OC

pthread_attr_t qosAttribute;

pthread_attr_init(&qosAttribute);

pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0);

pthread_create(&thread, &qosAttribute, f,NULL);

SWIFT

varthread= pthread_t()

var qosAttribute = pthread_attr_t()

pthread_attr_init(&qosAttribute)

pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0)

pthread_create(&thread, &qosAttribute, f,nil)

並且,可以使用下面的API對一個pthread進行修改。

蘋果的文件中,有一篇文件講述了GCD中使用pthread的禁忌:Compatibility with POSIX Threads。

6OBJECTIVE-C

pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND,0);

SWIFT

pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0)

二、NSThread

對於NSThread,在使用的過程中,我們需要手動完成很多動作才能確保執行緒的順利執行。但與此同時,它給我們帶來更大的定製化空間。

1.建立NSThread。

對於NSThread的建立,蘋果給出了三種使用方式。

detachNewThreadSelector(_:toTarget:with:)   detachNewThreadSelector  會建立一個新的執行緒,並直接進入執行緒執行。

initWith(Target:selector:object:)          iOS10.0之前的建立方式,需要手動執行。

initWithBlock                                      iOS10.0之後,可以建立一個執行block的執行緒。

2.NSThread執行緒通訊。

如果我們想對已經存在的執行緒進行操作,可以使用

performSelector:onThread:withObject:waitUntilDone:

跳轉到目標執行緒執行,實現執行緒間跳轉,達到執行緒通訊的目的。但是需要注意的是,這個方法不適合頻繁的進行通訊,尤其是對於一些敏感的操作。

3.NSThread執行緒的狀態。

在一個執行緒中,可以通過相關的函式獲取到它的當前狀態。

+ isMainThread:判斷當前執行緒是不是主執行緒。

+ mainThread:獲取當前的主執行緒。

+ isMultiThreaded :判斷當前環境是不是多執行緒環境

+ threadDictionary :獲取包含專案中的執行緒的字典

@property(readonly, getter=isExecuting)BOOL executingNS_AVAILABLE(10_5, 2_0);     是否處於執行狀態

@property(readonly, getter=isFinished)BOOL finishedNS_AVAILABLE(10_5, 2_0);          是否處於完成狀態

@property(readonly, getter=isCancelled)BOOL cancelledNS_AVAILABLE(10_5, 2_0);      是否處於取消狀態

4.NSThread執行緒的優先順序。

可通過給NSThread設定優先順序。以便讓開發者更靈活的控制程式的執行。

+ threadPriority  Returns the currentthread’s priority. 返回當前執行緒的優先順序別

threadPriority        The receiver’s priority  訊息傳送者的優先順序,這個傳送者是一個NSThread物件

+ setThreadPriority:     Sets the currentthread’s priority. 設定執行緒的優先順序

5.停止執行緒/終止執行緒

+ sleepUntilDate:   Blocks the currentthreaduntil the time specified.  直到某時刻執行

+ sleepForTimeInterval:     Sleeps thethreadfora given time interval.   暫停執行緒

+ exit    Terminates the currentthread.  關閉執行緒,這裡呼叫之前,為了確保程式的安全,我們應在明確執行緒的狀態是isFinished 和 isCancelled的時候執行。

- cancel    Changes the cancelled state of the receiver to indicate that it should exit.  主動進入取消狀態,如果當時執行緒沒有完成,會繼續執行完成。

6.使用NSThread

- (void)viewDidLoad {

[superviewDidLoad];

_testCount = 100;

_t1 =  [[NSThread alloc] initWithTarget:selfselector:@selector(test) object:nil];

_t1.name = @"執行緒一";

[_t1 start];

NSInvocationOperation

}

-(void)test{

for(inti = 0; i < 5 ; i++) {

[NSThreadsleepForTimeInterval:0.05];

NSLog(@"%ld,%@",(long)_testCount--,[[NSThreadcurrentThread] name]);

}

}

三、NSOperation & NSOperationQueue

1.NSOperation

NSOperation是對於執行緒物件的抽象封裝,不會被直接使用,在日常的開發中,會使用它的兩個子類:NSInvocationOperationNSBlockOperation。NSInvocationOperation類是NSOperation的具體子類,用於管理指定為呼叫的單個封裝任務的執行。 您可以使用此類來啟動包含在指定物件上呼叫選擇器的操作。 此類實現非併發操作。NSBlockOperation類也是NSOperation的具體子類,用於管理一個或多個block塊的併發執行。 您可以使用此物件一次執行多個block,而無需為每個塊建立單獨的操作物件。 當執行多個程式段時,只有當所有程式段執行完畢時,才會將操作本身完成。

NSInvocationOperation實現非併發操作。

3_invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];

_invCationOp.name = @"invocation執行緒";

[_invCationOp start];

列印:

{number = 1, name = main},

{number = 1, name = main}

從列印結果可以看出,NSInvocationOperation實現的是非並法的操作,至於在哪個執行緒中操作,取決於start的當前呼叫時的執行緒。

如果我們需要建立一個併發的Queue,可以使用NSBlockOperation。如果我們像這樣建立:

- (void)blockOperation{

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp addExecutionBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp addExecutionBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp start];

}

列印:

2017-05-23 15:11:00.289多執行緒[5377:589808] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:11:00.289多執行緒[5377:589808] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:11:00.289多執行緒[5377:589808] {number = 1, name = main},{number = 1, name =  main}

顯然,這裡都在主執行緒中執行,不能證明NSBlockOperation具有併發的能力,這是因為,每個NSBlockOperation物件的會優先在主執行緒中執行。如果主執行緒受到阻塞的時候才會開闢另一個執行緒去執行其他的操作。比如向下面這樣:

//NSBlockOperation

 - (void)blockOperation{

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

     }];

     [blockOp addExecutionBlock:^{

         [NSThreadsleepForTimeInterval:2.0];

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

     }];

     [blockOp addExecutionBlock:^{

         [NSThreadsleepForTimeInterval:2.0];

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

     }];

     [blockOp start];

 }

列印:

2017-05-23 15:28:37.780多執行緒[5645:617976] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:28:39.848多執行緒[5645:618027] {number = 3, name = (null)},{number = 1, name = (null)}

2017-05-23 15:28:39.848多執行緒[5645:618028] {number = 4, name = (null)},{number = 1, name = (null)}=

這裡就是非同步執行了。NSBlockOperation在使用的過程中,會針對主執行緒當前的使用情況,選擇性的建立其他的執行緒。在提升流暢度的同時,還節約了資源。

2.NSOperationQueue

NSOperationQueue:手動管理非同步執行。  如果我們想使用併發,並且要作到精確掌握併發的執行緒。可以使用NSOperationQueue。這是一個操作佇列,如果將NSOperation的具體子類物件新增進來的時候,開啟之後,所有的物件沒有先後,會非同步執行各自的程式碼。

- (void)operationQueue{

NSOperationQueue*queue = [[NSOperationQueuealloc] init];

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%ld,%@,%@",(long)_testCount--,[NSThreadcurrentThread],[NSThreadmainThread]);

}];

NSInvocationOperation*invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];

invCationOp.name = @"invocation執行緒";

[queue addOperation:invCationOp];

[queue addOperation:blockOp];

}

列印:

2017-05-23 15:38:45.110多執行緒[5772:633972] 100,{number = 3, name = (null)},{number = 1, name = (null)}

2017-05-23 15:38:45.203多執行緒[5772:633975] 99,{number = 4, name = (null)},{number = 1, name = (null)}

在NSOperationQueue中,正常情況下,所有的operation的執行次序是隨機,如果我們想要某個operation被率先執行,可以將這個operation的優先順序調高。對於優先順序有以下的選擇:

[invCationOp setQueuePriority:NSOperationQueuePriorityVeryHigh];

對於優先順序有以下的選擇:

typedefNS_ENUM(NSInteger,NSOperationQueuePriority) {

NSOperationQueuePriorityVeryLow= -8L,   最低

NSOperationQueuePriorityLow= -4L,       次低

NSOperationQueuePriorityNormal= 0,      普通  不做任何操作的operation的優先順序是這個

NSOperationQueuePriorityHigh= 4,        次高

NSOperationQueuePriorityVeryHigh= 8     最高

};

當然,如果有很多operation,使用的優先順序不能滿足的時候,還可以設定 operation的依賴關係。 設定依賴之後,將會先執行依賴物件。

[invCationOp addDependency:blockOp];

值得注意的是:優先順序和依賴關係不是衝突的。 優先順序的選擇會在依賴關係下發生效果,也就是,在依賴關係成立的情況下,優先順序的才會有效。

三、GCD  -  Grand Central Dispatch

Grand Central Dispatch早在Mac OS X 10.6雪豹上就已經存在了。後在iOS4.0的時候被引入。Grand Central Dispatch是OS X中的一個低階框架,用於管理整個作業系統中的併發和非同步執行任務。本質上,隨著處理器核心的可用性,任務排隊等待執行。通過允許系統控制對任務的執行緒分配,GCD更有效地使用資源,這有助於系統和應用程式執行更快,高效和響應。

GCD的一個重要的物件是佇列:Dispatch Queue。跟Operationqueue類似,通過將Operation加入到佇列中,執行相應的單元。在GCD中,大量採用了block的形式建立類似的operation。

1. Dispatch Queue  建立

Dispatch Queue 分為兩類,主要是根據並行和序列來區分:

a. Serial Dispatch Queue: 線性執行的執行緒佇列,遵循FIFO(First In First Out)原則; 又叫private dispatch queues,同時只執行一個任務。Serial queue常用於同步訪問特定的資源或資料。當你建立多個Serial queue時,雖然各自是同步,但serial queue之間是併發執行。 main dipatch屬於這個類別。

b. Concurrent Dispatch Queue: 併發執行的執行緒佇列,併發執行的處理數取決於當前狀態。又叫global dispatch queue,可以併發的執行多個任務,但執行完成順序是隨機的。系統提供四個全域性併發佇列,這四個佇列有這對應的優先順序,使用者是不能夠建立全域性佇列的,只能獲取。

我們可以自定義佇列,預設建立的佇列是序列的,但是也可以指定建立一個並行的佇列:

//序列佇列dispatch_queue_create("com.deafultqueue", 0)

//序列佇列dispatch_queue_create("com.serialqueue", DISPATCH_QUEUE_SERIAL)//並行佇列dispatch_queue_create("com.concurrentqueue", DISPATCH_QUEUE_CONCURRENT)

除了自定義佇列,系統其實也為有一些已經公開的佇列。這些佇列不需要我們顯示的建立,只能通過獲取的方式得到:

dispatch_get_main_queue()   獲取當前的APP主佇列,這個佇列在主執行緒中,通常我們呼叫它進行介面的重新整理。

dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)   獲取全域性的Concurrent佇列,這裡蘋果提供了四種不同的優先順序,

#define DISPATCH_QUEUE_PRIORITY_HIGH 2

#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0

#define DISPATCH_QUEUE_PRIORITY_LOW (-2)

#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

也即時有四個不同的並行佇列。

2. Dispatch Queue  執行

GCD的佇列有序列和並行兩種佇列,同時我們可以同步和非同步兩種方式執行佇列,所以最多有四種不同的場景。

(1)序列同步。 凡涉及到同步的的都會阻塞執行緒。 UI執行緒—也即是我們的所說的主執行緒預設情況下其實就是執行同步的。這個時候如果有一些耗時間的操作,則會出現卡頓的現象。這種方式大部分情況用於能快速響應和後臺執行緒的耗時場景中。

//序列同步

dispatch_queue_t  serialQ = dispatch_queue_create("序列", DISPATCH_QUEUE_SERIAL);//建立一個序列佇列

NSLog(@"%@",[NSThreadcurrentThread].description);

dispatch_sync(serialQ, ^{

[NSThreadsleepForTimeInterval:3];

NSLog(@"%@ --  %@佇列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_sync(serialQ, ^{

NSLog(@"%@ --  %@佇列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

(2)序列非同步。 這種情況下,GCD會開闢另一個新的執行緒,讓佇列中的內容在新的執行緒中按順序執行。

//序列非同步

dispatch_async(serialQ, ^{

NSLog(@"%@ 1-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 2-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 3-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 4-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

(3)並行同步。 因為是同步執行,所以實際上這裡的並行是沒有意義的。 依然在當前的執行緒中按順序執行,並阻塞。

dispatch_queue_t  conCurrentQ = dispatch_queue_create("並行", DISPATCH_QUEUE_CONCURRENT);//建立一個並行行佇列

//並行同步

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 1-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 2-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 3-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

(4)並行非同步。 並行非同步將極大的利用資源。首先會開闢新的執行緒,並且,當所有執行緒備佔用的情況下,會繼續開闢(如果沒有限制的話)。所以這裡還涉及執行緒的最大值的問題。

//並行非同步

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 1-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 2-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 3-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 4-- 佇列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

3. Dispatch Queue  暫停和繼續

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

4. Dispatch Queue  的銷燬

每個佇列在執行完新增到其中的所有的block事件的時候,在ARC模式下,會被自動銷燬。 但是在手動管理記憶體的時候,我們需要呼叫

dispatch_release(queue);

來釋放。

5.佇列組  Dispatch Group (這些內容來自http://blog.csdn.net/q199109106q/article/details/8566300

多數情況下,我們可能會遇到這種問題: 對一個頁面中的多張圖片,每張圖片要單獨的進行網路請求,我們沒有辦法保證每次的請求時間是一樣的,但是專案經理說必須要在獲取所有的圖片的情況下,才可以進行對頁面的重新整理。這裡有個很好例子可以解決這個問題。

// 根據url獲取UIImage

- (UIImage *)imageWithURLString:(NSString*)urlString {

NSURL*url = [NSURLURLWithString:urlString];

NSData*data = [NSDatadataWithContentsOfURL: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 = [selfimageWithURLString:url1];

// 下載第二張圖片

NSString*url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";

UIImage *image2 = [selfimageWithURLString:url2];

// 回到主執行緒顯示圖片

dispatch_async(dispatch_get_main_queue(), ^{

self.imageView1.image = image1;

self.imageView2.image = image2;

});

});

}

但是我們發現,事實上,圖片一和 二兩者在請求的過程中是完全獨立的, 但是這裡明顯的,圖片一的下載將阻塞,直到下載完才會開始圖片二的下載。這種方式畢竟還是有瑕疵的啊哈。

Dispatch Group可以幫助解決這個問題。 它是Dispatch Queue的組合,被加入到group的queue會在組內其他的queue也執行完操作的時候,有group統一呼叫預設好的一個block。最重要的是,在group中的內容是可以非同步執行的。也即是多個佇列在不同的執行緒執行。 如果圖片大小差不多的話,這種方式將節省我們不一半的時間。  我們來看看這個模型。

//dispaach group

dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue1, ^{

[NSThreadsleepForTimeInterval:5.0];

NSLog(@"第一個專案執行完成。");

});

dispatch_group_async(group, queue2, ^{

[NSThreadsleepForTimeInterval:10.0];

NSLog(@"第二個專案執行完成。");

});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

NSLog(@"集體回撥");

});

6.GCD的其他的用法

(1)控制一段程式碼只執行一次。用在建立單例的時候再好不過了。

//控制程式碼只執行一次數

for(inti = 1 ;i <= 10 ;i++){

staticdispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

NSLog(@"被執行 %d次",i);

});

}

列印結果:2017-05-24 18:15:17.704多執行緒[10542:898532]被執行1

(2) 只能控制執行一次是不是有點不夠完美 。dispatch_apply 可以讓你控制一段程式碼執行任意多次。這裡的執行是非同步執行的,如果為了確保順序執行,應該對執行的內容進行加鎖。

//控制執行任意多次

dispatch_queue_t queueX = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

__blockintcount = 0;

NSLock*lock = [[NSLockalloc]init];

dispatch_apply(5, queueX, ^(size_t index) {

[lock lock];

NSLog(@"%d,%zu",count++,index);

[lock unlock];

});

(3)做一個block式的延時。 除了使用performSelector之外,我們還以使用dispatch_after進行延時,並且是以block的形式進行。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 *NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

NSLog(@"五秒鐘之後執行的程式碼。");

});

相關文章