首页
文章
留言
首页
文章
留言
iOS开发之多线程
2016 年 11 月 11 日
移动应用
Objective-C
iOS
iOS 开发实现多线程有多种方式,下面总结 NSThread 和 GCD 的使用方式。 ### 一、NSThread NSThread 是苹果官方提供的,采用面向对象编程思想,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用 `[NSThread currentThread]` 来显示当前的进程信息。 #### 1、创建、启动线程 先创建线程,再启动线程 ```objectivec // 1. 创建线程 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; // 2. 启动线程 [thread start]; // 线程一启动,就会在线程thread中执行self的run方法 // 新线程调用方法,里边为需要执行的任务 - (void)run { NSLog(@"%@", [NSThread currentThread]); } ``` 创建线程后自动启动线程 ```objectivec // 1. 创建线程后自动启动线程 [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; // 新线程调用方法,里边为需要执行的任务 - (void)run { NSLog(@"%@", [NSThread currentThread]); } ``` 隐式创建并启动线程 ```objectivec // 1. 隐式创建并启动线程 [self performSelectorInBackground:@selector(run) withObject:nil]; // 新线程调用方法,里边为需要执行的任务 - (void)run { NSLog(@"%@", [NSThread currentThread]); } ``` #### 2、线程相关用法 ```objectivec // 获得主线程 + (NSThread *)mainThread; // 判断是否为主线程(对象方法) - (BOOL)isMainThread; // 判断是否为主线程(类方法) + (BOOL)isMainThread; // 获得当前线程 NSThread *current = [NSThread currentThread]; // 线程的名字——setter方法 - (void)setName:(NSString *)n; // 线程的名字——getter方法 - (NSString *)name; ``` #### 3、线程状态控制方法 启动线程方法 ```objectivec - (void)start; // 线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态 ``` 阻塞(暂停)线程方法 ```objectivec + (void)sleepUntilDate:(NSDate *)date; + (void)sleepForTimeInterval:(NSTimeInterval)ti; // 线程进入阻塞状态 ``` 强制停止线程 ```objectivec + (void)exit; // 线程进入死亡状态 ``` #### 4、线程之间的通信 在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。我们先来了解一下官方关于 NSThread 的线程间通信的方法。 ```objectivec // 在主线程上执行操作 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray
*)array; // 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); // 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法 - (id)performSelector:(SEL)aSelector; - (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2; ``` 下面通过一个经典的下载图片 DEMO 来展示线程之间的通信。具体步骤如下: 开启一个子线程,在子线程中下载图片。 回到主线程刷新 UI,将图片展示在 UIImageView 中。 DEMO 代码如下: ```objectivec /** * 创建一个线程下载图片 */ - (void)downloadImageOnSubThread { // 在创建的子线程中调用downloadImage下载图片 [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil]; } /** * 下载图片,下载完之后回到主线程进行 UI 刷新 */ - (void)downloadImage { NSLog(@"current thread -- %@", [NSThread currentThread]); // 1. 获取图片 imageUrl NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"]; // 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作 NSData *imageData = [NSData dataWithContentsOfURL:imageUrl]; // 通过二进制 data 创建 image UIImage *image = [UIImage imageWithData:imageData]; // 3. 回到主线程进行图片赋值和界面刷新 [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES]; } /** * 回到主线程进行图片赋值和界面刷新 */ - (void)refreshOnMainThread:(UIImage *)image { NSLog(@"current thread -- %@", [NSThread currentThread]); // 赋值图片到imageview self.imageView.image = image; } ``` #### 5、 NSThread 线程安全和线程同步 **线程安全:** 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。 **线程同步:** 可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。 举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。 下面,我们模拟火车票售卖的方式,实现 NSThread 线程安全和解决线程同步问题。 场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。 **1、NSThread 非线程安全** 先来看看不考虑线程安全的代码: ```objectivec /** * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票 */ - (void)initTicketStatusNotSave { // 1. 设置剩余火车票为 50 self.ticketSurplusCount = 50; // 2. 设置北京火车票售卖窗口的线程 self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil]; self.ticketSaleWindow1.name = @"北京火车票售票窗口"; // 3. 设置上海火车票售卖窗口的线程 self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil]; self.ticketSaleWindow2.name = @"上海火车票售票窗口"; // 4. 开始售卖火车票 [self.ticketSaleWindow1 start]; [self.ticketSaleWindow2 start]; } /** * 售卖火车票(非线程安全) */ - (void)saleTicketNotSafe { while (1) { // 如果还有票,继续售卖 if (self.ticketSurplusCount > 0) { self.ticketSurplusCount --; NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]); [NSThread sleepForTimeInterval:0.2]; } // 如果已卖完,关闭售票窗口 else { NSLog(@"所有火车票均已售完"); break; } } } ``` **2、NSThread 线程安全** **线程安全解决方案:**可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。`@synchronized`、` NSLock`、`NSRecursiveLock`、`NSCondition`、`NSConditionLock`、`pthread_mutex`、`dispatch_semaphore`、`OSSpinLock`、`atomic(property) set/get` 等等各种方式。为了简单起见,这里不对各种锁的解决方案和性能做分析,只用最简单的 `@synchronized` 来保证线程安全,从而解决线程同步问题。 考虑线程安全的代码: ```objectivec /** * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票 */ - (void)initTicketStatusSave { // 1. 设置剩余火车票为 50 self.ticketSurplusCount = 50; // 2. 设置北京火车票售卖窗口的线程 self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil]; self.ticketSaleWindow1.name = @"北京火车票售票窗口"; // 3. 设置上海火车票售卖窗口的线程 self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil]; self.ticketSaleWindow2.name = @"上海火车票售票窗口"; // 4. 开始售卖火车票 [self.ticketSaleWindow1 start]; [self.ticketSaleWindow2 start]; } /** * 售卖火车票(线程安全) */ - (void)saleTicketSafe { while (1) { // 互斥锁 @synchronized (self) { // 如果还有票,继续售卖 if (self.ticketSurplusCount > 0) { self.ticketSurplusCount --; NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]); [NSThread sleepForTimeInterval:0.2]; } // 如果已卖完,关闭售票窗口 else { NSLog(@"所有火车票均已售完"); break; } } } } ``` ### 二、GCD GCD,全名 Grand Central Dispatch,中文名郭草地,是基于C语言的一套多线程开发 API,一听名字就是个狠角色,也是目前苹果官方推荐的多线程开发方式。可以说是使用方便,又不失逼格。总体来说,他解决我提到的上面直接操作线程带来的难题,它自动帮你管理了线程的生命周期以及任务的执行规则。下面我们会频繁的说道一个词,那就是任务,说白了,任务其实就是你要执行的那段代码。 #### 任务管理方式 - 队列 当我们要管理多个任务时,线程开发给我们带来了一定的技术难度,或者说不方便性,GCD 给出了我们统一管理任务的方式,那就是队列。我们来看一下 iOS 多线程操作中的队列:(不管是串行还是并行,队列都是按照 FIFO 的原则依次触发任务)。 ##### 两个通用队列: - **串行队列:**所有任务会在一条线程中执行(有可能是当前线程也有可能是新开辟的线程),并且一个任务执行完毕后,才开始执行下一个任务。(等待完成) - **并行队列:**可以开启多条线程并行执行任务(但不一定会开启新的线程),并且当一个任务放到指定线程开始执行时,下一个任务就可以开始执行了。(等待发生) ##### 两个特殊队列: - **主队列:**系统为我们创建好的一个串行队列,牛逼之处在于它管理必须在主线程中执行的任务,属于有劳保的。 - **全局队列:**系统为我们创建好的一个并行队列,使用起来与我们自己创建的并行队列无本质差别。 #### 任务执行方式 说完队列,相应的,任务除了管理,还得执行,要不然有钱不花,掉了白搭,并且在 GCD 中并不能直接开辟线程执行任务,所以在任务加入队列之后,GCD 给出了两种执行方式——同步执行(sync)和异步执行(async)。 - **同步执行:**在当前线程执行任务,不会开辟新的线程。必须等到 `Block` 函数执行完毕后,`dispatch` 函数才会返回。 - **异步执行:**可以在新的线程中执行任务,但不一定会开辟新的线程。`dispatch` 函数会立即返回, 然后 `Block` 在后台异步执行。 #### 任务队列组合方式 线程死锁 ```objectivec - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"1========%@",[NSThread currentThread]); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2========%@",[NSThread currentThread]); }); NSLog(@"3========%@",[NSThread currentThread]); } ``` 这样不死锁 ```objectivec - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"1========%@",[NSThread currentThread]); NSLog(@"2========%@",[NSThread currentThread]); NSLog(@"3========%@",[NSThread currentThread]); } ``` #### 队列与执行方式的搭配 ##### 1、串行队列 + 同步执行 全部都在当前线程顺序执行,同步执行不具备开辟新线程的能力。 ```objectivec -(void)queue_taskTest{ dispatch_sync(self.serialQueue, ^{ NSLog(@"1========%@",[NSThread currentThread]); // [self nslogCount:10000 number:1]; }); dispatch_sync(self.serialQueue, ^{ NSLog(@"2========%@",[NSThread currentThread]); // [self nslogCount:10000 number:2]; }); dispatch_sync(self.serialQueue, ^{ NSLog(@"3========%@",[NSThread currentThread]); // [self nslogCount:10000 number:3]; }); NSLog(@"4========%@",[NSThread currentThread]); } ``` #### 2、串行队列 + 异步执行 异步执行具有开辟新线程的能力,并且串行队列必须等到前一个任务执行完才能开始执行下一个任务,同时,异步执行会使内部函数率先返回,不会与正在执行的外部函数发生死锁。 ```objectivec -(void)queue_taskTest{ dispatch_async(self.serialQueue, ^{ NSLog(@"1========%@",[NSThread currentThread]); // [self nslogCount:10000 number:1]; }); dispatch_async(self.serialQueue, ^{ NSLog(@"2========%@",[NSThread currentThread]); // [self nslogCount:10000 number:2]; }); dispatch_async(self.serialQueue, ^{ NSLog(@"3========%@",[NSThread currentThread]); // [self nslogCount:10000 number:3]; }); NSLog(@"4========%@",[NSThread currentThread]); } ``` #### 3、并行队列 + 同步执行 不具备开辟新线程的能力。 ```objectivec -(void)queue_taskTest{ dispatch_sync(self.concurrentQueue, ^{ NSLog(@"1========%@",[NSThread currentThread]); // [self nslogCount:10000 number:1]; }); dispatch_sync(self.concurrentQueue, ^{ NSLog(@"2========%@",[NSThread currentThread]); // [self nslogCount:10000 number:2]; }); dispatch_sync(self.concurrentQueue, ^{ NSLog(@"3========%@",[NSThread currentThread]); // [self nslogCount:10000 number:3]; }); NSLog(@"4========%@",[NSThread currentThread]); } ``` #### 4、并行队列 + 异步执行 具有开辟新线程的能力。 ```objectivec -(void)queue_taskTest{ dispatch_async(self.concurrentQueue, ^{ NSLog(@"1========%@",[NSThread currentThread]); // [self nslogCount:10000 number:1]; }); dispatch_async(self.concurrentQueue, ^{ NSLog(@"2========%@",[NSThread currentThread]); // [self nslogCount:10000 number:2]; }); dispatch_async(self.concurrentQueue, ^{ NSLog(@"3========%@",[NSThread currentThread]); // [self nslogCount:10000 number:3]; }); NSLog(@"4========%@",[NSThread currentThread]); } ``` #### GCD其他函数用法 ##### 1、dispatch_after 该函数用于任务延时执行,其中参数 `dispatch_time_t` 代表延时时长,`dispatch_queue_t` 代表使用哪个队列。如果队列未主队列,那么任务在主线程执行,如果队列为全局队列或者自己创建的队列,那么任务在子线程执行,代码如下: ```objectivec -(void)GCDDelay{ // 主队列延时 dispatch_time_t when_main = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)); dispatch_after(when_main, dispatch_get_main_queue(), ^{ NSLog(@"main_%@",[NSThread currentThread]); }); // 全局队列延时 dispatch_time_t when_global = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)); dispatch_after(when_global, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"global_%@",[NSThread currentThread]); }); // 自定义队列延时 dispatch_time_t when_custom = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)); dispatch_after(when_custom, self.serialQueue, ^{ NSLog(@"custom_%@",[NSThread currentThread]); }); } ``` ##### 2、dispatch_once 保证函数在整个生命周期内只会执行一次,看代码: ```objectivec -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSLog(@"%@",[NSThread currentThread]); }); } ``` ##### 3、dispatch_group_async & dispatch_group_notify 队列组,当加入到队列组中的所有任务执行完成之后,会调用 `dispatch_group_notify` 函数通知任务全部完成,代码如下: ```objectivec -(void)GCDGroup{ // [self jointImageView]; // dispatch_group_t group = dispatch_group_create(); __block UIImage *image_1 = nil; __block UIImage *image_2 = nil; // 在 group 中添加一个任务 dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ image_1 = [self imageWithPath:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1502706256731&di=371f5fd17184944d7e2b594142cd7061&imgtype=0&src=http%3A%2F%2Fimg4.duitang.com%2Fuploads%2Fitem%2F201605%2F14%2F20160514165210_LRCji.jpeg"]; }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ image_2 = [self imageWithPath:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=776127947,2002573948&fm=26&gp=0.jpg"]; }); // group中所有任务执行完毕,通知该方法执行 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ self.imageView_1.image = image_1; self.imageView_2.image = image_2; // UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0f); [image_2 drawInRect:CGRectMake(0, 0, 100, 100)]; [image_1 drawInRect:CGRectMake(100, 0, 100, 100)]; UIImage *image_3 = UIGraphicsGetImageFromCurrentImageContext(); self.imageView_3.image = image_3; UIGraphicsEndImageContext(); }); } -(void)jointImageView{ self.imageView_1 = [[UIImageView alloc] initWithFrame:CGRectMake(20, 50, 100, 100)]; [self.view addSubview:_imageView_1]; self.imageView_2 = [[UIImageView alloc] initWithFrame:CGRectMake(140, 50, 100, 100)]; [self.view addSubview:_imageView_2]; self.imageView_3 = [[UIImageView alloc] initWithFrame:CGRectMake(20, 200, 200, 100)]; [self.view addSubview:_imageView_3]; self.imageView_1.layer.borderColor = self.imageView_2.layer.borderColor = self.imageView_3.layer.borderColor = [UIColor grayColor].CGColor; self.imageView_1.layer.borderWidth = self.imageView_2.layer.borderWidth = self.imageView_3.layer.borderWidth = 1; } ``` ##### 4、dispatch_barrier_async 栅栏函数,使用此方法创建的任务,会查找当前队列中有没有其他任务要执行,如果有,则等待已有任务执行完毕后再执行,同时,在此任务之后进入队列的任务,需要等待此任务执行完成后,才能执行。看代码: ```objectivec -(void)GCDbarrier{ dispatch_async(self.concurrentQueue, ^{ NSLog(@"任务1"); }); dispatch_async(self.concurrentQueue, ^{ NSLog(@"任务2"); }); // dispatch_barrier_async(self.concurrentQueue, ^{ // NSLog(@"任务barrier"); // }); // NSLog(@"big"); dispatch_async(self.concurrentQueue, ^{ NSLog(@"任务3"); }); // NSLog(@"apple"); dispatch_async(self.concurrentQueue, ^{ NSLog(@"任务4"); }); } ``` ##### 5、dispatch_apply 该函数用于重复执行某个任务,如果任务队列是并行队列,重复执行的任务会并发执行,如果任务队列为串行队列,则任务会顺序执行,需要注意的是,该函数为同步函数,要防止线程阻塞和死锁。 **串行队列:** ```objectivec -(void)GCDApply{ // 重复执行 dispatch_apply(5, self.serialQueue, ^(size_t i) { NSLog(@"第%@次_%@",@(i),[NSThread currentThread]); }); } ``` **并行队列:** ```objectivec -(void)GCDApply{ // 重复执行 dispatch_apply(5, self.concurrentQueue, ^(size_t i) { NSLog(@"第%@次_%@",@(i),[NSThread currentThread]); }); } ``` **死锁:** ```objectivec -(void)GCDApply{ // 重复执行 dispatch_apply(5, dispatch_get_main_queue(), ^(size_t i) { NSLog(@"第%@次_%@",@(i),[NSThread currentThread]); }); } ``` ##### 6、dispatch_semaphore_create & dispatch_semaphore_signal & dispatch_semaphore_wait 这几个函数的时候你需要抛开队列,丢掉同步异步,不要把它们想到一起,混为一谈,信号量只是控制任务执行的一个条件而已,相对于上面通过队列以及执行方式来控制线程的开辟和任务的执行,它更贴近对于任务直接的控制。类似于单个队列的最大并发数的控制机制,提高并行效率的同时,也防止太多线程的开辟对 CPU 早层负面的效率负担。 > **dispatch_semaphore_create** 创建信号量,初始值不能小于0; > **dispatch_semaphore_wait** 等待降低信号量,也就是信号量-1; > **dispatch_semaphore_signal** 提高信号量,也就是信号量+1; > **dispatch_semaphore_wait** 和 **dispatch_semaphore_signal** 通常配对使用。
1
相关文章
iOS开发之面向对象
iOS开发之Touch ID指纹解锁实例
iOS开发之类与类扩展
iOS开发之WiFi传输文件
iOS开发之定时执行任务
全部分类
前端
后端
运维
架构
算法
数据库
移动应用
桌面应用
程序开发
热门标签
C++
CSS
Sphinx
Objective-C
爬虫
Supervisor
GUI
JavaScript
Linux
Nginx
PHP
Qt
NoSQL
Docker
Elasticsearch
MongoDB
Kubernetes
Git
Kafka
iOS
Composer
Shell
Redis
HTML
Lua
macOS
OpenResty
CentOS
Android
Python
MySQL
多线程
热门文章
Redis缓存击穿、穿透、雪崩
Kubernetes介绍
JavaScript之window.location
PHP开发之字符串处理
jquery.tmpl使用总结
Elasticsearch详解
Supervisor使用总结
PHP使用Kafka
PHP开发之PDO使用总结
CSS让元素水平垂直居中