本文主要从
1、 ios三种创建方式特点,场景。
2、再到串行,并行,同步,异步,概念介绍,搭配使用。
3、三种方式具体代码实现,搭配特点。
4、线程安全。
逐一的了解iOS分线程。既是对自己线程阶段的学习归纳,也是一个学习的过程的分享,如有什么地方说错了,或不好的地方,希望并欢迎大家指出一起交流。
1、iOS多线程三种方式
说起iOS多线程,如果线程的某些难点不知道这也很正常,但是你不知道iOS操作线程的三种方式,那就是没有学过iOS多线程啊。那本文就先讲一下三种方式的基本概念,使用场景。
一、NSThread
NSThread是iOS提供的可以开辟一个线程的类,是完全面向对象的,可以直接操控线程对象和方法,非常直观和方便。
- 特点
1、使用更加面向对象
2、简单易用,可直接操作线程对象
3、OC语言
- 缺点
线程需要管理线程的生命周期、同步、加锁问题,会导致一定的性能开销。同时线程总数无法控制(每次创建并不能重用之前的线程,只能创建一个新的线程)。
由于NSThread需要管理的太多,容易出错,所以在日常的开发中用到的并不多。
但是NSThread为我们提供几种方法用于调试十分方便。
1、[NSThread currentThread] 跟踪任务所在线程
2、[NSThread isMainThread] 当前是否是主线程
3、[NSThread sleepForTimeInterval:0.5]; 线程休眠
二、GCD
GCD用来解决多核编程问题,IOS4之后提出的,是基于C语言的底层API。GCD是苹果为多核编程的并行运算提出的解决方案,所以会自动合理的利用更多的CPU内核。
- 特点
1、自动管理线程的生命周期,创建线程、调度任务、销毁线程。
2、使用block来定义任务,使用起来非常灵活。
3、通过GCD可以创建串行、并发、主队列,可以同步和异步执行任务。
4、基于C语言的底层API,充分利用设备的多核(自动)
- 缺点
并发队列在多个线程中执行(前提是使用异步方法),顺序控制相对复杂。任务之间有依赖关系或者想要监听任务完成状态相对NSOperation比较难以实现
所以综合优缺点,在线程没有依赖关系和没有想要监听任务状态的情况下优先使用GCD。因为他更高效,更合理的使用CPU,是实际开发中使用最多的操作线程方式。
三、NSOperation
它是苹果公司对GCD的封装,是面向对象的线程技术。
- 特点
1、只需将任务放到对应的队列中,不必关心线程管理、同步等问题。
2、NSOperation是抽象类,调用只能使用子类NSInvocationOperation和NSBlockOperation。相比NSInvocationOperation推荐使用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题。
3、能更好的控制线程总数及线程依赖关系,监听线程状态。
4、基于GCD(底层是GCD)比GCD多了一些更简单实用的功能
所以NSOperation相对GCD是在一个比较复杂的线程逻辑环境(例如线程依赖关系,监听线程状态
)的时候使用。
2、 串行,并行,同步,异步。
前言:上节三种线程的创建方式是iOS线程的基础。这节同样线程的基本运行逻辑也是很重要的。两个队列,两个执行方式,还有主队列,全局队列的混搭使用是比较容易弄混的。下面我们就逐一解释介绍一下。
1、基本概念
队列分为串行和并行: 队列只是负责任务的调度,而不负责任务的执行
- 串行
让任务一个接着一个地执行
串行队列的任务都是有序的 一个一个的执行的。
- 并行
让多个任务并发(同时)执行 只要有空闲的线程,队列就会调度当前任务,交给线程去执行,不需要考虑前面是都有任务在执行。
只要有线程可以利用,队列就会调度任务。
任务的执行分为同步和异步
- 同步
不会开启新的线程,任务按顺序执行
- 异步
会开启新的线程,任务可以并发的执行
2、搭配使用
1、 串行队列同步执行 主线程中执行
同步不会开启新的线程,则串行队列同步执行只是按部就班的 one by one 执行。
2、 串行队列异步执行 分线程中执行
只开辟一个分线程
虽然队列中存放的是异步执行的任务,但是结合串行队列的特点,前一个任务不执行完毕,队列不会调度,所以串行队列异步执行也是 one by one 的执行
3、 并行队列同步执行 主线程中执行
虽然并行队列可以不需等待前一个任务执行完毕就可调度下一个任务,但是任务同步执行不会开启新的线程,所以任务也是 one by one 的执行
4、 并行队列异步执行 分线程中执行
开辟很多个分线程
异步执行是任务可以开启新的线程,所以这中组合可以实现任务的并发,再实际开发中也是经常会用到的
在iOS7.0的时候,使用GCD系统通常只能开5~8条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:3~5条最为合理。
5、 主队列(串行)异步执行 主队列中执行
主队列执行完才能执行
虽然是异步但是添加到了主队列中,需要等待主队列任务执行完全,才能执行任务,所以实际开发用到的很少。
6、 主队列(串行)同步执行 死锁
同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,就会和主线程形成一个相互等待的结果。从而造成死锁。
7、 全局队列(并行)同步和异步执行
和并行队列同步和异步执行效果一样。这里可以使用
dispatch_get_global_queue
直接获取主队列不需要创建。系统为每个应用程序提供四个并发调度队列。这些队列对应用程序是全局的,并且仅通过优先级来区分。因为它们是全局的,所以不需要显式的创建它们。
3、NSOperation中的队列
1、Operation Queues 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
2、操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
3、NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
3、代码实现
一步一步的敲击键盘是理解程序的最好方式
一、NSThread
第一种创建线程的方式:alloc init. //特点:需要手动开启线程,可以拿到线程对象进行详细设置
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(jisuanNumber) object:nil];
[thread start];
第二种创建线程的方式:分离出一条子线程 //特点:自动启动线程,无法对线程进行更详细的设置
[NSThread detachNewThreadSelector:@selector(jisuanNumber) toTarget:self withObject:nil];
第三种创建线程的方式:后台线程 //特点:自动启动县城,无法进行更详细设置
[self performSelectorInBackground:@selector(jisuanNumber) withObject:nil];
从子线程回到主线程
[self performSelectorOnMainThread:@selector(result:) withObject:@(sum) waitUntilDone:YES];
设置线程的名称
thread.name = @"线程A";
设置线程的优先级,
thread.threadPriority = 1.0;
注意线程优先级的取值范围为0.0~1.0之间,1.0表示线程的优先级最高,如果不设置该值,那么理想状态下默认为0.5
[NSThread exit]; //退出当前线程
[NSThread currentThread]; // 跟踪任务所在线程
[NSThread isMainThread]; // 当前是否是主线程
[NSThread sleepForTimeInterval:0.5]; // 线程休眠
二、GCD
最重要的一部分,直接上代码
- 1、获取队列,创建队列
/** 主队列 (串行) **/
dispatch_queue_t queueMain = dispatch_get_main_queue();
/** 全局并行队列 第一个参数优先级 第二个参数目前用不到**/
dispatch_queue_t queueGlobal = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/** 创建串行队列 **/
dispatch_queue_t queueSerial = dispatch_queue_create("serial",DISPATCH_QUEUE_SERIAL);
/** 创建并行队列 **/
dispatch_queue_t queueConcu = dispatch_queue_create("concu", DISPATCH_QUEUE_CONCURRENT);
- 2、同步搭配队列
/** 同步 - 串行 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_sync(queueSerial, ^{ /** 在主线程,与主队列顺序执行 **/
NSLog(@"%d %@ %d",i, [NSThread currentThread],[NSThread isMainThread]);
});
}
/** 同步 --- 并行 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_sync(queueConcu, ^{ /** 在主线程,与主队列顺序执行 **/
NSLog(@"%d %@ %d",i, [NSThread currentThread],[NSThread isMainThread]);
});
}
/** 同步 --- 全局并行队列 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_sync(queueGlobal, ^{ /** 在主线程,与主队列顺序执行 **/
NSLog(@"%d %@",i, [NSThread currentThread]);
});
}
/** 同步 --- 主队列 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_sync(queueMain, ^{ /** 死锁 **/
NSLog(@"%d %@ %d",i, [NSThread currentThread],[NSThread isMainThread]);
});
}
总结:同步添加到队列是立即执行的,无论是添加到什么队列都是在主线程中执行,不会开辟新的线程。
- 3、异步搭配队列
/** 异步 - 串行 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_async(queueSerial, ^{
NSLog(@"%d %@",i, [NSThread currentThread]); /** 不在主线程,同一个线程 **/
});
}
/** 异步 --- 并行 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_async(queueConcu, ^{
NSLog(@"%d %@",i, [NSThread currentThread]); /** 不在主线程,不同的线程 **/
});
}
/** 异步 --- 全局并行队列 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_async(queueGlobal, ^{
NSLog(@"%d %@",i, [NSThread currentThread]); /** 不在主线程,不同的线程 **/
});
}
/** 异步 -- 主队列 **/
for (int i = 0 ; i < 10; i ++) {
dispatch_async(queueMain, ^{
NSLog(@"%d %@",i, [NSThread currentThread]); /** 在主队列 (所以必须等主线程执行完成才能执行) **/
});
}
异步在主队列一样不会开辟分线程,并且需要等待主队列执行完毕。在其他队列都会开辟新的线程用来高效执行程序。
异步并行是最常用的分线程方式。
- 4、线程栅栏,控制线程执行顺序
dispatch_async(queueConcu, ^{
for (int i = 0 ; i < 5; i ++) {
NSLog(@" op1 ------->>>");
}
});
dispatch_async(queueConcu, ^{
for (int i = 0 ; i < 5; i ++) {
NSLog(@" op2 ------->>>");
}
});
NSLog(@"同步栅栏开始");
dispatch_barrier_sync(queueConcu, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@" 同步栅栏 --------->>");
}
});
NSLog(@"同步栅栏结束"); /** 同步栅栏在 线程1 2 和栅栏执行代码没有执行完,不会走这一步 **/
dispatch_async(queueConcu, ^{
for (int i = 0 ; i < 5; i ++) {
NSLog(@" op3 ------->>>");
}
});
NSLog(@"异步栅栏开始");
dispatch_barrier_async(queueConcu, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@" 异步栅栏 --------->>");
}
});
NSLog(@"异步栅栏结束"); /** 异步栅栏执不用等线程3 执行完就会走到这一步 **/
dispatch_async(queueConcu, ^{
for (int i = 0 ; i < 5; i ++) {
NSLog(@" op4 ------->>>");
}
});
NSLog(@"主线程结束");
- 5、线程组
/** GCD 线程组 **/
dispatch_queue_t queue = dispatch_queue_create("queue.group",DISPATCH_QUEUE_CONCURRENT); //一个并行队列
dispatch_group_t groupGCD = dispatch_group_create(); //一个线程组
dispatch_group_async(groupGCD, queue, ^{
NSLog(@"开始 :task1");
for (int i = 10; i <= 20 ; i ++) {
NSLog(@"当前线程名称:%@ ——%d",[NSThread currentThread].name,i);
}
});
dispatch_group_async(groupGCD, queue, ^{
NSLog(@"开始 :task2");
for (int i = 20; i <= 30 ; i ++) {
NSLog(@"当前线程名称:%@ ——%d",[NSThread currentThread].name,i);
}
});
dispatch_group_async(groupGCD, queue, ^{
for (int i = 30; i <= 40 ; i ++) {
[NSThread sleepForTimeInterval:0.2];
NSLog(@"当前线程名称:%@ ——%d",[NSThread currentThread].name,i);
}
});
dispatch_group_notify(groupGCD, queue, ^{ //线程组的监听通知
NSLog(@"回调之后,所在线程还是子线程, 这个子线程是以上group中开辟的线程");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始 :task3");
for (int i = 40; i <= 50 ; i ++) {
NSLog(@"当前线程名称:%@ ——%d",[NSThread currentThread].name,i);
if (i == 50) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"回到主线程");
});
}
}
});
});
- 6、和延迟加载
//CGD延迟
dispatch_time_t timeQueue = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC*5);
dispatch_after(timeQueue, dispatch_get_main_queue(), ^{
NSLog(@"*********** 延迟");
});
三、NSOperation
- 1、NSBlockOperation 创建线程 和 基本属性
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.添加额外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSLog(@"main");
2018-05-23 15:35:00.786681+0800 [88478:7181463] 3---<NSThread: 0x60400066e100>{number = 4, name = (null)}
2018-05-23 15:35:00.786681+0800 [88478:7181465] 2---<NSThread: 0x600000469600>{number = 3, name = (null)}
2018-05-23 15:35:00.786707+0800 [88478:7181252] 1---<NSThread: 0x60400007f140>{number = 1, name = main}
2018-05-23 15:35:00.786882+0800 [88478:7181465] 2---<NSThread: 0x600000469600>{number = 3, name = (null)}
2018-05-23 15:35:00.786880+0800 [88478:7181252] 1---<NSThread: 0x60400007f140>{number = 1, name = main}
2018-05-23 15:35:00.786880+0800 [88478:7181463] 3---<NSThread: 0x60400066e100>{number = 4, name = (null)}
2018-05-23 15:36:23.205236+0800 [88859:7190213] main
从打印结果可以看出 addExecutionBlock 的执行是在新的分线程里面。 不过主线程一直在等他们执行完毕。
op1.queuePriority = NSOperationQueuePriorityHigh; // 优先级
[op1 cancel]; // 取消操作
[op1 isFinished]; // 操作是否结束
[op1 isCancelled]; // 操作是否取消
[op1 isExecuting]; // 操作是否正在运行
[op1 isReady]; // 操作是否进入准备就绪状态
[op1 waitUntilFinished]; // 阻塞当前线程,直到该操作结束。
[op1 setCompletionBlock:^{
NSLog(@"-- 线程1 执行完毕");
}]; // 当前操作执行完执行
- 2、线程队列 和 队列常用属性方法
/** 获取主线程 **/
NSOperationQueue *queueMain = [NSOperationQueue mainQueue];
/** 自定义队列 **/
NSOperationQueue *queueCustom = [[NSOperationQueue alloc] init];
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"4===%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"5===%@", [NSThread currentThread]); // 打印当前线程
}
}];
/** 完全乱序,不在主线程。 **/
[queueCustom addOperation:op];
[queueCustom addOperation:op1];
[queueCustom addOperation:op2];
/** maxConcurrentOperationCount
默认情况下为-1,表示不进行限制,可进行并发执行。
为1时,队列为串行队列。只能串行执行。
大于1时,队列为并发队列。
注意:maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。
**/
queueCustom.maxConcurrentOperationCount = 2;
从执行结果可以看出,当maxConcurrentOperationCount 为2的时候,最多两个分线程在队列中同时执行,单并不是只有两个分线程。
如果maxConcurrentOperationCount为1 的时候这个队列就相当于串行队列,但是不同的是会有不同的分线程。
队列也可以直接添加执行线程 :
/** 完全乱序,不在主线程。 **/
[queueCustom addOperationWithBlock:^{
for (int i = 0; i < 10; i ++) {
NSLog(@"op1 : %@", [NSThread currentThread]);
}
}];
[queueCustom addOperationWithBlock:^{
for (int i = 0; i < 10; i ++) {
NSLog(@"op2 : %@", [NSThread currentThread]);
}
}];
/** NSOperationQueue 常用属性方法 **/
[queueCustom cancelAllOperations]; // 取消队列所有操作
[queueCustom setSuspended:YES]; // 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
[queueCustom isSuspended]; // 队列是否处于暂停状态
[queueCustom waitUntilAllOperationsAreFinished]; // 阻塞当前线程,直到队列中的操作全部执行完毕。
[queueCustom operations]; // 当前在队列中的操作数组
[queueCustom operationCount]; // 长度
[NSOperationQueue currentQueue]; // 获取当前队列
[NSOperationQueue mainQueue]; // 获取主队列
- 3、线程依赖
线程依赖其实很好理解,B依赖A 。意思就是B等A结束了才能执行。
但是线程依赖可以引出的就是NSOperation的准备就绪状态。
例如上面的依赖B依赖A进入队列,则A进入准备就绪装填,而B需要
等A执行完成才能执行,所以B并不在准备就绪状态。
当然与第一部分提到的 queuePriority 优先级相比。
执行顺序 依赖大于优先级,例如上面的B依赖A,如果B的优先级比A高,
一样会先执行A在执行B。
NSOperationQueue *queueCustom = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 10; i ++) {
NSLog(@"op1 : %@", [NSThread currentThread]);
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 10; i ++) {
NSLog(@"op2 : %@", [NSThread currentThread]);
}
}];
NSBlockOperation *op3 =[NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 10; i ++) {
NSLog(@"op3 : %@", [NSThread currentThread]);
}
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 10; i ++) {
NSLog(@"op4 : %@", [NSThread currentThread]);
}
}];
[op2 addDependency:op1];
[op3 addDependency:op2];
[op3 removeDependency:op2];
[queueCustom addOperations:@[op1, op2, op3, op4] waitUntilFinished:NO];
4、线程安全
伴随多线程的出现,不同的线程访问同一资源,在保证资源的正确性的前提下,线程锁的概念就运应而生。
一、首先需要介绍锁的类型。
1、二元信号量:占用非占用
他只有两种状态,占用非占用。
有一个线程在访问资源状态就会给为 占用状态, 第二个就不能访问。
2、互斥量: 占用非占用
获取就要释放
互斥量与信号量非常类似,区别就在于信号量可以被任意的线程获取并释放,互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放。
3、临界区: 占用非占用
获取就要释放
本进程
比互斥锁更为严格,临界区的作用范围仅限于本进程,其余线程不可获取该锁。
4、读写锁: 自由,共享,独占
有三种状态,自由,共享,独占。 当处于自由,共享状态的时候可以访问,当以独占方式去获取锁时,线程必须等待锁被所有资源释放后,才可以获取;
5、条件变量: 满足条件的线程才会被唤醒
该锁类似一个塞子,不同的线程均可以等待相同的条件,但只有满足条件的线程才会被唤醒。
二、基本概念
- 死锁
两个(多个)线程都要等待对方完成某个操作才能进行下一步,这时就会发生死锁。
- 线程安全
一段线程安全的代码(对象),可以同时被多个线程或并发的任务调度,不会产生问题。
非线程安全的只能按次序被访问。
- atomic
原子属性,为setter方法加锁,将属性以atomic的形式来声明,该属性变量就能支持互斥锁了。
- nonatomic
非原子属性,不会为setter方法加锁,声明为该属性的变量,客户端应尽量避免多线程争夺同一资源。
三、资源加锁,线程安全
最经典的案例就是卖火车票啦。 很多个窗口同时卖票相当于多个线程同时执行,而他们需要访问的资源是共用的票。
所以票这个东西就属于共享资源,必须让他们一个一个访问,不然会乱套。下面就是其中的一种加锁解决方式,线程安全。
- (void)initTicketStatusSave {
self.ticketSurplusCount = 50;
self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;
__weak typeof(self) weakSelf = self;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicket];
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicket];
}];
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicket {
while (1) {
[self.lock lock]; /** 加锁 **/
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}
[self.lock unlock]; /** 开锁 **/
if (self.ticketSurplusCount <= 0) {
NSLog(@"所有火车票均已售完");
break;
}
}
}
网友评论