IOS多線程實(shí)現(xiàn)方案一 (pthread、NSThread)
在iOS開發(fā)中润脸,多線程是我們?cè)陂_發(fā)中經(jīng)常使用的一門技術(shù)柬脸。那么本文章將和大家探討一下針對(duì)于多線程的技術(shù)實(shí)現(xiàn)。本文主要分為如下幾個(gè)部分:
- iOS開發(fā)中實(shí)現(xiàn)多線程的方式
- 單線程
- pthread
- NSThread
一毙驯、iOS開發(fā)中實(shí)現(xiàn)多線程的方式
- pthread: 跨平臺(tái)倒堕,適用于多種操作系統(tǒng),可移植性強(qiáng)爆价,是一套純C語(yǔ)言的通用API垦巴,且線程的生命周期需要程序員自己管理媳搪,使用難度較大,所以在實(shí)際開發(fā)中通常不使用骤宣。
- NSThread: 基于OC語(yǔ)言的API蛾号,使得其簡(jiǎn)單易用,面向?qū)ο蟛僮餮难拧>€程的聲明周期由程序員管理鲜结,在實(shí)際開發(fā)中偶爾使用。
- GCD: 基于C語(yǔ)言的API活逆,充分利用設(shè)備的多核精刷,旨在替換NSThread等線程技術(shù)。線程的生命周期由系統(tǒng)自動(dòng)管理蔗候,在實(shí)際開發(fā)中經(jīng)常使用怒允。
- NSOperation: 基于OC語(yǔ)言API,底層是GCD锈遥,增加了一些更加簡(jiǎn)單易用的功能纫事,使用更加面向?qū)ο蟆>€程生命周期由系統(tǒng)自動(dòng)管理所灸,在實(shí)際開發(fā)中經(jīng)常使用丽惶。
二、單線程
進(jìn)程爬立,線程的概念就不在此文進(jìn)行介紹了钾唬。至于提到單線程,只是想跟大家聊一聊如果沒有多線程開發(fā)我們的項(xiàng)目將是怎么樣的侠驯。作為一個(gè)ios開發(fā)人員抡秆,大家都知道主線程是用來(lái)刷新UI的,而線程又是串行的吟策。也就是說(shuō)儒士,我們的程序運(yùn)行在主線程上,如果此時(shí)迎來(lái)了一些耗時(shí)操作的時(shí)候檩坚,我們的手機(jī)屏幕會(huì)卡住着撩,所以多線程開發(fā)是很必要的。本文的ThreadDemo工作組中有一個(gè)在主線程耗時(shí)操作的demo效床,名稱為TimeConsumingDemo睹酌。大家看一下就好了,demo很簡(jiǎn)單剩檀,寫在這里的目的也是希望能給讀者一個(gè)直觀的感受憋沿。
三、pthread
pthread這個(gè)方案在這里只為大家了解沪猴,可能很多l(xiāng)inux開發(fā)人員在使用這種方案進(jìn)行多線程開發(fā)操作辐啄。不過(guò)對(duì)于我來(lái)講在ios開發(fā)中我一次都沒用過(guò)采章。
使用pthread 要引入頭文件
#import <pthread.h>
然后創(chuàng)建線程
pthread_t thread = NULL;
id str = @"i'm pthread param";
pthread_create(&thread, NULL, operate, (__bridge void *)(str));
pthread_create的函數(shù)原型為
int pthread_create(pthread_t * __restrict, const pthread_attr_t * __restrict,
void *(*)(void *), void * __restrict);
第一個(gè)參數(shù)pthread_t * __restrict
由于c語(yǔ)言沒有對(duì)象的概念,所以pthread_t實(shí)際是一個(gè)結(jié)構(gòu)體
所以創(chuàng)建的thread是一個(gè)指向當(dāng)前新建線程的指針
typedef __darwin_pthread_t pthread_t;
typedef struct _opaque_pthread_t *__darwin_pthread_t;
struct _opaque_pthread_t {
long __sig;
struct __darwin_pthread_handler_rec *__cleanup_stack;
char __opaque[__PTHREAD_SIZE__];
};
第二個(gè)參數(shù)const pthread_attr_t * __restrict
同樣是一個(gè)結(jié)構(gòu)體壶辜,這里是用來(lái)設(shè)置線程屬性的
typedef __darwin_pthread_attr_t pthread_attr_t;
typedef struct _opaque_pthread_attr_t __darwin_pthread_attr_t;
struct _opaque_pthread_attr_t {
long __sig;
char __opaque[__PTHREAD_ATTR_SIZE__];
};
第三個(gè)參數(shù)void ()(void *)這里給出了一個(gè)函數(shù)指針悯舟,指向的是一個(gè)函數(shù)的起始地址,所以是線程開啟后的回調(diào)函數(shù)砸民,這里demo給出的是operate函數(shù)抵怎,在線程中進(jìn)行耗時(shí)操作。
第四個(gè)參數(shù)是回調(diào)函數(shù)所用的參數(shù)
void *operate(void *param) {
NSString *str = (__bridge NSString *)(param);
// 用循環(huán)模擬耗時(shí)操作
for (int i = 0; i < 100000; i++) {
// [NSThread currentThread] 為獲取當(dāng)前
NSLog(@"timeConsuming in %@, times: %d, param: %@", [NSThread currentThread], i, str);
}
pthread_exit((void*)0);
}
那么岭参,Pthreads在這里就介紹這么多反惕,要注意的一點(diǎn)是在使用Pthreads的時(shí)候一定要手動(dòng)把當(dāng)前線程結(jié)束掉。因?yàn)槲覀兘?jīng)常使用的GCD和NSOperation已經(jīng)被蘋果封裝過(guò)了演侯,所以我們想要定制進(jìn)行線程的操作就會(huì)有一定限制姿染,如果有想從底層進(jìn)行定制的讀者,可以去搜一下相關(guān)的資料秒际。
四悬赏、 NSThread
NSThread由蘋果進(jìn)行了封裝,并且完全面向?qū)ο舐病K钥梢灾苯邮褂肙C方法操控線程對(duì)象闽颇,非常直觀和方便∏独颍可以說(shuō)對(duì)于ios開發(fā)人員而言进萄,使用NSThread就開始了真正的多線程開發(fā)捻脖。所以锐峭,通過(guò)NSThread我們具體討論一些線程相關(guān)的問題,包括如下內(nèi)容:
- 使用NSThread創(chuàng)建線程
- 線程狀態(tài)
- 線程間通信
- 線程安全
1. 使用NSThread創(chuàng)建線程
使用NSThread創(chuàng)建線程有以下幾種方式:
- 使用NSThread的init方法顯式創(chuàng)建
- 使用NSThread類方法顯式創(chuàng)建并啟動(dòng)線程
- 隱式創(chuàng)建并啟動(dòng)線程
具體的代碼實(shí)現(xiàn)在下面已經(jīng)給出了可婶,這里提醒大家注意一點(diǎn)沿癞。只有使用NSThread的init方法創(chuàng)建的線程才會(huì)返回具體的線程實(shí)例。也就是說(shuō)如果想要對(duì)線程做更多的控制矛渴,比如添加線程的名字椎扬、更改優(yōu)先級(jí)等操作,要使用第一種方式來(lái)創(chuàng)建線程具温。但是此種方法需要使用start
方法來(lái)手動(dòng)啟動(dòng)線程蚕涤。
/**
* 隱式創(chuàng)建并啟動(dòng)線程
*/
- (void)createThreadWithImplicit {
// 隱式創(chuàng)建并啟動(dòng)線程
[self performSelectorInBackground:@selector(threadMethod3:) withObject:@"implicitMethod"];
}
/**
* 使用NSThread類方法顯式創(chuàng)建并啟動(dòng)線程
*/
- (void)createThreadWithClassMethod {
// 使用類方法創(chuàng)建線程并自動(dòng)啟動(dòng)線程
[NSThread detachNewThreadSelector:@selector(threadMethod2:) toTarget:self withObject:@"fromClassMethod"];
}
/**
* 使用init方法顯式創(chuàng)建線程
*/
- (void)createThreadWithInit {
// 創(chuàng)建線程
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod1) object:nil];
// 設(shè)置線程名
[thread1 setName:@"thread1"];
// 設(shè)置優(yōu)先級(jí) 優(yōu)先級(jí)從0到1 1最高
[thread1 setThreadPriority:0.9];
// 啟動(dòng)線程
[thread1 start];
}
2. 線程狀態(tài)
線程狀態(tài)分為:啟動(dòng)線程
, 阻塞線程
铣猩,結(jié)束線程
啟動(dòng)線程:
// 線程啟動(dòng)
- (void)start;
阻塞線程:
// 線程休眠到某一時(shí)刻
+ (void)sleepUntilDate:(NSDate *)date;
// 線程休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
結(jié)束線程
// 結(jié)束線程
+ (void)exit;
大家在看官方api的時(shí)候可能會(huì)有一個(gè)疑問揖铜,api里明明有cancel
方法,為什么使用cancel
方法不能結(jié)束線程达皿?
當(dāng)我們使用cancel
方法時(shí)天吓,只是改變了線程的狀態(tài)標(biāo)識(shí)贿肩,并不能結(jié)束線程,所以我們要配合isCancelled
方法進(jìn)行使用龄寞。具體實(shí)現(xiàn)如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 創(chuàng)建線程
[self createThread];
}
/**
* 創(chuàng)建線程
*/
- (void)createThread {
// 創(chuàng)建線程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
thread.name = @"i'm a new thread";
// 啟動(dòng)線程
[thread start];
}
/**
* 線程方法
*/
- (void)threadMethod {
NSLog(@"thread is create -- the name is: \"%@\"", [NSThread currentThread].name);
// 線程阻塞 -- 延遲到某一時(shí)刻 --- 這里的時(shí)刻是3秒以后
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
NSLog(@"sleep end");
NSLog(@"sleep again");
// 線程阻塞 -- 延遲多久 -- 這里延遲2秒
[NSThread sleepForTimeInterval:2];
NSLog(@"sleep again end");
for (int i = 0 ; i < 100; i++) {
NSLog(@"thread working");
if(30 == i) {
NSLog(@"thread will dead");
[[NSThread currentThread] cancel];
}
if([[NSThread currentThread] isCancelled]) {
// 結(jié)束線程
// [NSThread exit];
return;
}
}
}
3. 線程間通信
線程間通信我們最常用的就是開啟子線程進(jìn)行耗時(shí)操作汰规,操作完畢后回到主線程,進(jìn)行數(shù)據(jù)賦值以及刷新主線程UI物邑。在這里溜哮,用一個(gè)經(jīng)典的圖片下載demo進(jìn)行簡(jiǎn)述。
首先我們先了解一下api給出的線程間通信的方法:
//與主線程通信
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
//與其他子線程通信
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
以下是demo中的代碼片段:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 下載圖片
[self downloadImage];
}
/**
* 下載圖片
*/
- (void)downloadImage {
// 創(chuàng)建線程下載圖片
[NSThread detachNewThreadSelector:@selector(downloadImageInThread) toTarget:self withObject:nil];
}
/**
* 線程中下載圖片操作
*/
- (void)downloadImageInThread {
NSLog(@"come in sub thread -- %@", [NSThread currentThread]);
// 獲取圖片url
NSURL *url = [NSURL URLWithString:@"http://img.ycwb.com/news/attachement/jpg/site2/20110226/90fba60155890ed3082500.jpg"];
// 計(jì)算耗時(shí)
NSDate *begin = [NSDate date];
// 使用CoreFoundation計(jì)算耗時(shí) CFDate
CFTimeInterval beginInCF = CFAbsoluteTimeGetCurrent();
// 從url讀取數(shù)據(jù)(下載圖片) -- 耗時(shí)操作
NSData *imageData = [NSData dataWithContentsOfURL:url];
NSDate *end = [NSDate date];
CFTimeInterval endInCF= CFAbsoluteTimeGetCurrent();
// 計(jì)算時(shí)間差
NSLog(@"time difference -- %f", [end timeIntervalSinceDate:begin]);
NSLog(@"time difference inCF -- %f", endInCF - beginInCF);
// 通過(guò)二進(jìn)制data創(chuàng)建image
UIImage *image = [UIImage imageWithData:imageData];
// 回到主線程進(jìn)行圖片賦值和界面刷新
[self performSelectorOnMainThread:@selector(backToMainThread:) withObject:image waitUntilDone:YES];
// 這里也可以使用imageView的set方法進(jìn)行操作
// [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
}
/**
* 回到主線程的操作
*/
- (void)backToMainThread:(UIImage *)image {
NSLog(@"back to main thread --- %@", [NSThread currentThread]);
// 賦值圖片到imageview
self.imageView.image = image;
}
在demo中已經(jīng)把注釋寫的比較清晰了色解,需要補(bǔ)充的有三點(diǎn):
-
performSelectorOnMainThread:withObject:waitUntilDone:
方法這里是回到了主線程進(jìn)行操作茬射,同樣也可以使用
[self performSelector:@selector(backToMainThread:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
回到主線程,或者進(jìn)入其他線程進(jìn)行操作冒签。
- 在實(shí)際項(xiàng)目中我們可能會(huì)分析耗時(shí)操作所花費(fèi)時(shí)間或者分析用戶行為的時(shí)候要計(jì)算用戶在當(dāng)前頁(yè)面所耗時(shí)間在抛,所以在demo中加入了時(shí)間的兩種計(jì)算方式,分別是CoreFoundation和Foundation中的萧恕。
// 計(jì)算耗時(shí)
NSDate *begin = [NSDate date];
// 使用CoreFoundation計(jì)算耗時(shí) CFDate
CFTimeInterval beginInCF = CFAbsoluteTimeGetCurrent();
// 從url讀取數(shù)據(jù)(下載圖片) -- 耗時(shí)操作
NSData *imageData = [NSData dataWithContentsOfURL:url];
NSDate *end = [NSDate date];
CFTimeInterval endInCF= CFAbsoluteTimeGetCurrent();
// 計(jì)算時(shí)間差
NSLog(@"time difference -- %f", [end timeIntervalSinceDate:begin]);
NSLog(@"time difference inCF -- %f", endInCF - beginInCF);
- 如果自己寫的項(xiàng)目無(wú)法運(yùn)行刚梭,可能是因?yàn)閄code7 創(chuàng)建HTTP請(qǐng)求報(bào)錯(cuò)導(dǎo)致,具體解決方案請(qǐng)點(diǎn)擊這里票唆。
4. 線程安全
因?yàn)槭嵌嗑€程操作朴读,所以會(huì)存在一定的安全隱患。原因是多線程會(huì)存在不同線程的資源共享走趋,也就是說(shuō)我們可能在同一時(shí)刻兩個(gè)線程同時(shí)操作了某一個(gè)變量的值衅金,但是線程的對(duì)變量的操作不同,導(dǎo)致變量的值出現(xiàn)誤差簿煌。下面是一個(gè)存取錢的demo片段:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 初始化狀態(tài)
[self initStatus];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 啟動(dòng)線程
[self startThread];
}
/**
* 初始化狀態(tài)
*/
- (void)initStatus {
// 設(shè)置存款
self.depositMoney = 5000;
// 創(chuàng)建存取錢線程
self.saveThread = [[NSThread alloc] initWithTarget:self selector:@selector(saveAndDraw) object:nil];
self.saveThread.name = @"save";
self.drawThread = [[NSThread alloc] initWithTarget:self selector:@selector(saveAndDraw) object:nil];
self.drawThread.name = @"draw";
}
/**
* 開啟線程
*/
- (void)startThread {
// 開啟存取錢線程
[self.saveThread start];
[self.drawThread start];
}
/**
* 存取錢操作
*/
- (void)saveAndDraw {
while(1) {
if(self.depositMoney > 3000) {
// 阻塞線程氮唯,模擬操作花費(fèi)時(shí)間
[NSThread sleepForTimeInterval:0.05];
if([[NSThread currentThread].name isEqualToString:@"save"]) {
self.depositMoney += 100;
} else {
self.depositMoney -= 100;
}
NSLog(@"currentThread: %@, depositMoney: %d", [NSThread currentThread].name, self.depositMoney);
} else {
NSLog(@"no money");
return;
}
}
}
在上面的demo中我們發(fā)現(xiàn),存取錢的線程是同時(shí)開啟的姨伟,而存取錢的錢數(shù)相同惩琉,所以每一次存取操作結(jié)束后,存款值應(yīng)該不會(huì)改變夺荒。大家可以運(yùn)行demo進(jìn)行查看結(jié)果瞒渠。
所以需要在線程操作中加入鎖:
/**
* 存取錢操作
*/
- (void)saveAndDraw {
while(1) {
// 互斥鎖
@synchronized (self) {
if(self.depositMoney > 3000) {
// 阻塞線程,模擬操作花費(fèi)時(shí)間
[NSThread sleepForTimeInterval:0.05];
if([[NSThread currentThread].name isEqualToString:@"save"]) {
self.depositMoney += 100;
} else {
self.depositMoney -= 100;
}
NSLog(@"currentThread: %@, depositMoney: %d", [NSThread currentThread].name, self.depositMoney);
} else {
NSLog(@"no money");
return;
}
}
}
}
線程安全解決方案:
* 互斥鎖
@synchronized 的作用是創(chuàng)建一個(gè)互斥鎖技扼,保證此時(shí)沒有其它線程對(duì)鎖住的對(duì)象進(jìn)行修改伍玖。
* 互斥鎖使用格式:
@synchronized (鎖對(duì)象) { // 需要鎖定的代碼 }
* 互斥鎖的優(yōu)缺點(diǎn):
優(yōu)點(diǎn): 防止多線程對(duì)共享資源進(jìn)行搶奪造成的數(shù)據(jù)安全問題
缺點(diǎn): 需要消耗大量cpu資源
注:NSThread頭文件中的相關(guān)方法
//獲取當(dāng)前線程
+(NSThread *)currentThread;
//創(chuàng)建線程后自動(dòng)啟動(dòng)線程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
//是否是多線程
+ (BOOL)isMultiThreaded;
//線程字典
- (NSMutableDictionary *)threadDictionary;
//線程休眠到什么時(shí)間
+ (void)sleepUntilDate:(NSDate *)date;
//線程休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//退出線程
+ (void)exit;
//線程優(yōu)先級(jí)
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority NS_AVAILABLE(10_6, 4_0);
- (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
//調(diào)用棧返回地址
+ (NSArray *)callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
+ (NSArray *)callStackSymbols NS_AVAILABLE(10_6, 4_0);
//設(shè)置線程名字
- (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
- (NSString *)name NS_AVAILABLE(10_5, 2_0);
//獲取棧的大小
- (NSUInteger)stackSize NS_AVAILABLE(10_5, 2_0);
- (void)setStackSize:(NSUInteger)s NS_AVAILABLE(10_5, 2_0);
//是否是主線程
- (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
//初始化方法
- (id)init NS_AVAILABLE(10_5, 2_0); // designated initializer
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
//是否正在執(zhí)行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
//是否執(zhí)行完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
//是否取消線程
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//線程啟動(dòng)
- (void)start NS_AVAILABLE(10_5, 2_0);
- (void)main NS_AVAILABLE(10_5, 2_0); // thread body method
@end
//多線程通知
FOUNDATION_EXPORT NSString * const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSString * const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSString * const NSThreadWillExitNotification;
@interface NSObject (NSThreadPerformAdditions)
//與主線程通信
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
//與其他子線程通信
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes
//隱式創(chuàng)建并啟動(dòng)線程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);
由于多線程內(nèi)容比較多,所以這里拆成兩個(gè)部分剿吻。此文介紹PThread和NSThread窍箍,下篇文章將會(huì)跟大家一起討論一下GCD和NSOperation的知識(shí)。本文中的代碼已經(jīng)上傳GitHub,希望本文章能對(duì)大家有所幫助仔燕。
由于示例demo比較多造垛,所以放到了workspace中,大家下載后在運(yùn)行的時(shí)候請(qǐng)選擇好demo晰搀。