iOS多线程
1.Pthreads
真正跨平台的多线程技术,可以跨UNIX、Linux、windows平台。
-
创建Pthreads线程
- 如果要使用Pthreads,先导入头文件
<pthread.h>
- 创建
pthread_create(pthread_t *restrict, const pthread_attr_t *attr,void *(*)(void *), void *restrict)
- 第一个参数 : 保存线程ID的pthread_t,线程的代号(当做就是线程)
- 第二个参数 : 线程的属性
- 第三个参数 : 线程开启之后,用来执行的函数,传入函数指针,就是将来线程需要执行的函数
- 第四个参数 : 给第三个参数的指向函数的指针 传递的参数
- 函数返回值为int类型:0代表线程开启成功;其他代表开启失败
- 线程开启之后,就会在子线程里面执行传入的函数
- 如果要使用Pthreads,先导入头文件
-
其实pthread的功能相当强大,这里只是做一个最简单的了解,后期看情况是否要继续研究
补充点:函数指针
和block的指针类似,函数指针这么来表示:
-
函数指针: (
返回值类型
)(* 变量名
)(参数类型
) 例如: (int
)(* sum
)(int
,int
)- 这个函数指针的变量名为
sum
,函数的返回值类型为int
,两个参数都是int
类型的
- 这个函数指针的变量名为
-
block
指针:(返回值类型
)(^变量名
)(参数类型
) 例如:(void
)(^success
)(int
,int
)- 这个
block
指针的变量名为success
,函数无返回值,两个参数类型为int
- 这个
2.NSThread
NS开头的直接就来到了Foundation框架,一个NSThread对象,就代表一条线程
-
创建线程:
-
**alloc + init **
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
-
此时线程上的任务不会立即执行,而是要启动线程:
-
会返回创建的线程,可以设置线程的一些属性。
-
系统会强引用该线程,直到线程死亡(任务执行完毕或强制关闭)。
[thread start]
;//线程一旦启动,就会执行任务
-
-
直接开启新线程:
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
-
这个方法会直接开启新线程,并执行任务
-
无返回值,拿不到创建的线程
-
-
隐式开启线程:
[self performSelectorInBackground:@selector(run) withObject:nil];
-
这个方法会直接开启新线程,并执行任务
-
无返回值,拿不到创建的线程
-
-
-
线程常用方法:
-
[NSThread currentThread]
: 获取当前线程 -
[NSThread mainThread]
: 获取主线程 -
[thread setName:name]
: 设置线程的名称,方便调试 -
[thread name]
: 获取线程的名称
-
-
控制线程的状态
-
alloc + init
: 创建线程,进入新建状态 -
start
: 启动线程,进入准备就绪状态(等待CPU来调度) -
CPU调度
: 进入运行状态 -
sleep
: 进入阻塞状态 -
exit
: 关闭线程,进入死亡状态
-
-
线程状态示意图:
-
线程间通信常用方法
-
-(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
-
-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
-
用上面的方法已经可以满足普通的多线程开发了。
-
-
多线程数据访问问题:
不同线程,同一时刻访问同一块内存,可能导致数据出错。
解决办法,对可能会同时访问一块内存的代码加锁,同一时刻最多只能有一条线程访问这块内存。
互斥锁:对一段代码加锁之后,同一时刻,最多只能有一条线程执行加锁的代码。
-
使用方法:
@synchronized(锁对象) { 要锁住的代码 }
-
注意点:
- 一定要是同一把锁,否则达不到上锁的的目的。
- 锁住尽量少的代码,互斥锁(上锁、解锁过程)非常耗资源。
- 一定要是同一把锁,否则达不到上锁的的目的。
-
当多条线程想同时访问加锁的代码:(例如让三个线程同时执行一段加锁的代码)
- 当三个线程都开启之后,会陆续(虽然时间基本相同,但是还是有时间差的)来执行这段代码。
- 第一个线程来到之后,会开锁,进入锁住的代码,进入之后,就会解锁,防止其他线程进入。
- 当第一个线程执行完锁住的代码之后,就会走出加锁的代码,此时就会解锁。
- 之后,在锁外等候的第二个线程,就会进入加锁的代码,进入之后就会上锁。依次循环往复。
-
关键点:
当一个线程进入加锁的代码后,就会上锁,执行完毕之后就会解锁;当一个线程访问互斥锁锁住的代码,如果这段代码处于锁住的状态,这个线程就会等待,当这段代码解锁之后,马上进入代码,加上锁,执行代码。
互斥锁解决资源抢夺
补充点:自旋锁(automic)
- iOS中属性默认是automic的,这种原子属性,相当于给set方法加上了自旋锁:
- 使用自旋锁的时候,当上锁之后,等候的线程不会休眠,会一直循环,等候解锁;
- 互斥锁:
- 互斥锁,当加锁的资源已经被一条线程访问的时候,等候的线程会进入休眠状态。
3.GCD
GCD:Grand Central Dispatch,伟大的中枢调度器。使用GCD的时候要把自己置身于一个调度者的身份,而不是纠结线程的问题。就好比十字路口的交警,你不能只关注于一条路,而是调度所有的车辆在不同的道路上畅通行驶。
-
GCD中两个非常重要的概念:
- 任务:要执行的操作
- 队列:存放要执行的操作的地方
-
将任务添加到队列中
- GCD会自动将队列中的任务取出,放到对应的线程中执行
- 任务的取出遵循FIFO原则:先进先出,后进后出
- 注意:
- 任务的取出是有顺序的
- 只要将任务添加到队列,我们不用管线程的问题,系统会自己调度
-
同步与异步 :是否会阻塞当前线程
- 同步 :不具备开启新线程的能力,会等到当前的任务执行完毕,函数才返回
- 异步 :具备开启新线程的能力,不会等到当前的任务执行完毕,函数就会返回
-
串行与并行 :决定任务的执行方式
- 串行 : 任务会一个接着一个的执行
- 并行 : 任务会同时一起执行
-
关于串行和并行:
- 任务被添加至队列之后,GCD按照FIFO(先进先出)的原则取出队列中的任务,放到线程上执行,对于不同的情况,系统会选择是否创建子线程来执行任务。
- 串行 : 从线程上取任务是FIFO的,而且要等一个任务执行完毕之后,才去取下一个任务;下一个执行完毕,再取下一个,依次循环,直到任务都执行完毕,队列被销毁。
- 并行 : 从线程上取任务是FIFO的,但是把一个任务放到线程上之后,马上会去取另一个任务,知道队列上的任务都被放到线程上,如果此时系统的有能力开启多的线程,这些任务都会执行起来,至于哪个任务先执行完不一定。
- 任务被添加至队列之后,GCD按照FIFO(先进先出)的原则取出队列中的任务,放到线程上执行,对于不同的情况,系统会选择是否创建子线程来执行任务。
-
不同函数与队列的搭配方式下,线程开辟及任务执行方式:
-
简单的代码来说明:
#pragma mark - 几种 函数 与 队列 的搭配方式 /** * 异步函数 + 并行队列 ==>会创建一条或多条子线程,任务并行执行。 */ - (void)asyncConcurrent { /** * 创建一个队列 * * @param 第一个参数:队列的标示(方便我们调试) * @param 第二个参数:创建的队列的类型(串行/并行) DISPATCH_QUEUE_CONCURRENT ==> 并行队列 DISPATCH_QUEUE_SERIAL 《==》NULL ==> 串行队列 * * @return 返回创建好的队列 */ // dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc", DISPATCH_QUEUE_CONCURRENT); /** * 获取一个全局的并行队列,这个队列已经由系统创建好 * * @param 第一个参数:队列的优先级/服务质量,传 0 代表使用默认的(具体可以查看头文件) * @param 第二个参数:只作为占位,苹果目前没有用上,要求传 0 * * @return 返回一个全局队列 */ dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ NSLog(@"1-%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"2-%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"3-%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ for (int i = 0; i < 9999; i ++) { NSLog(@"%d-%@",i,[NSThread currentThread]); } NSLog(@"4-%@",[NSThread currentThread]); }); /** 这种情况下: 会首先执行下面的代码,再执行任务(执行block中的代码)。 在异步函数时: 1.首先会执行当前的代码,而不会马上把任务(block中的代码)拿出来执行。 2.在当前的代码执行完毕,就会来执行任务,至于需不需要开辟新的线程,还要看任务是放在什么队列当中执行: 普通串行队列:开辟一条新的线程,所有任务在这个线程中串行执行。 并行队列:可能会开辟多条线程(至于开多少条,有系统决定),并发执行这些任务。 主队列:不会开辟新的线程,任务会在主队列当中串行执行。 */ NSLog(@"code over"); } /** 异步函数 + 串行队列 ==> 会创建一条子线程,任务会在新创建的子线程里面串行执行 */ - (void)asyncSerial { dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc",DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ NSLog(@"1-%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"2-%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"3-%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ for (int i = 0; i < 9999; i ++) { NSLog(@"%d-%@",i,[NSThread currentThread]); } NSLog(@"4-%@",[NSThread currentThread]); }); /** 这种情况下,也会先执行下面的代码,再执行任务(block中的代码) 道理同上 */ NSLog(@"code over"); } /** 同步函数 + 并行队列 ==> 不会创建子线程,任务会在当前线程串行执行。 */ - (void)syncConcurrent { dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_sync(queue, ^{ NSLog(@"1-%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"2-%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"3-%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ for (int i = 0; i < 9999; i ++) { NSLog(@"%d-%@",i,[NSThread currentThread]); } NSLog(@"4-%@",[NSThread currentThread]); }); /** 这里会在任务都执行完毕之后,再执行下面的代码 在同步函数时: 1、当前线程会被阻塞。 2、立即执行任务 3、在任务执行完毕之前,当前线程相当于阻塞住了(所以,在主线程中使用相当于没用) */ NSLog(@"code over"); } /** 同步函数 + 串行队列 ==> 不会创建子线程,任务会在当前线程串行执行 */ - (void)syncSerial { dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc",DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ NSLog(@"1-%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"2-%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"3-%@",[NSThread currentThread]); }); dispatch_sync(queue, ^{ for (int i = 0; i < 9999; i ++) { NSLog(@"%d-%@",i,[NSThread currentThread]); } NSLog(@"4-%@",[NSThread currentThread]); }); /** 会把任务依次执行完毕,才会执行下面的代码 道理同上 */ NSLog(@"code over"); } /** 异步函数 + 主队列 ==> 不会创建子线程,任务会在主队列串行执行 */ - (void)asyncMainQueue { /** * 获取主队列。系统会自己创建主队列,主队列是串行队列 * */ dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_async(mainQueue, ^{ NSLog(@"1-%@",[NSThread currentThread]); }); dispatch_async(mainQueue, ^{ NSLog(@"2-%@",[NSThread currentThread]); }); dispatch_async(mainQueue, ^{ NSLog(@"3-%@",[NSThread currentThread]); }); dispatch_async(mainQueue, ^{ for (int i = 0; i < 9999; i ++) { NSLog(@"%d-%@",i,[NSThread currentThread]); } NSLog(@"4-%@",[NSThread currentThread]); }); /** 这里会先执行下面的代码,再执行任务 原因: 这里是异步函数,不会阻塞当前线程。 因为是主队列(主队列是串行队列),所以任务会在主线程上串行执行。 */ NSLog(@"code over"); } /** 同步函数 + 主队列 ==> 不会创建子线程,主线程会卡死 */ - (void)syncMainQueue { /** 这里的主线程卡死。是由于调用的是同步函数,会阻塞当前的线程(当前的线程是主线程),所以把任务添加到主队列中之后,GCD会将主队列中的任务,取到主线程执行,但是此时主线程被阻塞,所以无法执行,导致主线程卡死。 */ dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_sync(mainQueue, ^{ NSLog(@"1-%@",[NSThread currentThread]); }); dispatch_sync(mainQueue, ^{ NSLog(@"2-%@",[NSThread currentThread]); }); dispatch_sync(mainQueue, ^{ NSLog(@"3-%@",[NSThread currentThread]); }); dispatch_sync(mainQueue, ^{ }); NSLog(@"code over"); }
-
注意
- 如果手动创建多条串行队列,这些队列将会并行,每个队列里面的任务会串行。
-
GCD 内存管理:
- ARC环境,就像使用OC对象一样使用
- MRC环境使用
dispatch_retain
和dispatch_release
管理
-
GCD dispatch_barrier:
- 俗称 栅栏,顾名思义,就是将任务分隔开。
- 有
dispatch_barrier_async(,)
、dispatch_barrier_sync(,)
两个函数。 - 会让栅栏前面的任务执行完毕之后,才执行栅栏里面的任务;栅栏里面的任务执行完毕,才执行栅栏后面的任务。
-
GCD dispatch_group
- 将任务包装在一个组里面,组里面的任务执行完毕之后,会调用
dispatch_group_notify(, ,)
函数。 - 用这种方式,可以实现任务的依赖,但是不能跨组和队列。
- 将任务包装在一个组里面,组里面的任务执行完毕之后,会调用
-
GCD的其他常用函数
- 延时执行:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2秒后执行这里的代码... });
- 快速迭代:
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){ // 执行10次代码,index顺序不确定 });
- 注意,这里的快速迭代,会开启多条线程进行遍历,效率更高。
- 一次性代码:
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只执行1次的代码(这里面默认是线程安全的) });
- 与
+ load
、+ inliazed
是不一样的,理论上,只要能拿到他们的类对象就可以执行他们。-
+load
方法在一个类加载的时候会调用。如果这个类有分类,那么先调用这个类的+load方法,再调用这个类的+ load方法。 -
+ inliazed
会在子类和本类初始化类对象的时候调用。子类初始化类对象的时候,调用+inliazed
方法的时候,会调用父类的+ inliazed
方法。
-
- 与
- 延时执行:
补充:单例
单例: 程序运行过程中,一个类始终只有一个实例对象。从创建好之后,程序死亡,才会让这个实例对象死亡。
一次性代码,经常是用在创建单例对象的时候,保证只分配一次内存。
-
实现单例的方案
- 保证只分配一次内存
- 调用alloc方法的时候,内部会调用
allocWithZone
方法,所以控制好allocWithZone
方法的内存开辟操作就能控制alloc
-
copy
、mutableCopy
同样要控制,直接返回调用者就好(因为copy
和mutableCopy
是对象方法,所以如果第一次内存分配控制好了,这里直接返回self
)
- 调用alloc方法的时候,内部会调用
- 保证只分配一次内存
-
具体实现代码
//保存单例对象的静态全局变量 static id _instance; + (instancetype)sharedTools { return [[self alloc]init]; } //在调用alloc方法之后,最终会调用allocWithZone方法 + (instancetype)allocWithZone:(struct _NSZone *)zone { //保证分配内存的代码只执行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instance = [super allocWithZone:zone]; }); return _instance; } //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行 - (id)copyWithZone:(NSZone *)zone { return self; } //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行 - (id)mutableCopyWithZone:(NSZone *)zone { return self; } #if __has_feature(objc_arc) //如果是ARC环境 #else //如果不是ARC环境 //既然是单例对象,总不能被人给销毁了吧,一旦销毁了,分配内存的代码已经执行过了,就再也不能创建对象了。所以覆盖掉release操作 - (oneway void)release { } //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行 - (instancetype)retain { return self; } //为了便于识别,这里返回 MAXFLOAT ,别的程序员看到这个数据,就能意识到这是单例了。纯属装逼…… - (NSUInteger)retainCount { return MAXFLOAT; } #endif
-
注意
- 单例不能继承,由于保存单例的是静态全局变量,所以如果有子类继承的话,拿到的将是同一个对象,访问的是同一块内存。
- 不同的单例,最好直接继承自NSObject,而不要继承自实现单例的类。
- 为了便于创建单例,可以把上面的代码,抽成宏,方便以后使用。
4.NSOperation
是苹果用OC对GCD的封装,更加的面向对象。把任务创建好,添加到队列即可,系统会自己分配线程,让任务执行。
-
NSOperation和NSOperationQueue实现多线程的具体步骤
-
先将需要执行的操作封装到一个
NSOperation
对象中 -
然后将
NSOperation
对象添加到NSOperationQueue
中 -
系统会自动将
NSOperationQueue
中的NSOperation
取出来 -
将取出的
NSOperation
封装的操作放到一条新线程中执行
-
-
队列
NSOperationQueue
-
mainQueue
:主队列- 获取方式:
[NSOperationQueue mainQueue]
- 任务添加到主队列之后,只会被分配到主线程来执行,所以任务一定会是串行
- 获取方式:
- 自己创建的队列:
- 获取方式 :
alloc + init
创建 - 任务添加到这个自己创建的队列,不会被分派到主线程来执行,所以一定会在子线程执行。至于开多少条线程来执行任务,要根据任务的数量以及队列的
maxConcurrentOperationCount
来决定。 -
maxConcurrentOperationCount
:最大并发数:- 给自己创建的队列设置最大并发数,能够控制系统同时最多开启的线程数
- 设置为1,任务会在子线程里面串行执行。(因为对已一条线程而言,任务只会在上面串行执行)
- 设置为大于1,任务会并发在多条子线程上执行
- 设置为0,任务不会执行
- 给自己创建的队列设置最大并发数,能够控制系统同时最多开启的线程数
- 获取方式 :
-
-
任务:
NSOperation
NSOperation
是个抽象类,并不具备封装操作的能力,必须使用它的子类
-
NSOperation
的子类:-
NSInvocationOperation
,设置target
和selector
,任务是:要执行某个对象的某个方法- 调用它的
start
方法之后,这个任务就会被添加到当前线程执行 - 添加到队列之后,就会在新的线程执行
- 调用它的
-
NSBlockOperation
类方法创建:[NSBlockOperation blockOperationWithBlock:^{ }]
- 添加到队列之后,就会执行任务
-
自定义
Operation
继承自NSOperation
- 把要做的操作放到自定义
Operation
的main
方法里面即可 - 即实现
- (void)main
方法 - 创建自定义的
Operation
对象,添加到队列中,就会执行main
方法里面的任务
- 把要做的操作放到自定义
-
-
队列的操作:
-
取消任务:
- 取消队列的所有任务
- (void)cancelAllOperations
; - 也可以调用NSOperation的
- (void)cancel
方法取消单个操作
- 取消队列的所有任务
-
注意:
- 取消操作的时候,只会取消还没有执行的操作,已经在执行的操作会执行完毕
- 所以,在自定义Operation的时候,每开始一个耗时操作,就检测一下,当前的操作是否已经取消(
[self isCancelled]
),取消了就直接return,不要执行。 - 操作一旦取消了,就不能恢复
-
暂停和恢复队列
-
-(void)setSuspended:(BOOL)b
; // YES代表暂停队列,NO代表恢复队列 -
-(BOOL)isSuspended
; //判断是否被暂停了
-
-
注意:
- 操作暂停,只会暂停还没有执行的操作,已经在执行的操作会执行完毕
- 操作暂停了可以再恢复
-
- NSOperation依赖:
- 添加依赖:调用
addDependency
方法添加依赖。 - 一个必须等到它依赖的操作执行完毕了,才能执行这个操作。
- 操作是可以跨队列依赖的
- 不要相互依赖,不然操作执行不了
- 添加依赖:调用