首页
文章
留言
首页
文章
留言
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开发之定时执行任务
iOS开发之面向对象
iOS开发之收集崩溃信息
iOS开发之WiFi传输文件
全部分类
前端
后端
运维
架构
算法
数据库
移动应用
桌面应用
程序开发
热门标签
Git
MongoDB
macOS
Python
Composer
Objective-C
Kubernetes
OpenResty
Linux
PHP
Qt
Elasticsearch
GUI
JavaScript
CentOS
多线程
NoSQL
Redis
MySQL
CSS
Lua
Kafka
iOS
Sphinx
C++
Shell
Nginx
爬虫
HTML
Supervisor
Docker
Android
热门文章
Supervisor使用总结
OpenResty+Lua+Kafka收集日志
iOS开发之WiFi传输文件
iOS开发之定时执行任务
Kubernetes介绍
10种常见的软件架构模式
Elasticsearch详解
Git使用总结
JavaScript常用函数总结
iOS开发之多线程