iOS
實現提供實現多線程的方案有:NSThread
、NSOperation
、GCD
。
在iOS
所有實現多線程的方案中,GCD
應該是最有魅力的,而且使用起來也是最方便的,因爲GCD
是蘋果公司爲多核的並行運算提出的解決方案。
GCD
是Grand
Central Dispatch
的簡稱,它是基於C
語言的。使用GCD
,我們不需要編寫線程代碼,其生命週期也不需要我們手動管理,定義想要執行的任務,然後添加到適當的調度隊列,也就是dispatch
queue
。GCD
會負責創建線程和調度任務,系統直接提供線程管理。
由於GCD
是基於C
語言的,因此使用起來對於沒有學習過C
語言的同學們,相對困難一些。不過,事實上使用是很簡單,只要注意死鎖等問題就好了。
概念:隊列(Queue)
我們需要了解隊列的概念,GCD
提供了dispatch
queues
來處理代碼塊,這些隊列管理所提供給GCD
的任務並用FIFO
順序執行這些任務。這樣才能保證第一個被添加到隊列裏的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列的終點。
概念:調度隊列(dispath queue)
所有的調度隊列(dispatch queues
)自身都是線程安全的,我們能從多個線程並行的訪問它們。 GCD
的優點是顯而易見的。我們需要了解調度隊列如何我們的代碼的不同部分提供線程安全,以決定使用何種隊列,在哪個線程上執行等。
GCD
將長期運行的任務拆分成多個工作單元,並將這些單元添加到dispath
queue
中,系統會管理這些dispath queue
,爲我們在多個線程上執行工作單元,我們不需要手動啓動和管理後臺線程。
系統提供了許多預定義的dispath queue
,包括始終在主線程上執行工作的dispath
queue
。我們可以創建自己的dispath queue
,而且可以創建任意多個。GCD
的dispath
queue
嚴格遵循FIFO
(先進先出)原則,添加到dispath
queue
的工作任務將按照加入dispath queue
的順序啓動。
概念:串行(Serial)
我們在學習操作系統這門課程的時候,經常會提到串行。我們使用GCD
,也會用到串行的概念。
所謂串行(Serial)執行,指同一時間每次只能執行一個任務。
概念:併發(Concurrent)
說到串行,自然會想到併發。在操作系統這門課程中,這個概念是非常重要的。
所謂併發(Concurrent),指同一時間可以同時執行多個任務。
概念:死鎖(Deadlock)
操作系統這門課程中對死鎖的介紹說明有很多。在實際開發中,也經常遇到死鎖的問題。
所謂死鎖(Deadlock)是指它們都卡住了,並等待對方完成或執行其它操作。第一個不能完成是因爲它在等待第二個的完成。但第二個也不能完成,因爲它在等待第一個的完成。
概念:線程安全(Thread Safe)
還記得我們在寫單例的時候都加了哪些代碼嗎?我們應該知道,既然要聲明爲單例,說明這是共享資源區,就會存在競態條件,因此,我們必須保證只創建一次。
像這樣添加了線程鎖的:
1
2
3
4
5
|
@synchronized(<#token#>)
{
<#statements#>
}
|
還有這樣用於創建單例的,以確保只執行一次:
1
2
3
4
5
6
|
static
dispatch_once_t
onceToken;
dispatch_once(&onceToken,
^{
<#code
to be executed once#>
});
|
創建和管理dispatch queue
1.獲取全局併發調度隊列
併發的調度隊列可以同時並行地執行多個任務,但是併發隊列也是隊列,因此同樣遵循着FIFO
的原則來啓動任務。因爲併發執行任務與系統有關,其同時執行任務的數量是由系統根據應用和系統動態變化決定的。
現在iOS
系統,爲每個應用提供了四種併發的全局共享的調度隊列,其區別在於優先級不一樣。
1
2
3
4
5
6
7
8
9
10
11
|
/*
*
The global concurrent queues may still be identified by their priority,
*
which map to the following QOS classes:
*
* -
DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
* -
DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
* -
DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
* -
DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND
*/
|
我們不需要創建它,只需要直接獲取就可以了,因爲這是系統爲我們提供的,而且這個還是全局共享的:
1
2
3
|
dispatch_queue_t
globalQueue
=
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0);
|
第一個參數爲優先級,就是上面提供的這四種。第二個參數沒有使用到,這個參數是預留的,使用0即可,看官方說明:
1
2
3
4
5
|
*
@param
flags
*
Reserved
for
future
use.
Passing
any
value
other
than
zero
may
result
in
*
a
NULL
return
value.
|
flags
就是第二個參數,也就是爲未來預留的參數。看看蘋果想得真夠遠的,爲未來預留~~。
注意:雖然
dispatch queue
是引用計數的對象,但我們不需要retain
和release
這個全局的併發queue
。因爲這些queue
對應用是全局的,retain
和release
調用會被忽略。
2.創建串行調度隊列
當任務需要按特定的順序執行時,就需要使用串行調度隊列(Dispatch Queue),串行調度隊列每次只能執行一個任務。
我們可以使用串行隊列替代鎖,保護共享資源等。和鎖不一樣的是,串行隊列確保任務按指定的順序執行,而且只要你異步地提交任務到串行隊列,就永遠不會產生死鎖。
我們可以手動創建和管理串行隊列,且可以創建很多個,但是我們不要創建很多個串行隊列來執行很多的任務,當需要執行大量的任務時,應該交給全局併發隊列來完成。從操作系統方面思考,雖然允許應用創建很多個串行隊列,但是其優先級永遠不會比系統級的高,因此當任務很多時,所要求的資源未必就可以提供。所以,任務量大時,應該交給系統提供的全局隊列來完成纔是最佳的。
使用下面的方法來創建串行隊列,其中第一個參數是隊列的名稱,通常使用公司的反域名,如com.company.project。第二個參數是隊列相關屬性,通常都傳NULL
:
1
2
3
|
dispatch_queue_t
sequalQueue
=
dispatch_queue_create("com.huangyibiao.helloworld",
NULL);
|
3.獲取公共隊列
應用提供了幾下幾種獲取公共隊列的方法:
-
dispatch_get_current_queue
:在iOS 6.0
之後已經廢棄,用於獲取當前正在執行任務的隊列,主要用於調試 -
dispatch_get_main_queue
: 最常用的,用於獲取應用主線程關聯的串行調度隊列 -
dispatch_get_global_queue
:最常用的,用於獲取應用全局共享的併發隊列
對於後面這兩個分別獲取主線程的串行隊列和獲取應用全局共享的併發隊列是非常常用的,當我們需要開一個線程併發地異步執行任務時,我們就會放到全局隊列中。當我們在異步執行完成時,通常需要回到主線程更新UI
顯示。
4.調度隊列(Dispatch Queue)的內存管理
調度隊列,即Dispatch Queue
與其它類型的dispatch
對象是引用計數的數據類型。當創建一個串行dispatch
queue
時,初始引用計數爲1
,我們可用dispatch_retain
和dispatch_release
函數來增加和減少引用計數。當引用計數爲0
時,系統會異步地銷燬這個queue
。
以上是對於普通創建的調度隊列有用,但對於系統本身提供的全局併發隊列和主線程串行隊列則不需要我們手動內管其內存,系統會自動管理。
在使用全局併發隊列時,我們只通過dispatch_get_global_queue
方法來獲取即可,我們不需要管理其引用。
在使用主線程串行隊列時,我們只通過dispatch_get_main_queue
方法來獲取即可,我們也不需要管理其內存問題。
添加任務到調度隊列
要想讓調度隊列執行任務,那麼我們就需要將任務添加到適當的調度隊列中。在實際iOS
開發中,我們通常配合block
的使用,將任務封裝到一個block
中。
我們可以異步或者同步添加任務到隊列中,但是我們應該儘可能地使用dispatch_async
或dispatch_async_f
。前者是提交一個block
任務到隊列中,後者是提供一個函數任務到隊列中。基本上都是直接使用dispatch_async
提交一個block
到隊列中,這代碼寫起來更加地簡潔。
當然,我們也可以同步添加任務。有時候我們可能希望同步地調度任務,以避免競爭條件或其它同步錯誤。使用dispatch_sync
或dispatch_sync_f
函數同步地添加任務到Queue,這兩個函數會阻塞當前調用線程,直到相應任務完成執行。在實際開發中,當需要同步執行任務時,大多是直接使用dispatch_sync
這個提交block
任務的方法,使用起來更簡潔。
注意:當隊列中有任務正在同步執行時,我們不能使用
dispatch_sync
或dispatch_sync_f
同步調度新任務到當前正在執行的queue
中。對於串行queue
肯定會導致死鎖,而對於併發queue
也應該避免這麼使用。原來我接手的項目中,有一個同步任務正在執行數據庫操作,可是當我也需要操作數據時,調用其所提供的api
,使用dispatch_sync
將我的任務添加到隊列中,結果導致了死鎖,每次都crash
。
爲什麼儘可能地添加異步執行的任務呢?因此同步任務會阻塞主線程,很可能導致事件響應不了。
我們看看如何簡單地創建隊列、異步、同步任務添加到隊列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
dispatch_queue_t
queue
=
dispatch_queue_create("com.huangyibiao.helloworld",
NULL);
dispatch_async(queue,
^{
NSLog(@"開啓了一個異步任務,當前線程:%@",
[NSThread
currentThread]);
});
dispatch_sync(queue,
^{
NSLog(@"開啓了一個同步任務,當前線程:%@",
[NSThread
currentThread]);
});
//
MRC下才能調用,對於ARC就不能添加這行代碼了。
dispatch_release(queue);
|
由於這個串行調度隊列是我們自己創建的,我們需要管理其內存。不過在實際開發中,使用自己創建創建的方式是比較少見的,通常都是直接使用系統爲每個應用提供的全局共享併發隊列異步執行任務,然後使用主線程串行隊列更新界面。
控制併發數
太多併發是會帶來很多的風險的。在實際開發中,並不是併發數越多就越好,往往是需要控制其併發數量的。比如,在處理網絡請求併發數時,通常會設置限制最大併發數爲4左右。當併發數量大了,開銷也會很大。學過操作系統應該清楚,併發量大了,臨界資源訪問操作就很難控制,控制不好就會導致死鎖等。當我們需要執行循環異步處理任務時,可以考慮使用dispatch_apply
來替代。請看下一節!
併發地循環迭代任務
如果迭代執行的任務與其它迭代任務是獨立無關的,而且循環迭代執行順序也無關緊要的話,我們可以調用dispatch_apply
或dispatch_apply_f
函數來替換循環。前者是提交block
任務,後者是提交函數任務到隊列中。比如,我們需要上傳多張圖片,這些圖片的上傳是互不干擾的,迭代執行的順序是不重要的,那麼我們就可以使用dispatch_apply
來替換掉for
循環。
下面代碼使用dispatch_apply
替換了for
循環,所傳遞的block
必須包含一個size_t
類型的參數,用來標識當前循環迭代。第一次迭代這個參數值爲0
,最後一次值爲count
- 1
:
1
2
3
4
5
6
7
8
9
10
11
12
|
//
獲得全局併發queue
dispatch_queue_t
gqueue
=
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0);
size_t
gcount
=
10;
dispatch_apply(gcount,
gqueue,
^(size_t
i)
{
[self
uploadImageWithIndex:(NSUInteger)(i)];
});
-
(void)uploadImageWithIndex:(NSUInteger)imageIndex
{
NSLog(@"上傳索引爲%lu的圖片",
imageIndex);
}
|
打印結果說明順序是不確定的,可看得出來這是併發執行的:
1
2
3
4
5
6
7
8
9
10
11
12
|
2015-11-24
00:06:11.692
TestGCD[27714:2678984]
上傳索引爲0的圖片
2015-11-24
00:06:11.692
TestGCD[27714:2679067]
上傳索引爲3的圖片
2015-11-24
00:06:11.692
TestGCD[27714:2678984]
上傳索引爲4的圖片
2015-11-24
00:06:11.692
TestGCD[27714:2679064]
上傳索引爲2的圖片
2015-11-24
00:06:11.692
TestGCD[27714:2678984]
上傳索引爲6的圖片
2015-11-24
00:06:11.692
TestGCD[27714:2679065]
上傳索引爲1的圖片
2015-11-24
00:06:11.693
TestGCD[27714:2678984]
上傳索引爲8的圖片
2015-11-24
00:06:11.692
TestGCD[27714:2679067]
上傳索引爲5的圖片
2015-11-24
00:06:11.693
TestGCD[27714:2679064]
上傳索引爲7的圖片
2015-11-24
00:06:11.693
TestGCD[27714:2679065]
上傳索引爲9的圖片
|
注意:
dispatch_apply
或dispatch_apply_f
函數也是在所有迭代完成之後纔會返回,因此這兩個函數會阻塞當前線程。當我們在主線程中使用時,一定要小心,很容易造成事件無法響應,所以如果循環代碼需要一定的時間執行,可考慮在另一個線程中調用這兩個函數。如果所傳遞的參數是串行queue
,而且正是執行當前代碼的queue
,就會產生死鎖。
主線程中執行任務
看看下面很常用的異步下載圖片的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
|
//
異步下載圖片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{
NSURL
*url
=
[NSURL
URLWithString:@"圖片的URL"];
UIImage
*image
=
[UIImage
imageWithData:[NSData
dataWithContentsOfURL:url]];
//
回到主線程顯示圖片
dispatch_async(dispatch_get_main_queue(),
^{
self.imageView.image
=
image;
});
});
|
這裏先將異步下載圖片的任務放到dispatch_get_global_queue
全局共享併發隊列中執行,在完成以後,需要放在dispatch_get_main_queue
回到主線程更新UI
。
暫停和繼續queue
我們可以使用·dispatch_suspend·函數暫停一個queue
以阻止它執行block
對象;使用dispatch_resume
函數繼續dispatch
queue
。調用dispatch_suspend
會增加queue
的引用計數,調用dispatch_resume
則減少queue
的引用計數。當引用計數大於0
時,queue
就保持掛起狀態。因此你必須對應地調用dispatch_suspend
和dispatch_resume
函數。掛起和繼續是異步的,而且只在執行block
之間生效,掛起一個queue
不會導致正在執行的block
停止。
1
2
3
4
|
dispatch_suspend(gqueue);
dispatch_resume(gqueue);
|
注意:
dispatch_suspend
和dispatch_resume
是成對出現的。
調度組(Dispatch Group)的使用
當我們需要下載多張圖片並且圖片要求這幾張圖片都下載完成以後才能更新UI,那麼這種情況下,我們就需要使用dispatch_group_t
來完成了。
像這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//
異步下載圖片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{
//
下載第一張圖片
UIImage
*image1
=
[self
imageWithURLString:url1];
//
下載第二張圖片
UIImage
*image2
=
[self
imageWithURLString:url2];
//
回到主線程顯示圖片
dispatch_async(dispatch_get_main_queue(),
^{
self.imageView1.image
=
image1;
self.imageView2.image
=
image2;
});
});
|
這段代碼是不能做到的,但是,我們還是有辦法做到的。dispatch_group_t
就是很好的選擇。對於調度組,所添加的任務可以是同步的,也可以是異步的,在最近任務全部完成後都會有回調。
首先,我們通過dispatch_group_create
創建一個組,然後通過dispatch_group_async
將任務分別添加到該組中。當組中的所有任務都完成以後,我們可以通過dispatch_group_notify
得到回調,然後在主線程更新UI。
代碼寫法像下面這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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;
//
分別將任務添加到組中
dispatch_group_async(group,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{
image1
=
[self
downloadImage:url1];
});
dispatch_group_async(group,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{
image2
=
[self
downloadImage:url2];
});
//
等待組中的任務執行完畢,回到主線程執行block回調
dispatch_group_notify(group,
dispatch_get_main_queue(),
^{
self.imageView1.image
=
image1;
self.imageView2.image
=
image2;
});
});
|
延遲執行
我們常見的延遲執行方法有:
方法一:使用NSObject
的api
,同步執行:
1
2
3
|
[self
performSelector:@selector(myFunction)
withObject:nil
afterDelay:5.0];
|
方法二:使用NSTimer
定時器,不過這種方法沒必要。
方法三:使用dispatch_after
方法異步延遲執行:
1
2
3
4
5
6
7
8
|
CGFloat
time
=
5.0f;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(time
*
NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
//
time秒後異步執行這裏的代碼...
});
|
結尾
對於在實際開發中常用的差不多全了,其它比較偏的API
就不說了,在開發中比較少用。