基本概念
線程
線程是進(jìn)程的基本執(zhí)行單元,一個進(jìn)程的所有任務(wù)都在線程中執(zhí)行 進(jìn)程要想執(zhí)行任務(wù),必須得有線程,進(jìn)程至少要有一條線程。
程序啟動會默認(rèn)開啟一條線程,這條線程被稱為主線程或UI線程。
進(jìn)程
進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個應(yīng)用程序。每個進(jìn)程之間是獨(dú)立的,每個進(jìn)程均運(yùn)行在其專用的且受保護(hù)的內(nèi)存。
線程與進(jìn)程的關(guān)系
- 地址空間: 同一進(jìn)程的線程共享本進(jìn)程的地址空間,而進(jìn)程之間則是獨(dú)立的地址空間。
- 資源擁有: 同一進(jìn)程內(nèi)的線程共享本進(jìn)程的資源如內(nèi)存、
I/O、cpu等,但是進(jìn)程之間的資源是獨(dú)立的。 - 一個進(jìn)程崩潰后,在保護(hù)模式下不會對其他進(jìn)程產(chǎn)生影響,但是一個線程崩潰整個進(jìn)程 都死掉。所以多進(jìn)程要比多線程健壯。
- 進(jìn)程切換時,消耗的資源大,效率高。所以涉及到頻繁的切換時,使用線程要好于進(jìn)程。同樣如果要求同時進(jìn)行并且又要共享某些變量的并發(fā)操作,只能用線程不能用進(jìn)程。
- 執(zhí)行過程: 每個獨(dú)立的進(jìn)程有一個程序運(yùn)行的入口、順序執(zhí)行序列和程序入口。但是線 程不能獨(dú)立執(zhí)行,必須依存在應(yīng)用程序中,由應(yīng)用程序提供多個線程執(zhí)行控制。
- 線程是處理器調(diào)度的基本單位,但是進(jìn)程不是。
例如,有一個進(jìn)程是打掃教室,其中掃地、擦桌子、擦玻璃、擦黑板就是不同的線程。
多線程原理
CPU在單位時間片里在各個線程之間切換。
線程的生命周期包含5個階段,包括:新建、準(zhǔn)備、運(yùn)行、阻塞、銷毀。
- 新建:創(chuàng)建線程
- 準(zhǔn)備:調(diào)用的線程的
start()方法后,此時線程處于等待CPU分配資源階段,誰先搶的CPU資源,誰開始執(zhí)行。 - 運(yùn)行:當(dāng)線程被調(diào)度并獲得
CPU資源時,便進(jìn)入運(yùn)行狀態(tài),run方法定義了線程的操作和功能。 - 阻塞:在運(yùn)行的時候,可能因?yàn)槟承┰驅(qū)е逻\(yùn)行狀態(tài)的線程被阻塞,比如
sleep()、wait()之后線程就處于了阻塞狀態(tài),這個時候需要其他機(jī)制將處于阻塞狀態(tài)的線程喚醒,比如調(diào)用notify或者notifyAll()方法。喚醒的線程不會立刻執(zhí)行run方法,它們要再次等待CPU分配資源進(jìn)入運(yùn)行狀態(tài)。
銷毀:如果線程正常執(zhí)行完畢后或線程被提前強(qiáng)制性的終止或出現(xiàn)異常導(dǎo)致結(jié)束,那么線程就要被銷毀,釋放資源。
整個流程如下所示:
多線程處理是通過線程池來進(jìn)行的,處理過程中將任務(wù)添加到隊(duì)列,然后在創(chuàng)建線程后自動啟動這些任務(wù)。線程池中的線程都是后臺線程。每個線程都有默認(rèn)的堆棧大小,以默認(rèn)的優(yōu)先級運(yùn)行,并處在多線程單元中。線程池的
飽和策略分為四種,均實(shí)現(xiàn)的RejectedExecutionHandler接口:
-
AbortPolicy直接拋出RejectedExecutionExeception異常來阻止系統(tǒng)正常運(yùn)行 -
CallerRunsPolicy將任務(wù)回退到調(diào)用者 -
DisOldestPolicy丟掉等待最久的任務(wù) -
DisCardPolicy直接丟棄任務(wù)
多線程的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 能適當(dāng)提高程序的執(zhí)行效率
- 能適當(dāng)提高資源的利用率(
CPU,內(nèi)存) - 線程上的任務(wù)執(zhí)行完成后,線程會自動銷毀
缺點(diǎn):
- 開啟線程需要占用一定的內(nèi)存空間(默認(rèn)情況下,每一個線程都占512
KB) - 如果開啟大量的線程,會占用大量的內(nèi)存空間,降低程序的性能
- 線程越多,
CPU在調(diào)用線程上的開銷就越大 - 程序設(shè)計(jì)更加復(fù)雜,比如線程間的通信、多線程的數(shù)據(jù)共享
iOS多線程技術(shù)
iOS多線程有很多技術(shù),下面做了一下對比:
對比得知,我們一般主要使用的還是GCD和NSOperation。
線程和runloop的關(guān)系
-
runloop與線程是一一對應(yīng)的,一個runloop對應(yīng)一個核心的線程。為什么說是核心的,是因?yàn)?code>runloop是可以嵌套的,但是核心的只能有一個,他們的關(guān)系保存在一個全局的字典里。 -
runloop是來管理線程的,當(dāng)線程的runloop被開啟后,線程會在執(zhí)行完任務(wù)后進(jìn)入休眠狀態(tài),有了任務(wù)就會被喚醒去執(zhí)行任務(wù)。 -
runloop在第一次獲取時被創(chuàng)建,在線程結(jié)束時被銷毀。 - 對于主線程來說,
runloop在程序一啟動就默認(rèn)創(chuàng)建好了。 - 對于子線程來說,
runloop是懶加載的,只有當(dāng)我們使用的時候才會創(chuàng)建,所以在子線程用定時器要注意,確保子線程的runloop被創(chuàng)建,不然定時器不會回調(diào)。
線程間的通訊
多線程之間的通訊,除過簡單的獲取主線程,還有2種方式。
-
performSelector:onThread。比如說有A、B兩個線程,我們可以在A線程里使用該方法,調(diào)用B的方法,然后再在B線程里使用該方法調(diào)用A線程的方法,這樣就實(shí)現(xiàn)了線程之間的通訊。
- (void)performSelectorOnMainThread:(SEL)aSelector
withObject:(nullable id)arg
waitUntilDone:(BOOL)wait
- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr
withObject:(nullable id)arg waitUntilDone:(BOOL)wait
-
NSPort。NSPort使用的方式為接收線程中注冊NSPort,在另外的線程中使用此port發(fā)送消息,則被注冊線程會收到相應(yīng)消息,然后最終在主線程里調(diào)用某個回調(diào)函數(shù)。
NSPort有3個子類,NSSocketPort、NSMessagePort、NSMachPort,但在iOS下只有NSMachPort可用。
NSMachPort的方式如下:
- 2.1 創(chuàng)建
A線程的port,將port加入runloop
@property (nonatomic, strong) NSPort *aPort;
@property (nonatomic, strong) TPerson *person;
//1. 創(chuàng)建主線程的port
// 子線程通過此端口發(fā)送消息給主線程
self.aPort = [NSMachPort port];
//2. 設(shè)置port的代理回調(diào)對象
self.aPort.delegate = self;
//3. 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.aPort forMode:NSDefaultRunLoopMode];
self.person = [[TPerson alloc] init];
[NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:) toTarget:self.person withObject:self.aPort];
- 2.2 在
person類中創(chuàng)建一個響應(yīng)線程的方法
@interface TPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end
#import "TPerson.h"
@interface TPerson()
@property (nonatomic, strong) NSPort *bPort;
@end
@implementation TPerson
- (void)personLaunchThreadWithPort:(NSPort *)port{
@autoreleasepool {
// 設(shè)置子線程名字
[[NSThread currentThread] setName:@"TPersonThread"];
// 開啟runloop
[[NSRunLoop currentRunLoop] run];
// 創(chuàng)建自己port
self.bPort = [NSMachPort port];
// 完成向主線程port發(fā)送消息
NSData *data = [@"portPass" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data,self.bPort]];
// 發(fā)送消息到A線程
// 第一個參數(shù):發(fā)送時間。
// msgid 消息標(biāo)識。
// components,發(fā)送消息附帶參數(shù)。
// reserved:為頭部預(yù)留的字節(jié)數(shù)
[port sendBeforeDate:[NSDate date]
msgid:10086
components:array
from:self.bPort
reserved:0];
}
}
- 2.3 在
A線程實(shí)現(xiàn)的代理方法,獲取回調(diào)的信息
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"VC == %@",[NSThread currentThread]);
NSLog(@"從person 傳過來一些信息:");
// NSLog(@"localPort == %@",[message valueForKey:@"localPort"]);
// NSLog(@"remotePort == %@",[message valueForKey:@"remotePort"]);
// NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
// NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
// NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
// NSLog(@"components == %@",[message valueForKey:@"components"]);
NSArray *messageArr = [message valueForKey:@"components"];
NSString *dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"傳過來一些信息 :%@",dataStr);
NSPort *destinPort = [message valueForKey:@"remotePort"];
if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
NSLog(@"傳過來的數(shù)據(jù)有誤");
return;
}
}
Tips
atomic與nonatomic
-
nonatomic非原子性,非線程安全,但是性能高一些,適合內(nèi)存小的移動設(shè)備 -
atomic原子屬性(線程安全),針對多線程設(shè)計(jì)的,是默認(rèn)的。保證同一時間只有一個線程能夠?qū)懭?但是同一個時間多個線程都可以取值),在屬性的setter方法中添加了一把鎖(自旋鎖)。屬于單寫多讀:單個線程寫入,多個線程可以讀取。但是需要消耗大量的資源。
一般iOS開發(fā)中,關(guān)于線程相關(guān)需要注意的是:
- 盡量所有屬性都聲明為
nonatomic - 盡量避免多線程搶奪同一塊資源
- 盡量將加鎖、資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動客戶端的壓力