iOS 多線程
1.線程與進程
1.1 線程的定義
- 線程是進程的基本執(zhí)行單元,一個進程的所有任務大偶在線程中執(zhí)行
- 進程要想執(zhí)行任務,必須得有線程,進程至少要有一條線程
- 程序啟動會默認開啟一條線程,這條線程被稱為主線程或UI線程
1.2進程
- 進程是系統(tǒng)進行資源和調度的基本單位
- 在移動端進程是指在系統(tǒng)中正在運行的一個應用程序(注:iOS是單進程,Android可以實現(xiàn)多進程)
- 每個進程之間是獨立的,每個進程均運行在其專用的切受保護的內存
1.3進程與線程的關系
- 地址空間:統(tǒng)一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間。
- 資源擁有:同一進程內的線程共享本進程的資源內存、I/O、CPU等,但是進程之間的資源是獨立的。
- 相互影響:一個進程崩潰后,在保護模式下不會對其他進程產生影響,但是一個線程崩潰后整個進程就死掉了,所以多進程要比多線程健壯
- 資源占用:進程切換時資源消耗大,效率高。所以涉及到頻繁的切換時,使用線程要好于進程。同樣如果要求同時進行并且又要共享某些變量的并發(fā)操作,只能用線程,不能用進程。
- 執(zhí)行過程:每個獨立的進程有一個程序的入口、順序執(zhí)行序列和程序入口。但是線程不能獨立執(zhí)行,必須存在應用程序中(進程),有應用程序提供多個線程執(zhí)行控制(多線程開發(fā))。
- 線程是處理器調度的基本單位,但進程不是,線程也是進程執(zhí)行的基本單位
1.4線程與隊列的關系
多線程中的隊列有:串行隊列,并發(fā)隊列,全局隊列,主隊列。
隊可以理解為一種數(shù)據結構,以某種方式,等待線程去執(zhí)行。
1.5 iOS線程與Runloop的關系
- 線程與Runloop是一一對應的,一個Runloop對應一個核心線程,為什么說是核心線程,因為Runloop是可以嵌套的,但是核心只有一個,他們的對應關系保存在一個全局字典里
- Runloop是用來管理線程的,線程執(zhí)行完任務時會進入休眠狀態(tài),有任務進來時會被喚醒,開始執(zhí)行任務。所以說Runloop是事件驅動的。
- Runloop在第一次獲取時被創(chuàng)建,線程結束時被銷毀。主線程的Runloop在程序啟動的時候就會被創(chuàng)建。
- 子線程的Runloop是懶加載的,只有在使用的時候才被創(chuàng)建。
注:在子線程使用NSTimer時要注意確保子線程的Runloop已經創(chuàng)建,否則NSTimer不會生效。
2.多線程
2.1 多線程的定義
多線程是一個進程中并發(fā)多個線程同時執(zhí)行各自的任務。就是由單核CPU通過時間片不斷的切換執(zhí)行程序。
分時操作系統(tǒng)會把CPU的時間劃分為長短基本相同的時間區(qū)間(時間片),在一個時間片內,CPU只能處理一個線程中的一個任務,對于一個單核CPU來說,在不同的時間片來執(zhí)行不同線程中的任務,就形成了多個任務在同時執(zhí)行的“假象”。
2.2多線程的優(yōu)缺點
1.優(yōu)點
*減少應用程序的堵塞,增加程序的執(zhí)行效率
*適當提高CPU和內存的利用率
*線程上的任務執(zhí)行完成后,線程自動銷毀(部分API可實現(xiàn))
3.缺點
*線程的開啟需要占用一定的內存空間,默認是512KB/線程
*線程開啟的越多內存占用越大,會降低程序的性能
*線程越多CPU在調用線程上的開銷就越大
*程序設計更加復雜,需要考慮線程間通信,多線程的數(shù)據共享等問題
2.3 多線程的生命周期
可能造成阻塞的條件:
鎖(lock),循環(huán)引用,sleep()函數(shù),循環(huán)執(zhí)行
2.4 可調度線程池
什么是線程池:
提供一組線程資源用來復用線程資源的一個池子
線程池中常用參數(shù)釋義:
- corePoolSize 線程池的基本大?。ê诵木€程池大?。?/li>
- maximumPool 線程池最大大小
-
keepAliveTime 線程池中超過
corePoolSize數(shù)目的空閑線程的最大存活時間 -
unit
keepAliveTime參數(shù)的時間單位 - workQueue 任務阻塞隊列
- threadFactory 新建線程的工廠
-
handler 當提交任務數(shù)超過
maximumPoolSize與workQueue之和時,任務會交給RejectedExecutionHandler來處理
線程池調度原理:
飽和策略:
- AboutPolicy 直接拋出RejectedExecutionExeception異常,阻止系統(tǒng)的正常運行
- CallerRunsPolicy 將任務回退到調用者
- DisOldestPolicy 丟掉等待最久的任務
- DisCardPolicy 直接丟棄任務
注: 以上四種拒絕策略均實現(xiàn)的RejectedExencutionHandler
注:iOS開發(fā)中不會直接接觸到線程池,這是因為GCD已經包含了線程池的管理,我們常用的就是GCD(NSOperation底層也是GCD),所以只需要通過GCD獲取線程來執(zhí)行任務即可。
繼續(xù)探索多線程更底層的知識可以嘗試搜索Java Posix
2.6 iOS的幾種多線程技術方案
-
pthread:即POSIX Thread,是線程的POSIX標準,是一套通用的多線程API,可以在Unix/Linux/Windows等平臺跨平臺使用。iOS中基本不使用。 -
NSThread:蘋果封裝的面向對象的線程類,可以直接操作線程,比起GCD,NSThread效率更高,由程序員自行創(chuàng)建,當線程中的任務執(zhí)行完畢后,線程會自動退出,程序員也可手動管理線程的生命周期。使用頻率較低。 -
GCD:全稱Grand Central Dispatch,由C語言實現(xiàn),是蘋果為多核的并行運算提出的解決方案,GCD會自動利用更多的CPU內核,自動管理線程的聲明周期,程序員只需要告訴GCD需要執(zhí)行的任務,無需編寫任何管理線程的代碼。GCD也是iOS使用頻率最高的多線程技術。 -
NSOperation:基于GCD封裝的面向對象的多線程類,相較于GCD提供了很多方便的API,使用頻率較高。
3. 線程間的通訊
3.1 幾種線程間的通訊方式
通過上面的介紹我們對線程有了基本的了解,那么線程之間是如何通訊的呢?其實我們都知道的就是在子線程獲取數(shù)據,然后切換到主線程進行刷新UI,其實這只是表象,并不是真正的線程通訊方式,如果在面試中這樣回答基本就是回家等消息了。那么線程之間倒地是如何通訊的呢?在蘋果官方文檔中給我們列出了線程間通訊的幾種方式:

線程之間有許多通信方式,每種方式都有其優(yōu)缺點。配置線程本地存儲列出了您可以在OS x中使用的最常見的通信機制(除了消息隊列和Cocoa分布式對象,這些技術在iOS中也可用)。這個表中的技術是按照復雜度遞增的順序列出的。其中后兩種只能在OS X中使用。
-
Direct messaging:這個就是我們常用的selector系列,例如-performSelector:。 -
Global variables, shared memory, and objects: 直接通過全局變量、共享內存等方式,但這種方式會造成資源搶奪,涉及到線程安全問題。 -
Conditions:條件鎖,一種特殊的鎖,我們可以使用它來控制線程何時執(zhí)行特定部分的代碼。 -
Run loop sources: 自定義Runloop來設置用于在線程上接收應用程序特定消息的循環(huán)源,因為它們是事件驅動的,所以運行循環(huán)源會在無事可做時將線程自動休眠,從而提高線程的效率。 -
Ports and sockets: 通過端口和套接字來實現(xiàn)線程間通訊。
4. iOS中使用線程間通訊示例(使用NSPort)
此處的例子是通過一個PortPerson對象跟ViewController進行發(fā)消息的實例。
代碼中首先將VC的self.myPort添加到主線程的Runloop中,然后起新線程讓Person像VC發(fā)送消息,VC在收到消息后也像Person發(fā)送一條消息,在此過程中并沒有切換線程的代碼,通過打印線程可以看到VC的操作是在主線程中完成的,Person是在子線程(7)中完成的,通過這些代碼就完成了線程間的通訊。
PortPerson 代碼:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface PortPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end
NS_ASSUME_NONNULL_END
#import "PortPerson.h"
@interface PortPerson()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end
@implementation PortPerson
- (void)personLaunchThreadWithPort:(NSPort *)port {
@autoreleasepool {
NSLog(@"Person Thread %@", [NSThread currentThread]);
//1. 保存主線程傳入的port
self.vcPort = port;
//2. 設置子線程名字
[[NSThread currentThread] setName:@"PortPersonThread"];
//3. 開啟runloop
[[NSRunLoop currentRunLoop] run];
//4. 創(chuàng)建自己port
self.myPort = [NSMachPort port];
//5. 設置port的代理回調對象
self.myPort.delegate = self;
//6. 完成向主線程port發(fā)送消息
[self sendPortMessage];
}
}
- (void)sendPortMessage {
NSData *data1 = [@"這是一條port信息" dataUsingEncoding:NSUTF8StringEncoding];
// NSData *data2 = [@"data2" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
// 發(fā)送消息到VC的主線程
// 第一個參數(shù):發(fā)送時間。
// msgid 消息標識。
// components,發(fā)送消息附帶參數(shù)。
// reserved:為頭部預留的字節(jié)數(shù)
[self.vcPort sendBeforeDate:[NSDate date]
msgid:10086
components:array
from:self.myPort
reserved:0];
}
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"person:handlePortMessage == %@",[NSThread currentThread]);
NSLog(@"從VC 傳過來一些信息:");
NSLog(@"components == %@",[(id)message valueForKey:@"components"]);
NSLog(@"receivePort == %@",[(id)message valueForKey:@"receivePort"]);
NSLog(@"sendPort == %@",[(id)message valueForKey:@"sendPort"]);
NSLog(@"msgid == %@",[(id)message valueForKey:@"msgid"]);
}
@end
PortViewController 代碼:
#import "PortViewController.h"
#import <objc/runtime.h>
#import "PortPerson.h"
@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) PortPerson *person;
@end
@implementation PortViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 創(chuàng)建主線程的port,子線程通過此端口發(fā)送消息給主線程
self.myPort = [NSMachPort port];
// 設置port的代理回調對象
self.myPort.delegate = self;
// 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.person = [[PortPerson alloc] init];
[NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
toTarget:self.person
withObject:self.myPort];
}
- (void)handlePortMessage:(NSPortMessage *)message {
NSLog(@"VC Thread --- %@", [NSThread currentThread]);
NSLog(@"從 person 傳過來一些信息");
NSArray *messageArr = [(id)message valueForKey:@"components"];
NSString * dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"通過port傳過來一些信息:%@", dataStr);
NSPort *destinPort = [(id)message valueForKey:@"remotePort"];
if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
NSLog(@"傳過來的數(shù)據有誤");
return;
}
NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
// 非常重要,如果你想在Person的port接收信息,必須加入到當前主線程的runloop
[[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
NSLog(@"VC == %@",[NSThread currentThread]);
BOOL success = [destinPort sendBeforeDate:[NSDate date]
msgid:10010
components:array
from:self.myPort
reserved:0];
NSLog(@"%d",success);
}
@end
打印結果:

注意?。。?/strong>
-
NSPort對象必須添加到要接收消息的線程的Runloop中。 - 接收消息的對象實現(xiàn)
NSPortDelegate協(xié)議的-handlePortMessage:方法來獲取消息內容。!