前言
前面第一章知识简单介绍了多线程的理论知识,并没有太多的实际示例,那么本章就主要介绍关于GCD相关的函数,通过每个示例,更加深刻的了解多线程.如果文中观点有错,请大家提出来,相互进步
- 队列的创建
- (void)viewDidLoad {
[super viewDidLoad];
/*
const char *label : 队列的标签
dispatch_queue_attr_t attr : 队列的类型(串行还是并发)
#define DISPATCH_QUEUE_SERIAL 或 NULL :都表示串行
#define DISPATCH_QUEUE_CONCURRENT
*/
dispatch_queue_t queue = dispatch_queue_create("com.William.Alex", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 执行的任务
});
}
- 常用方法
- (void)viewDidLoad {
[super viewDidLoad];
// 获取当前线程
[NSThread currentThread];
// 获取主队列
NSLog(@"%@",dispatch_get_main_queue());
// 获取全局并发队列
/*
long identifier : 优先级(0:表示默认优先级)
unsigned long flags : 预留字段,填0即可
*/
NSLog(@"%@",dispatch_get_global_queue(0, 0));
}
延迟操作
上一章我们大概提了一下延迟操作的三种方法,但是没有细说,在实际开发中,很多时候都需要使用到延迟操作,比如说使用GCD的延迟操作来模拟网络延迟,从而找出隐藏较深的Bug.本章主要讲GCD的延迟操作.
-
方法
GCD的延迟操作.png
- GCD中的延迟操作函数
// 参数 1 :时间 参数 2 : 队列 参数3 : 任务block代码块
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block);
// 参数 : delayInSeconds : 延迟的时间,表示多少时间后执行block代码块中的代码
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 需要执行的任务
});
dispatch_after示例
// 示例虽然简单,但是通俗易懂
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"点击屏幕就会立即打印出来");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"点击屏幕后,2秒之后才会答应出来,不信自己去试试");
});
}
补充 : performSelector方法虽然不是很常用,但是最好也要知道有这个方法可以实现延迟操作
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"仍然需要有一个参照,不然谁知道是不是延迟了3秒");
[self performSelector:@selector(run) withObject:nil afterDelay:3.0];
}
- (void)run {
NSLog(@"看来真的延迟了3秒后才打印");
}
- 控制线程的状态
- (void)viewDidLoad {
[super viewDidLoad];
// 创建一条线程
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(run)
object:nil];
// 开启线程(当线程执行完毕之后,自动进入死亡状态)
[thread start];
// 手动阻塞线程
[NSThread sleepForTimeInterval:0.2];
// 强制停止线程(一旦线程停止(死亡)状态,就无法再次开启该线程)
[NSThread exit];
}
- (void)run {
NSLog(@"跑你妹啊");
}
线程的安全隐患问题
- 多线程的安全隐患 : 为什么多线程存在安全隐患等问题呢,具体是什么原因造成的呢?带着这些问题我们一步一步的学习为什么多线存在安全隐患,解决方法是怎样的.
经典示例 : 资源共享
- 分析 : 当多个线程访问同一块资源时,就很容易引发数据混乱和数据安全等问题.总之,多个线程访问同一个对象,同一个变量,或者同一个文件时,就可能发生数据紊乱等现象.
- 解决方案 : 针对上面所述的问题,这里引入了一个新的概念:互斥锁.
互斥锁
- 互斥锁的原理 : 它的原理就是线程同步技术.
- 线程同步 : 即多条线程在同一条线生执行任务(并且执行任务是有序的,是按顺序执行任务的).
- 互斥锁的使用格式 : @synchronized(锁对象){ // 需要锁定的代码 }
互斥锁的优缺点
- 优点
- 能够有效防止因多条线程因抢夺资源而引发的数据安全问题
- 缺点
- 需要消耗大量的CPU资源
互斥锁的注意点
- 锁定一份代码只能使用1把锁,不管你嵌套多少把锁都是无效的.
- 互斥锁的使用前提 : 多条线程抢夺同一块资源.
互斥锁的应用场景
-
讲到互斥锁的应用场景,这里我们又要引入每天都能接触到两个概念:即原子和非原子属性,对,就是我们每天定义属性时,传入的修饰参数.
-
OC中定义属性时有两种选择:即nonatomic和atomic
-
nonatomic : 非原子属性,不会为setter方法加互斥锁
-
atomic : 原子属性,会为setter方法加锁(系统默认是atomic是需要枷锁的).
-
知识补充 : 一般我们开发APP时,都是使用nonatomic,就性能而言非原子属性的性能高于原子属性,原因是atomic是线程安全的,加锁会消耗大量的CPU资源,nonatomic虽然是非线程安全,但是对于内存较小的移动设备,尽量使用nonatomic.
示例
(忽略多次点击程序会崩溃,这里主要想演示线程加锁)
#import "ViewController.h"
@interface ViewController ()
/** 火车票数量 */
@property (nonatomic, assign) NSInteger titketCount;
/** 售票员1 */
@property(nonatomic, strong) NSThread *conductor1;
/** 售票员2 */
@property(nonatomic, strong) NSThread *conductor2;
/** 售票员3 */
@property(nonatomic, strong) NSThread *conductor3;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// 假设有50张火车票
self.titketCount = 50;
// 创建三个售票窗口
self.conductor1 = [[NSThread alloc] initWithTarget:self
selector:@selector(saleTitkets)
object:nil];
self.conductor1.name = @"售票员1";
self.conductor2 = [[NSThread alloc] initWithTarget:self
selector:@selector(saleTitkets)
object:nil];
self.conductor2.name = @"售票员2";
self.conductor3 = [[NSThread alloc] initWithTarget:self
selector:@selector(saleTitkets)
object:nil];
self.conductor3.name = @"售票员3";
}
#pragma mark - 开启线程
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.conductor1 start];
[self.conductor2 start];
[self.conductor3 start];
}
#pragma mark - 开始售票
- (void)saleTitkets {
while (1) {
@synchronized(self) {
NSInteger count = self.titketCount;
if (count > 0) {
self.titketCount = count - 1;
NSLog(@"%@卖了一张火车票,还剩%ld张火车票",[NSThread currentThread].name, self.titketCount);
} else
{
NSLog(@"没票了");
break;
}
}
}
}
打印结果(没有枷锁时的部分打印)
2016-03-22 14:57:28.770 01 - 多线程[1241:105971] 售票员3卖了一张火车票,还剩47张火车票
2016-03-22 14:57:28.770 01 - 多线程[1241:105969] 售票员1卖了一张火车票,还剩49张火车票
2016-03-22 14:57:28.770 01 - 多线程[1241:105970] 售票员2卖了一张火车票,还剩48张火车票
2016-03-22 14:57:28.771 01 - 多线程[1241:105971] 售票员3卖了一张火车票,还剩46张火车票
2016-03-22 14:57:28.771 01 - 多线程[1241:105969] 售票员1卖了一张火车票,还剩45张火车票
2016-03-22 14:57:28.771 01 - 多线程[1241:105970] 售票员2卖了一张火车票,还剩44张火
- 根据上面的打印,我们没有添加互斥锁,造成的结果就是数据紊乱,第49对应的火车票都还没有被售出去,就直接将第47对应火车票售出.应该是先售出第49对应的火车票,才能去售卖第47对应的火车票.
打印结果(加锁后的部分打印)
2016-03-22 15:02:55.688 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩49张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108070] 售票员2卖了一张火车票,还剩48张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108071] 售票员3卖了一张火车票,还剩47张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩46张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108070] 售票员2卖了一张火车票,还剩45张火车票
2016-03-22 15:02:55.690 01 - 多线程[1268:108071] 售票员3卖了一张火车票,还剩44张火车票
2016-03-22 15:02:55.690 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩43张火
- 注意 : 使用@synchronized(锁对象) { 需要锁定的代码}时,注意它的位置,我又一次将循环也放在锁内,导致打印的结果:火车票全是"售票员1"卖出的.这明显不对.我们只需要将售票环节锁住即可.
一次性代码(单例:下一章专门讲单例模式)
- 一次性代码 : 使用dispatch_once_t函数,能保证在整个应用程序在运行过程中只会被执行一次,其内部的代码系统默认是线程安全的.
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"内部的代码系统默认是线程安全的");
NSLog(@"整个进程运行过程中,只会有一份内存");
});
}
- 一次性代码的应用场景 : 单例模式,具体下一章会专门介绍单例模式的原理以及使用,这里就不再过多介绍了.
快速迭代遍历
- 快速迭代类似于for循环,但是它在并发队列中时,会并发执行block中的任务,所以它的效率肯定是比一般的for循环要更加高效一点
- 注意最后的打印,dispatch_apply是会阻塞主线程的。这个NSLog打印会在dispatch_apply迭代结束后才开始执行,如果没有打印,那么说明阻塞了.
- dispatch_apply快速迭代会避免线程爆炸,因为GCD会管理线程
快速剪切文件-dispatch_apply示例
- (void)apply {
/*
思路 :
1, 获取文件所在的原始文件夹路径
2, 获取文件要去的新文件夹的路径
3, 创建一个文件管理者
4, 拿到原始路径下的所有文件
5, 遍历原始路径下的的文件夹中的所有文件
6, 拼接原始路径和新路径为全路径
7, 使用异步 + 并发队列 将会话管理者中的所有文件从原始文件移动到新文件夹下.
*/
// 创建一个全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 设置剪切文件的路径
NSString *from = @"/Users/mac/Desktop/Frome";
NSString *to = @"/Users/mac/Desktop/To";
/*
NSFileManager也是一个单例
创建文件管理者
*/
NSFileManager *fileManager = [NSFileManager defaultManager];
// 拿到from文件路径中所有子路径
NSArray *subPaths = [fileManager subpathsAtPath:from];
/*
参数 1: from路径下的所有子路径的总数. 参数 2 : 队列 参数 3: block代码块,双击后需要添加索引index
*/
dispatch_apply(subPaths.count, queue, ^(size_t index) {
// 根据索引拿到需要剪切的文件
NSString *subpath = subPaths[index];
// 拼接全路径
NSString *fullFromPath = [from stringByAppendingPathComponent:subpath];
NSString *fullToPath = [to stringByAppendingPathComponent:subpath];
/*
剪切文件:从from文件夹中剪切文件到to文件夹
*/
[fileManager moveItemAtPath:fullFromPath toPath:fullToPath error:nil];
});
}
队列组 : dispatch_group
- dispatch_group的作用 : dispatch_group是专门用来监听多个异步任务,换句话说,dispatch_group实例是用于追踪不同队列中不同的任务.
- 当dispatch_group(队列组)中所有的事件都执行完毕后,GCD提供了两种API方式发送通知.
- 第一种方式 : dispatch_group_wait
- 会阻塞线程,等所有任务都完成或者等待超时时才会执行下一个任务.
- 第二种方式 : dispatch_group_notify(重点讲这个)
-不会阻塞线程,是异步执行block中的任务.
- 第一种方式 : dispatch_group_wait
示例
要求分别下载两张网络上的图片,然后将两张图片合成一张显示出来
- 分析 : 上述示例简单总结为:
- 1, 分别执行两个耗时的异步操作.
- 2, 等2个耗时的异步操作执行完毕之后,再回到主线程刷新UI界面
#import "ViewController.h"
@interface ViewController ()
/**
* 显示下载的图片
*/
@property (weak, nonatomic) IBOutlet UIImageView *AppleImage;
/**
* 图片 1
*/
@property (nonatomic, weak) UIImage *image1;
/**
* 图片 2
*/
@property (nonatomic, weak) UIImage *image2;
@end
@implementation ViewController
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self group];
}
- (void)group {
// 创建全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建一个队列组
dispatch_group_t groupQueue = dispatch_group_create();
// 下载第一张图片
dispatch_group_async(groupQueue, queue, ^{
// 根据url下载图片
NSURL *url = [NSURL URLWithString:@"网络图片Ur"];
// 将图片以二进制的形式保存到本地
NSData *imageData = [NSData dataWithContentsOfURL:url];
// 显示图片
self.image1 = [UIImage imageWithData:imageData];
});
// 下载第二种图片
dispatch_group_async(groupQueue, queue, ^{
// 通过url找到图片
NSURL *url = [NSURL URLWithString:@"网络图片Url"];
// 将图片保存到本地
NSData *imageData = [NSData dataWithContentsOfURL:url];
// 显示图片
self.image2 = [UIImage imageWithData:imageData];
});
// 执行完毕耗时操作,将图片合二为一,然后刷新界面显示图片
dispatch_group_notify(groupQueue, queue, ^{
// 开启上下文
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
// 绘制图片
[self.image1 drawInRect:CGRectMake(0, 0, 100, 100)];
[self.image2 drawInRect:CGRectMake(50, 0, 100, 100)];
// 获取合成的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭上下文
UIGraphicsEndImageContext();
// 回到主线程刷新界面,显示图片
dispatch_async(dispatch_get_main_queue(), ^{
// 显示图片
self.AppleImage.image = image;
});
});
}
@end
- 总结 : 这里使用了图形上下文的知识,因为是需要将下载完毕的两张图片合成一张新图片展示到屏幕上.但是注意,当队列中耗时的操作执行完毕之后,必须要调用_notify这个方法,在这个方法中合成新图片,当该方法中的队列中的任务完成之后,才会来到主线程,进行刷新UI界面.处理UI事件.
线程间的通信
在应用程序运行过程中,线程之间并不是孤立存在的,它们之间是可以线程通讯的
-
线程通信的具体体现
- 一条线程想要传递数据给另一条线程
- 当在某条线程上执行完毕任务之后,需要跳转到另一条线程上去执行任务
-
比如上面队列组中的示例(下载图片),当程序开始下载图片是,原理是:会开启子线程执行下载图片的操作,当下载操作执行完毕之后,需要跳转到主线程上执行UI刷新操作.
下载图片的线程原理
线程之间通信的三种方法
// 跳转到指定线程上执行任务(或者将数据传递到指定线程)
[self performSelector:(nonnull SEL) onThread:(nonnull NSThread *) withObject:(nullable id) waitUntilDone:(BOOL)];
[self performSelector:(nonnull SEL) onThread:(nonnull NSThread *) withObject:<#(nullable id)#> waitUntilDone:(BOOL) modes:(nullable NSArray<NSString *> *)];
// 跳转到主线程上执行任务(或者将数据传递到主线程)
[self performSelectorOnMainThread:(nonnull SEL) withObject:(nullable id) waitUntilDone:(BOOL)]
[self performSelectorOnMainThread:(nonnull SEL) withObject:(nullable id) waitUntilDone:(BOOL) modes:(nullable NSArray<NSString *> *)]
- 注意 :他们虽然传入的参数不一样,但是功能是一样的.
下面介绍一种比较常用的
// 创建全局的并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
// 执行子线程上耗时的操作
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程,执行UI刷新的操作
});
});
-
这种方式的线程通信: 以将它的原理看出一个嵌套.在子线程中嵌套一个主线程,比如:下载图片
-
首先在获取一个异步的并发队列,在并发队列中执行下载图片(耗时的操作),然后在并发队列中嵌套一个异步的队列,该队列是主线程,回到主线程处理UI更新操作.
网友评论