iOS 多线程实现-NSThread

一、介绍

NSThread由苹果进行了封装,并且完全面向对象。所以可以直接使用OC方法操控线程对象,非常直观和方便。可以说对于ios开发人员而言,使用NSThread就开始了真正的多线程开发。所以,通过NSThread我们具体讨论一些线程相关的问题,包括如下内容:
使用NSThread创建线程

  • 线程状态
  • 线程间通信T
  • 线程安全

二、创建线程

使用NSThread创建线程有以下几种方式:

  • 使用NSThread的init方法显式创建
  • 使用NSThread类方法显式创建并启动线程
  • 隐式创建并启动线程

具体的代码实现在下面已经给出了,这里提醒大家注意一点。只有使用NSThread的init方法创建的线程才会返回具体的线程实例。也就是说如果想要对线程做更多的控制,比如添加线程的名字、更改优先级等操作,要使用第一种方式来创建线程。但是此种方法需要使用start方法来手动启动线程。

/**
 * 隐式创建并启动线程
 */
- (void)createThreadWithImplicit { 
    // 隐式创建并启动线程
    [self performSelectorInBackground:@selector(threadMethod3:) withObject:@"implicitMethod"];
}

/**
 * 使用NSThread类方法显式创建并启动线程
 */
- (void)createThreadWithClassMethod {
    // 使用类方法创建线程并自动启动线程
    [NSThread detachNewThreadSelector:@selector(threadMethod2:) toTarget:self withObject:@"fromClassMethod"];
}

/**
 * 使用init方法显式创建线程
 */
- (void)createThreadWithInit { 
    // 创建线程
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod1) object:nil];
    // 设置线程名
    [thread1 setName:@"thread1"]; 
    // 设置优先级 优先级从0到1 1最高
    [thread1 setThreadPriority:0.9]; 
    // 启动线程
    [thread1 start];
}

三、线程状态

线程状态分为:启动线程,阻塞线程,结束线程
启动线程:

// 线程启动
- (void)start;

阻塞线程:

// 线程休眠到某一时刻
+ (void)sleepUntilDate:(NSDate *)date;
// 线程休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

结束线程

// 结束线程
+ (void)exit;

大家在看官方api的时候可能会有一个疑问,api里明明有cancel方法,为什么使用cancel方法不能结束线程?
当我们使用cancel方法时,只是改变了线程的状态标识,并不能结束线程,所以我们要配合isCancelled方法进行使用。具体实现如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
    // 创建线程
    [self createThread];
}

/**
 * 创建线程
 */
- (void)createThread
 { 
    // 创建线程 
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil]; thread.name = @"i'm a new thread"; 
    // 启动线程 
    [thread start];
}

/**
 * 线程方法
 */
- (void)threadMethod {
    NSLog(@"thread is create -- the name is: \"%@\"", [NSThread currentThread].name); 
    // 线程阻塞 -- 延迟到某一时刻 --- 这里的时刻是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]) { 
            // 结束线程
            [NSThread exit]; 
            return;
        } 
    }
}

四、线程间通信

线程间通信我们最常用的就是开启子线程进行耗时操作,操作完毕后回到主线程,进行数据赋值以及刷新主线程UI。在这里,用一个经典的图片下载demo进行简述。
首先我们先了解一下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 {
    // 创建线程下载图片 
    [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"]; 
    // 计算耗时
    NSDate *begin = [NSDate date];
    // 使用CoreFoundation计算耗时
    CFDate CFTimeInterval beginInCF = CFAbsoluteTimeGetCurrent(); 
    // 从url读取数据(下载图片) -- 耗时操作 
    NSData *imageData = [NSData dataWithContentsOfURL:url]; 
    NSDate *end = [NSDate date];
    CFTimeInterval endInCF= CFAbsoluteTimeGetCurrent(); 
    // 计算时间差 NSLog(@"time difference -- %f", [end timeIntervalSinceDate:begin]); 
    NSLog(@"time difference inCF -- %f", endInCF - beginInCF); 
    // 通过二进制data创建image 
    UIImage *image = [UIImage imageWithData:imageData]; 
    // 回到主线程进行图片赋值和界面刷新
    [self performSelectorOnMainThread:@selector(backToMainThread:) withObject:image waitUntilDone:YES]; 
    // 这里也可以使用imageView的set方法进行操作
    // [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中已经把注释写的比较清晰了,需要补充的有几点:
1、performSelectorOnMainThread:withObject:waitUntilDone:
方法这里是回到了主线程进行操作,同样也可以使用

[self performSelector:@selector(backToMainThread:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];

回到主线程,或者进入其他线程进行操作。
2、在实际项目中我们可能会分析耗时操作所花费时间或者分析用户行为的时候要计算用户在当前页面所耗时间,所以在demo中加入了时间的两种计算方式,分别是CoreFoundation和Foundation中的。

// 计算耗时 
  NSDate *begin = [NSDate date]; 
// 使用CoreFoundation计算耗时 
  CFDate CFTimeInterval beginInCF = CFAbsoluteTimeGetCurrent(); 
// 从url读取数据(下载图片) -- 耗时操作 
  NSData *imageData = [NSData dataWithContentsOfURL:url];   
  NSDate *end = [NSDate date]; 
  CFTimeInterval endInCF= CFAbsoluteTimeGetCurrent(); 
// 计算时间差 
  NSLog(@"time difference -- %f", [end timeIntervalSinceDate:begin]); 
  NSLog(@"time difference inCF -- %f", endInCF - beginInCF);

五、线程安全

因为是多线程操作,所以会存在一定的安全隐患。原因是多线程会存在不同线程的资源共享,也就是说我们可能在同一时刻两个线程同时操作了某一个变量的值,但是线程的对变量的操作不同,导致变量的值出现误差。下面是一个存取钱的demo片段:

- (void)viewDidLoad {
    [super viewDidLoad]; 
    
    // 初始化状态 
    [self initStatus];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
    // 启动线程 
    [self startThread];
}

/**
 * 初始化状态
 */
- (void)initStatus { 
    // 设置存款 
    self.depositMoney = 5000; 
    // 创建存取钱线程
    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) {
            // 阻塞线程,模拟操作花费时间 
            [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中我们发现,存取钱的线程是同时开启的,而存取钱的钱数相同,所以每一次存取操作结束后,存款值应该不会改变。大家可以运行demo进行查看结果。所以需要在线程操作中加入锁:

/**
 * 存取钱操作
 */
- (void)saveAndDraw { 
    while(1) { 
        // 互斥锁 
        @synchronized (self) { 
        if(self.depositMoney > 3000) {
            // 阻塞线程,模拟操作花费时间
            [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 的作用是创建一个互斥锁,保证此时没有其它线程对锁住的对象进行修改。
* 互斥锁使用格式:
    @synchronized (锁对象) { // 需要锁定的代码 }
* 互斥锁的优缺点:
    优点: 防止多线程对共享资源进行抢夺造成的数据安全问题
    缺点: 需要消耗大量cpu资源

六、NSThread头文件中的相关方法

//获取当前线程 
+(NSThread *)currentThread; 

//创建线程后自动启动线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

//是否是多线程
+ (BOOL)isMultiThreaded;

//线程字典
- (NSMutableDictionary *)threadDictionary;

//线程休眠到什么时间
+ (void)sleepUntilDate:(NSDate *)date;

//线程休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

//退出线程
+ (void)exit;

//线程优先级
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority NS_AVAILABLE(10_6, 4_0);
- (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);

//调用栈返回地址
+ (NSArray *)callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
+ (NSArray *)callStackSymbols NS_AVAILABLE(10_6, 4_0);

//设置线程名字
- (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);

//是否正在执行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);

//是否执行完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);

//是否取消线程
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);

//线程启动
- (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
//隐式创建并启动线程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容