前言
- 提到线程,那就不得不提CPU,现代的CPU有一个很重要的特性,就是时间片,每一个获得CPU的任务只能运行一个时间片规定的时间。
- 其实线程对操作系统来说就是一段代码以及运行时数据。操作系统会为每个线程保存相关的数据,当接收到来自CPU的时间片中断事件时,就会按一定规则从这些线程中选择一个,恢复它的运行时数据,这样CPU就可以继续执行这个线程了。
- 也就是其实就单核CUP而言,并没有办法实现真正意义上的并发执行,只是CPU快速地在多条线程之间调度,CPU调度线程的时间足够快,就造成了多线程并发执行的假象。并且就单核CPU而言多线程可以解决线程阻塞的问题,但是其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题。
- 系统中正在运行的每一个应用程序都是一个进程,每个进程系统都会分配给它独立的内存运行。也就是说,在iOS系统中中,每一个应用都是一个进程。
- 一个进程的所有任务都在线程中进行,因此每个进程至少要有一个线程,也就是主线程。那多线程其实就是一个进程开启多条线程,让所有任务并发执行。
- 多线程在一定意义上实现了进程内的资源共享,以及效率的提升。同时,在一定程度上相对独立,它是程序执行流的最小单元,是进程中的一个实体,是执行程序最基本的单元,有自己栈和寄存器。
今天要讲的内容
1、 iOS中的多线程
2、 iOS中的各种线程锁
3、 你不得不知的runloop
1.1 Pthreads
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。
我们来用Pthreads
创建一个线程去执行一个任务:
记得引入头文件`#import "pthread.h"`
-(void)pthreadsDoTask{
/*
pthread_t:线程指针
pthread_attr_t:线程属性
pthread_mutex_t:互斥对象
pthread_mutexattr_t:互斥属性对象
pthread_cond_t:条件变量
pthread_condattr_t:条件属性对象
pthread_key_t:线程数据键
pthread_rwlock_t:读写锁
//
pthread_create():创建一个线程
pthread_exit():终止当前线程
pthread_cancel():中断另外一个线程的运行
pthread_join():阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init():初始化线程的属性
pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate():获取脱离状态的属性
pthread_attr_destroy():删除线程的属性
pthread_kill():向线程发送一个信号
pthread_equal(): 对两个线程的线程标识号进行比较
pthread_detach(): 分离线程
pthread_self(): 查询线程自身线程标识号
//
*创建线程
int pthread_create(pthread_t _Nullable * _Nonnull __restrict, //指向新建线程标识符的指针
const pthread_attr_t * _Nullable __restrict, //设置线程属性。默认值NULL。
void * _Nullable (* _Nonnull)(void * _Nullable), //该线程运行函数的地址
void * _Nullable __restrict); //运行函数所需的参数
*返回值:
*若线程创建成功,则返回0
*若线程创建失败,则返回出错编号
*/
//
pthread_t thread = NULL;
NSString *params = @"Hello World";
int result = pthread_create(&thread, NULL, threadTask, (__bridge void *)(params));
result == 0 ? NSLog(@"creat thread success") : NSLog(@"creat thread failure");
//设置子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源
pthread_detach(thread);
}
void *threadTask(void *params) {
NSLog(@"%@ - %@", [NSThread currentThread], (__bridge NSString *)(params));
return NULL;
}
输出结果:
ThreadDemo[1197:143578] creat thread success
ThreadDemo[1197:143649] {number = 3, name = (null)} - Hello World
从打印结果来看,该任务是在新开辟的线程中执行的,但是感觉用起来超不友好,很多东西需要自己管理,单单是任务队列以及线程生命周期的管理就够你头疼的,那你写出的代码还能是艺术么!其实之所以抛弃这套API很少用,是因为我们有更好的选择:NSThread
。
1.2 NSThread
NSThread的基本使用比较简单,可以动态创建初始化NSThread对象,对其进行设置然后启动;也可以通过NSThread的静态方法快速创建并启动新线程;此外NSObject基类对象还提供了隐式快速创建NSThread线程的performSelector系列类别扩展工具方法;NSThread还提供了一些静态工具接口来控制当前线程以及获取当前线程的一些信息。
我们先来一看一下系统提供给我们的API自然就知道怎么用了
@interface NSThread : NSObject
//当前线程
@property (class, readonly, strong) NSThread *currentThread;
//使用类方法创建线程执行任务
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
//判断当前是否为多线程
+ (BOOL)isMultiThreaded;
//指定线程的线程参数,例如设置当前线程的断言处理器。
@property (readonly, retain) NSMutableDictionary *threadDictionary;
//当前线程暂停到某个时间
+ (void)sleepUntilDate:(NSDate *)date;
//当前线程暂停一段时间
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//退出当前线程
+ (void)exit;
//当前线程优先级
+ (double)threadPriority;
//设置当前线程优先级
+ (BOOL)setThreadPriority:(double)p;
//指定线程对象优先级 0.0~1.0,默认值为0.5
@property double threadPriority NS_AVAILABLE(10_6, 4_0);
//服务质量
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
//线程名称
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
//栈区大小
@property NSUInteger stackSize NS_AVAILABLE(10_5, 2_0);
//是否为主线程
@property (class, readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
//获取主线程
@property (class, readonly, strong) NSThread *mainThread NS_AVAILABLE(10_5, 2_0);
//初始化
- (instancetype)init NS_AVAILABLE(10_5, 2_0) NS_DESIGNATED_INITIALIZER;
//实例方法初始化,需要再调用start方法
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
//线程状态,正在执行
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);
//线程状态,正在完成
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);
//线程状态,已经取消
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
//取消,仅仅改变线程状态,并不能像exist一样真正的终止线程
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//开始
- (void)start NS_AVAILABLE(10_5, 2_0);
//线程需要执行的代码,一般写子类的时候会用到
- (void)main NS_AVAILABLE(10_5, 2_0);
@end
另外,还有一个NSObject的分类,瞅一眼:
@interface NSObject (NSThreadPerformAdditions)
//隐式的创建并启动线程,并在指定的线程(主线程或子线程)上执行方法。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end
下面以在一个UIViewController中为例展示NSThread的使用方法:
- (void)viewDidLoad {
[super viewDidLoad];
/** NSThread静态工具方法 **/
/* 1 是否开启了多线程 */
BOOL isMultiThreaded = [NSThread isMultiThreaded];
/* 2 获取当前线程 */
NSThread *currentThread = [NSThread currentThread];
/* 3 获取主线程 */
NSThread *mainThread = [NSThread mainThread];
NSLog(@"main thread");
/* 4 睡眠当前线程 */
/* 4.1 线程睡眠5s钟 */
[NSThread sleepForTimeInterval:5];
/* 4.2 线程睡眠到指定时间,效果同上 */
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
/* 5 退出当前线程,注意不要在主线程调用,防止主线程被kill掉 */
//[NSThread exit]; NSLog(@"main thread");
/** NSThread线程对象基本创建,target为入口函数所在的对象,selector为线程入口函数 **/
/* 1 线程实例对象创建与设置 */
NSThread *newThread= [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
/* 设置线程优先级threadPriority(0~1.0),即将被抛弃,将使用qualityOfService代替 */
newThread.threadPriority = 1.0;
newThread.qualityOfService = NSQualityOfServiceUserInteractive;
/* 开启线程 */
[newThread start];
/* 2 静态方法快速创建并开启新线程 */
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
[NSThread detachNewThreadWithBlock:^{
NSLog(@"block run...");
}];
/** NSObejct基类隐式创建线程的一些静态工具方法 **/
/* 1 在当前线程上执行方法,延迟2s */
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
/* 2 在指定线程上执行方法,不等待当前线程 */
[self performSelector:@selector(run) onThread:newThread withObject:nil waitUntilDone:NO];
/* 3 后台异步执行函数 */
[self performSelectorInBackground:@selector(run) withObject:nil];
/* 4 在主线程上执行函数 */
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
}
- (void)run {
NSLog(@"run...");
}
1.2.1 performSelecor补充
基础用法
performSelecor响应了OC语言的动态性:延迟到运行时才绑定方法。当我们在使用以下方法时:
[obj performSelector:@selector(play)];
[obj performSelector:@selector(play:) withObject:@"张三"];
[obj performSelector:@selector(play:with:) withObject:@"张三" withObject:@"李四"];
编译阶段并不会去检查方法是否有效存在,只会给出警告
如果要执行的方法名也是动态不确定的一个参数
编译器也只会提示说因为当前方法名未知可能会引起内存泄露相关问题:
PerformSelector may cause a leak because its selector is unknown
延迟执行
[obj performSelector:@selector(play) withObject:@"张三" afterDelay:4.f];
该方法将延迟4秒后再执行play方法。其实说到对时间方面的处理在项目中经常用到的是NSTimer:当一个NSTimer注册到Runloop后,Runloop会重复的在相应的时间点注册事件,当然Runloop为了节省资源并不会在准确的时间点触发事件。
而performSelector:withObject:afterDelay:其实就是在内部创建了一个NSTimer,然后会添加到当前线程的Runloop中。所以当该方法添加到子线程中时,需要格外的注意两个地方:
① 在子线程中执行会不会调用test方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self performSelector:@selector(test) withObject:nil afterDelay:2];
});
会发现test方法并没有被调用,因为子线程中的runloop默认是没有启动的状态。使用run方法开启当前线程的runloop,但是一定要注意run方法和执行该延迟方法的顺序。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(test) withObject:nil afterDelay:2];
});
会发现即便添加了run方法,但是test方法还是没有被调用,在最后打印当前线程的runloop,会发现:
timers = {type = mutable-small, count = 1, values = (
0 : {valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 544280547 (1.98647892 @ 3795501066754), callout = (Delayed Perform) lZLearningFromInterviewController test (0x105ea0d9c / 0x104b2e2c0) (), context = }
子线程的runloop中确实添加了一个CFRunLoopTimer的事件,但是到最后都不会被执行。
将run方法和performSelector延迟方法调换顺序后运行:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self performSelector:@selector(test) withObject:nil afterDelay:2];
[[NSRunLoop currentRunLoop] run];
});
此时test方法会被调用,分别打印执行完performSelecor和run方法之后,发现在执行完performSelector方法后该timer事件会被添加到子线程的runloop中:
timers = {type = mutable-small, count = 1, values = (
0 : {valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 544280800 (1.98171604 @ 4048676578329), callout = (Delayed Perform) lZLearningFromInterviewController test (0x10e88fd9c / 0x1
但是当执行完run方法之后,runloop中的timer事件已经是执行完的状态:
timers = {type = mutable-small, count = 0, values = ()},
所以在子线程中两者的顺序必须是先执行performSelector延迟方法之后再执行run方法。因为run方法只是尝试想要开启当前线程中的runloop,但是如果该线程中并没有任何事件(source、timer、observer)的话,并不会成功的开启。
② test方法中执行的线程
[self performSelector:@selector(test) withObject:nil afterDelay:2];
如果在子线程中调用该performSelector延迟方法,会发现调用该延迟方法的子线程和test方法中执行的子线程是同一个,也就是说:
对于该performSelector延迟方法而言,如果在主线程中调用,那么test方法也是在主线程中执行;如果是在子线程中调用,那么test也会在该子线程中执行。
在回答完延迟方法之后,会将该方法和performSelector:withObject:作对比,那么performSelector:withObject:在不添加到子线程的Runloop中时是否能执行?
我当时想的是,performSelector:withObject:方法和延迟方法类似,只不过是马上执行而已,所以也需要添加到子线程的RunLoop中。
这么想是错的,performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行。
所以能直接使用NSThread的三个方法:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[NSThread detachNewThreadSelector:@selector(test) toTarget:self withObject:nil];
[NSThread detachNewThreadWithBlock:^{
NSLog(@"block中的线程 ---- %@",[NSThread currentThread]);
}];
performSelector如何进行多值传输?
问题一听马上就能回答使用NSArray或者NSDictionary或者自定义Model的形式,但是我查到了一个很妙的方法:
因为在OC中调用一个方法实际上就是发送消息objc_msgSend:
{
NSNumber *age = [NSNumber numberWithInt:20];
NSString *name = @"李周";
NSString *gender = @"女";
NSArray *friends = @[@"谢华华",@"亚呼呼"];
SEL selector = NSSelectorFromString(@"getAge:name:gender:friends:");
NSArray *array = @[age,name,gender,friends];
((void(*)(id,SEL,NSNumber*,NSString*,NSString*,NSArray*)) objc_msgSend)(self,selector,age,name,gender,friends);
}
- (void)getAge:(NSNumber *)age name:(NSString *)name gender:(NSString *)gender friends:(NSArray *)friends
{
NSLog(@"%d----%@---%@---%@",[age intValue],name,gender,friends[0]);
}
导入#import
第二种方法其实也是以NSArray的形式传值,然后创建NSInvocation的方式,将参数一一绑定。
-(id)performSelector:(SEL)aSelector withObject:(NSArray *)object
{
//获得方法签名
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector];
if (signature == nil) {
return nil;
}
//使用NSInvocation进行参数的封装
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = aSelector;
//减去 self _cmd
NSInteger paramtersCount = signature.numberOfArguments - 2;
paramtersCount = MIN(object.count, paramtersCount);
for (int i = 0; i < paramtersCount; i++) {
id obj = object[i];
if ([obj isKindOfClass:[NSNull class]]) continue;
[invocation setArgument:&obj atIndex:i+2];
}
[invocation invoke];
id returnValue = nil;
if (signature.methodReturnLength > 0) { //如果有返回值的话,才需要去获得返回值
[invocation getReturnValue:&returnValue];
}
return returnValue;
}
NSNumber *age = [NSNumber numberWithInt:20];
NSString *name = @"李周";
NSString *gender = @"女";
NSArray *friends = @[@"谢华华",@"亚呼呼"];
SEL selector = NSSelectorFromString(@"getAge:name:gender:friends:");
NSArray *array = @[age,name,gender,friends];
[self performSelector:selector withObject:array];
1.3 GCD
GCD,全名Grand Central Dispatch
,大中枢派发,是基于C语言的一套多线程开发API,是目前苹果官方推荐的多线程开发方式。总体来说,他解决我提到的上面直接操作线程带来的难题,它自动帮你管理了线程的生命周期以及任务的执行规则。下面我们会频繁的说道一个词,那就是任务
,说白了,任务
其实就是你要执行的那段代码
。
任务管理方式——队列
当我们要管理多个任务时,线程开发给我们带来了一定的技术难度,或者说不方便性,GCD给出了我们统一管理任务的方式,那就是队列。我们来看一下iOS
多线程操作中的队列:(⚠️不管是串行还是并行,队列都是按照FIFO的原则依次触发任务)
两个通用队列:
- 串行队列:所有任务会在一条线程中执行(有可能是当前线程也有可能是新开辟的线程),并且一个任务执行完毕后,才开始执行下一个任务。(等待完成)
- 并行队列:可以开启多条线程并行执行任务(但不一定会开启新的线程),并且当一个任务放到指定线程开始执行时,下一个任务就可以开始执行了。(等待发生)
两个特殊队列:
- 主队列:系统为我们创建好的一个串行队列,牛逼之处在于它管理必须在主线程中执行的任务,属于有劳保的。
- 全局队列:系统为我们创建好的一个并行队列,使用起来与我们自己创建的并行队列无本质差别。
任务执行方式
说完队列,相应的,任务除了管理,还得执行。并且在GCD中并不能直接开辟线程执行任务,所以在任务加入队列之后,GCD给出了两种执行方式——同步执行(sync)和异步执行(async)。
- 同步执行:在当前线程执行任务,不会开辟新的线程。必须等到Block函数执行完毕后,dispatch函数才会返回。
- 异步执行:可以在新的线程中执行任务,但不一定会开辟新的线程。dispatch函数会立即返回, 然后Block在后台异步执行。
任务队列组合方式
1. 线程死锁
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1========%@",[NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2========%@",[NSThread currentThread]);
});
NSLog(@"3========%@",[NSThread currentThread]);
}
打印结果:
ThreadDemo[5615:874679] 1========{number = 1, name = main}
我们还是得分析一下为什么会死锁:
我们先做一个定义:- (void)viewDidLoad{} ---> 任务A,GCD同步任务 --->任务B。
总而言之呢,大概是这样的,首先,任务A在主队列,并且已经开始执行,在主线程打印出1===... ...
,然后这时任务B被加入到主队列中,并且同步执行,这尼玛事都大了,系统说,同步执行啊,那我不开新的线程了,任务B说我要等我里面的Block函数执行完成,要不我就不返回,但是主队列说了,玩蛋去,我是串行的,你得等A执行完才能轮到你,不能坏了规矩,同时,任务B作为任务A的内部函数,必须等任务B执行完函数返回才能执行下一个任务。那就造成了,任务A等待任务B完成才能继续执行,但作为串行队列的主队列又不能让任务B在任务A未完成之前开始执行,所以任务A等着任务B完成,任务B等着任务A完成,等待,永久的等待。所以就死锁了。
2. 这样不死锁
不如就写个最简单的:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1========%@",[NSThread currentThread]);
NSLog(@"2========%@",[NSThread currentThread]);
NSLog(@"3========%@",[NSThread currentThread]);
}
打印结果:
ThreadDemo[5803:939324] 1========{number = 1, name = main}
ThreadDemo[5803:939324] 2========{number = 1, name = main}
ThreadDemo[5803:939324] 3========{number = 1, name = main}
其实这里有一个误区,那就是任务在主线程顺序执行就是主队列。其实一点关系都没有,如果当前在主线程,同步执行任务,不管在什么队列任务都是顺序执行。把所有任务都以异步执行的方式加入到主队列中,你会发现它们也是顺序执行的。
3. 我们改一下
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1========%@",[NSThread currentThread]);
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"2========%@",[NSThread currentThread]);
});
NSLog(@"3========%@",[NSThread currentThread]);
}
打印结果:
ThreadDemo[5830:947858] 1========{number = 1, name = main}
ThreadDemo[5830:947858] 2========{number = 1, name = main}
ThreadDemo[5830:947858] 3========{number = 1, name = main}
你发现正常执行了,并且是顺序执行的,和上诉情况一样,任务A在主队列中,但是任务B加入到了全局队列,这时候,任务A和任务B没有队列的约束,所以任务B就先执行,执行完毕之后函数返回,任务A接着执行。
4. 我们再改一下
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1========%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"2========%@",[NSThread currentThread]);
});
NSLog(@"3========%@",[NSThread currentThread]);
}
打印结果:
ThreadDemo[5911:962470] 1========{number = 1, name = main}
ThreadDemo[5911:962470] 3========{number = 1, name = main}
ThreadDemo[5911:962470] 2========{number = 1, name = main}
发现不是顺序打印了,而且也不会死锁,明明都是加到主队列里了啊,其实当任务A在执行时,任务B加入到了主队列,注意,是异步执行,所以dispatch函数不会等到Block执行完成才返回,dispatch函数返回后,那任务A可以继续执行,Block任务我们可以认为在下一帧顺序加入队列,并且默认无限下一帧执行。这就是为什么你看到2===... ...
是最后输出的了。(⚠️一个函数的有多个内部函数异步执行时,不会造成死锁的同时,任务A执行完毕后,这些异步执行的内部函数会顺序执行)。
队列与执行方式的搭配
//串行队列
self.serialQueue = dispatch_queue_create("serialQueue.ys.com", DISPATCH_QUEUE_SERIAL);
//并行队列
self.concurrentQueue = dispatch_queue_create("concurrentQueue.ys.com", DISPATCH_QUEUE_CONCURRENT);
1. 串行队列 + 同步执行
-(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]);
}
打印结果:
ThreadDemo[6735:1064390] 1========{number = 1, name = main}
ThreadDemo[6735:1064390] 2========{number = 1, name = main}
ThreadDemo[6735:1064390] 3========{number = 1, name = main}
ThreadDemo[6735:1064390] 4========{number = 1, name = main}
全部都在当前线程顺序执行,也就是说,同步执行不具备开辟新线程的能力。
2. 串行队列 + 异步执行
-(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]);
}
打印结果:
ThreadDemo[6774:1073235] 4========{number = 1, name = main}
ThreadDemo[6774:1073290] 1========{number = 3, name = (null)}
ThreadDemo[6774:1073290] 2========{number = 3, name = (null)}
ThreadDemo[6774:1073290] 3========{number = 3, name = (null)}
先打印了4,然后顺序在子线程中打印1,2,3。说明异步执行具有开辟新线程的能力,并且串行队列必须等到前一个任务执行完才能开始执行下一个任务,同时,异步执行会使内部函数率先返回,不会与正在执行的外部函数发生死锁。
3. 并行队列 + 同步执行
-(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]);
}
运行结果:
ThreadDemo[7012:1113594] 1========{number = 1, name = main}
ThreadDemo[7012:1113594] 2========{number = 1, name = main}
ThreadDemo[7012:1113594] 3========{number = 1, name = main}
ThreadDemo[7012:1113594] 4========{number = 1, name = main}
未开启新的线程执行任务,并且Block函数执行完成后dispatch函数才会返回,才能继续向下执行,所以我们看到的结果是顺序打印的。
4. 并行队列 + 异步执行
-(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]);
}
打印结果:
ThreadDemo[7042:1117492] 1========{number = 3, name = (null)}
ThreadDemo[7042:1117491] 3========{number = 5, name = (null)}
ThreadDemo[7042:1117451] 4========{number = 1, name = main}
ThreadDemo[7042:1117494] 2========{number = 4, name = (null)}
开辟了多个线程,触发任务的时机是顺序的,但是我们看到完成任务的时间却是随机的,这取决于CPU对于不同线程的调度分配,但是,线程不是无条件无限开辟的,当任务量足够大时,线程是会重复利用的。
总结一下
1. 对于单核CPU来说,不存在真正意义上的并行,所以,多线程执行任务,其实也只是一个人在干活,CPU的调度决定了非等待任务的执行速率,同时对于非等待任务,多线程并没有真正意义提高效率。
2. 线程可以简单的认为就是一段代码+运行时数据。
3. 同步执行会在当前线程执行任务,不具备开辟线程的能力或者说没有必要开辟新的线程。并且,同步执行必须等到Block函数执行完毕,dispatch函数才会返回,从而阻塞同一串行队列中外部方法的执行。
4. 异步执行dispatch函数会直接返回,Block函数我们可以认为它会在下一帧加入队列,并根据所在队列目前的任务情况无限下一帧执行,从而不会阻塞当前外部任务的执行。同时,只有异步执行才有开辟新线程的必要,但是异步执行不一定会开辟新线程。
5. 只要是队列,肯定是FIFO(先进先出),但是谁先执行完要看第1条。
6. 只要是串行队列,肯定要等上一个任务执行完成,才能开始下一个任务。但是并行队列当上一个任务开始执行后,下一个任务就可以开始执行。
7. 想要开辟新线程必须让任务在异步执行,想要开辟多个线程,只有让任务在并行队列中异步执行才可以。执行方式和队列类型多层组合在一定程度上能够实现对于代码执行顺序的调度。
8. 同步+串行:未开辟新线程,串行执行任务;同步+并行:未开辟新线程,串行执行任务;异步+串行:新开辟一条线程,串行执行任务;异步+并行:开辟多条新线程,并行执行任务;在主线程中同步使用主队列执行任务,会造成死锁。
9. 对于多核CPU来说,线程数量也不能无限开辟,线程的开辟同样会消耗资源,过多线程同时处理任务并不是想象中的人多力量大。
比喻:
任务的管理方式——队列,串行队列和并行队列就像是人以什么规则打电话,排队一个等一个去,还是抢着去;
任务的执行方式——同步或异步执行,就像提供当前一个电话机,还是可以申请新的电话机。
而多线程的运作就等于是这些人去打电话。
同步执行的时候不能开辟新的线程,异步执行的时候可以开辟新的线程,但不一定开辟。
GCD其他函数用法
1. dispatch_after
该函数用于任务延时执行,其中参数dispatch_time_t
代表延时时长,dispatch_queue_t
代表使用哪个队列。如果队列未主队列,那么任务在主线程执行,如果队列为全局队列或者自己创建的队列,那么任务在子线程执行,代码如下:
-(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]);
});
}
打印结果:
ThreadDemo[1508:499647] main_{number = 1, name = main}
ThreadDemo[1508:499697] global_{number = 3, name = (null)}
ThreadDemo[1508:499697] custom_{number = 3, name = (null)}
2. dispatch_once
保证函数在整个生命周期内只会执行一次,看代码。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"%@",[NSThread currentThread]);
});
}
打印结果:
ThreadDemo[1524:509261] {number = 1, name = main}
无论你怎么疯狂的点击,在第一次打印之后,输出台便岿然不动。
3. dispatch_group_async & dispatch_group_notify
-(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
栅栏函数,使用此方法创建的任务,会查找当前队列中有没有其他任务要执行,如果有,则等待已有任务执行完毕后再执行,同时,在此任务之后进入队列的任务,需要等待此任务执行完成后,才能执行。看代码(⚠️ 这里并发队列必须是自己创建的。如果选择全局队列,这个函数和dispatch_async将会没有差别。)
-(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");
});
}
运行结果:
ThreadDemo[1816:673351] 任务3
ThreadDemo[1816:673353] 任务1
ThreadDemo[1816:673350] 任务2
ThreadDemo[1816:673370] 任务4
下面我们打开第一句注释:
-(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");
});
}
打印结果:
ThreadDemo[1833:678739] 任务2
ThreadDemo[1833:678740] 任务1
ThreadDemo[1833:678740] 任务barrier
ThreadDemo[1833:678740] 任务3
ThreadDemo[1833:678739] 任务4
再打开第二个和第三个注释,如下:
-(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");
});
}
运行结果:
ThreadDemo[1853:692434] 任务1
ThreadDemo[1853:692421] 任务2
ThreadDemo[1853:692387] big
ThreadDemo[1853:692421] 任务barrier
ThreadDemo[1853:692387] apple
ThreadDemo[1853:692421] 任务3
ThreadDemo[1853:692434] 任务4
我们换一下函数:
-(void)GCDbarrier{
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务1");
});
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务2");
});
dispatch_barrier_sync(self.concurrentQueue, ^{
NSLog(@"任务barrier");
});
NSLog(@"big");
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务3");
});
NSLog(@"apple");
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务4");
});
}
打印结果:
ThreadDemo[1874:711841] 任务1
ThreadDemo[1874:711828] 任务2
ThreadDemo[1874:711793] 任务barrier
ThreadDemo[1874:711793] big
ThreadDemo[1874:711793] apple
ThreadDemo[1874:711828] 任务3
ThreadDemo[1874:711841] 任务4
发现了吗?这两个函数对于队列的栅栏作用是一样的,但是对于该函数相对于其他内部函数遵循了最开始说到的同步和异步的规则。
5. dispatch_apply
该函数用于重复执行某个任务,如果任务队列是并行队列,重复执行的任务会并发执行,如果任务队列为串行队列,则任务会顺序执行,需要注意的是,该函数为同步函数,要防止线程阻塞和死锁哦。
串行队列:
-(void)GCDApply{
//重复执行
dispatch_apply(5, self.serialQueue, ^(size_t i) {
NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
});
}
运行结果:
ThreadDemo[1446:158101] 第0次_{number = 1, name = main}
ThreadDemo[1446:158101] 第1次_{number = 1, name = main}
ThreadDemo[1446:158101] 第2次_{number = 1, name = main}
ThreadDemo[1446:158101] 第3次_{number = 1, name = main}
ThreadDemo[1446:158101] 第4次_{number = 1, name = main}
并行队列:
-(void)GCDApply{
//重复执行
dispatch_apply(5, self.concurrentQueue, ^(size_t i) {
NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
});
}
运行结果:
ThreadDemo[1461:160567] 第2次_{number = 4, name = (null)}
ThreadDemo[1461:160534] 第0次_{number = 1, name = main}
ThreadDemo[1461:160566] 第3次_{number = 5, name = (null)}
ThreadDemo[1461:160569] 第1次_{number = 3, name = (null)}
ThreadDemo[1461:160567] 第4次_{number = 4, name = (null)}
死锁:
-(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
通常配对使用。
-(void)GCDSemaphore{
//
//dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_apply(5, self.concurrentQueue, ^(size_t i) {
//dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(self.concurrentQueue, ^{
NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
//dispatch_semaphore_signal(semaphore);
});
});
}
ThreadDemo[1970:506692] 第0次_{number = 3, name = (null)}
ThreadDemo[1970:506711] 第1次_{number = 4, name = (null)}
ThreadDemo[1970:506713] 第2次_{number = 5, name = (null)}
ThreadDemo[1970:506691] 第3次_{number = 6, name = (null)}
ThreadDemo[1970:506694] 第4次_{number = 7, name = (null)}
-(void)GCDSemaphore{
//
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_apply(5, self.concurrentQueue, ^(size_t i) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(self.concurrentQueue, ^{
NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
dispatch_semaphore_signal(semaphore);
});
});
}
ThreadDemo[2020:513651] 第0次_{number = 3, name = (null)}
ThreadDemo[2020:513651] 第1次_{number = 3, name = (null)}
ThreadDemo[2020:513651] 第2次_{number = 3, name = (null)}
ThreadDemo[2020:513651] 第3次_{number = 3, name = (null)}
ThreadDemo[2020:513651] 第4次_{number = 3, name = (null)}
信号量是控制任务执行的重要条件,当信号量为0时,所有任务等待,信号量越大,允许可并行执行的任务数量越多。
1.4 NSOperation
NSOperation
以及NSOperationQueue
是苹果对于GCD的封装,其中NSOperation
其实就是我们上面所说的任务,但是这个类不能直接使用,我们要用他的两个子类,NSBlockOperation
和NSInvocationOperation
,而NSOperationQueue
呢,其实就是类似于GCD中的队列,用于管理你加入到其中的任务。
NSOperation提供了关于任务的执行,取消,以及随时获取任务的状态,添加任务依赖以及优先级等方法和属性,相对于GCD提供的方法来说,更直观,更方便,并且提供了更多的控制接口。(很多时候,苹果设计的架构是很棒的,不要只是在乎他实现了什么,可能你学到的东西会更多),有几个方法和属性我们了解一下
@interface NSOperation : NSObject {
@private
id _private;
int32_t _private1;
#if __LP64__
int32_t _private1b;
#endif
}
- (void)start;//启动任务 默认在当前线程执行
- (void)main;//自定义NSOperation,写一个子类,重写这个方法,在这个方法里面添加需要执行的操作。
@property (readonly, getter=isCancelled) BOOL cancelled;//是否已经取消,只读
- (void)cancel;//取消任务
@property (readonly, getter=isExecuting) BOOL executing;//正在执行,只读
@property (readonly, getter=isFinished) BOOL finished;//执行结束,只读
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);//是否并发,只读
@property (readonly, getter=isReady) BOOL ready;//准备执行
- (void)addDependency:(NSOperation *)op;//添加依赖
- (void)removeDependency:(NSOperation *)op;//移除依赖
@property (readonly, copy) NSArray *dependencies;//所有依赖关系,只读
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};//系统提供的优先级关系枚举
@property NSOperationQueuePriority queuePriority;//执行优先级
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);//任务执行完成之后的回调
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);//阻塞当前线程,等到某个operation执行完毕。
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);//已废弃,用qualityOfService替代。
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);//服务质量,一个高质量的服务就意味着更多的资源得以提供来更快的完成操作。
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);//任务名称
@end
然而NSOperation
本身是个抽象类,不能直接使用,我们有三种方式赋予它新的生命,就是下面这三个东西。
NSOperation自定义子类
这是我要说的第一个任务类型,我们可以自定义继承于NSOperation
的子类,并重写父类提供的方法,实现一波具有特殊意义的任务。比如我们去下载一个图片:
.h
#import
@protocol QPImageDownLoadOperationDelegate
-(void)QPImageDownLoadFinished:(UIImage*)image;
@end
@interface QPImageDownLoadOperation : NSOperation
-(id)initOperationWithUrl:(NSURL*)imageUrl delegate:(id)delegate;
@end
.m
#import "QPImageDownLoadOperation.h"
@implementation QPImageDownLoadOperation{
NSURL *_imageUrl;
id _delegate;
}
-(id)initOperationWithUrl:(NSURL*)imageUrl delegate:(id)delegate{
if (self == [super init]) {
_imageUrl = imageUrl;
_delegate = delegate;
}
return self;
}
-(void)main{
@autoreleasepool {
UIImage *image = [self imageWithUrl:_imageUrl];
if (_delegate && [_delegate respondsToSelector:@selector(QPImageDownLoadFinished:)]) {
[_delegate QPImageDownLoadFinished:image];
}
}
}
-(UIImage*)imageWithUrl:(NSURL*)url{
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
return image;
}
@end
然后调用:
-(void)QPDownLoadImageOperationRun{
QPImageDownLoadOperation *ysOper = [[QPImageDownLoadOperation alloc] initOperationWithUrl:[NSURL URLWithString:@"http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg"] delegate:self];
[ysOper start];
}
-(void)QPImageDownLoadFinished:(UIImage *)image{
NSLog(@"%@",image);
}
运行打印结果:
ThreadDemo[4141:1100329] , {700, 1050}
NSBlockOperation
第二个,就是系统提供的NSOperation
的子类NSBlockOperation
,我们看一下他提供的API:
@interface NSBlockOperation : NSOperation {
@private
id _private2;
void *_reserved2;
}
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
- (void)addExecutionBlock:(void (^)(void))block;
@property (readonly, copy) NSArray *executionBlocks;
@end
很简单,就这几个,我们就用它实现一个任务:
-(void)NSBlockOperationRun{
NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperationRun_%@_%@",[NSOperationQueue currentQueue],[NSThread currentThread]);
}];
[blockOper start];
}
运行结果:
ThreadDemo[4313:1121900] NSBlockOperationRun_{name = 'NSOperationQueue Main Queue'}_{number = 1, name = main}
我们发现这个任务是在当前线程顺序执行的,我们发现还有一个方法addExecutionBlock:
试一下:
-(void)NSBlockOperationRun{
NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperationRun_1_%@",[NSThread currentThread]);
}];
[blockOper addExecutionBlock:^{
NSLog(@"NSBlockOperationRun_2_%@",[NSThread currentThread]);
}];
[blockOper addExecutionBlock:^{
NSLog(@"NSBlockOperationRun_3_%@",[NSThread currentThread]);
}];
[blockOper addExecutionBlock:^{
NSLog(@"NSBlockOperationRun_4_%@",[NSThread currentThread]);
}];
[blockOper start];
}
打印结果:
ThreadDemo[4516:1169835] NSBlockOperationRun_1_{number = 1, name = main}
ThreadDemo[4516:1169875] NSBlockOperationRun_3_{number = 4, name = (null)}
ThreadDemo[4516:1169877] NSBlockOperationRun_4_{number = 5, name = (null)}
ThreadDemo[4516:1169893] NSBlockOperationRun_2_{number = 3, name = (null)}
从打印结果来看,这个4个任务是异步并发执行的,开辟了多条线程。
NSInvocationOperation
第三个,就是它了,同样也是系统提供给我们的一个任务类,基于一个target对象以及一个selector来创建任务,具体代码:
-(void)NSInvocationOperationRun{
NSInvocationOperation *invocationOper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperSel) object:nil];
[invocationOper start];
}
-(void)invocationOperSel{
NSLog(@"NSInvocationOperationRun_%@",[NSThread currentThread]);
}
运行结果:
ThreadDemo[4538:1173118] NSInvocationOperationRun_{number = 1, name = main}
运行结果与NSBlockOperation
单个block函数的执行方式相同,同步顺序执行。的确系统的封装给予我们关于任务更直观的东西,但是对于多个任务的控制机制并不完善。
NSOperationQueue
上面说道我们创建的NSOperation
任务对象可以通过start
方法来执行,同样我们可以把这个任务对象添加到一个NSOperationQueue对象中去执行,先看一下系统的API:
@interface NSOperationQueue : NSObject {
@private
id _private;
void *_reserved;
}
- (void)addOperation:(NSOperation *)op;//添加任务
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);//添加一组任务
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);//添加一个block形式的任务
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;//队列中所有的任务数组
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);//队列中的任务数
@property NSInteger maxConcurrentOperationCount;//最大并发数
@property (getter=isSuspended) BOOL suspended;//暂停
@property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);//名称
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);//服务质量,一个高质量的服务就意味着更多的资源得以提供来更快的完成操作。
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);
- (void)cancelAllOperations;//取消队列中的所有任务
- (void)waitUntilAllOperationsAreFinished;//阻塞当前线程,等到队列中的任务全部执行完毕。
#if FOUNDATION_SWIFT_SDK_EPOCH_AT_LEAST(8)
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0);//获取当前队列
@property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);//获取主队列
#endif
@end
-(void)NSOperationQueueRun{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSInvocationOperation *invocationOper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperSel) object:nil];
[queue addOperation:invocationOper];
NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperationRun_%@",[NSThread currentThread]);
}];
[queue addOperation:blockOper];
[queue addOperationWithBlock:^{
NSLog(@"QUEUEBlockOperationRun_%@",[NSThread currentThread]);
}];
}
打印结果:
ThreadDemo[4761:1205689] NSBlockOperationRun_{number = 4, name = (null)}
ThreadDemo[4761:1205691] NSInvocationOperationRun_{number = 3, name = (null)}
ThreadDemo[4761:1205706] QUEUEBlockOperationRun_{number = 5, name = (null)}
我们发现,加入队列之后不用调用任务的start
方法,队列会帮你管理任务的执行情况。上述执行结果说明这些任务在队列中为并发执行的。
添加依赖关系
-(void)NSOperationQueueRun{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOper_1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 1000; i++) {
NSLog(@"blockOper_1_%@_%@",@(i),[NSThread currentThread]);
}
}];
NSBlockOperation *blockOper_2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 1000; i++) {
NSLog(@"blockOper_2_%@_%@",@(i),[NSThread currentThread]);
}
}];
[blockOper_1 addDependency:blockOper_2];
[queue addOperation:blockOper_1];
[queue addOperation:blockOper_2];
}
打印结果:
ThreadDemo[5066:1233824] blockOper_2_0_{number = 3, name = (null)}
ThreadDemo[5066:1233824] blockOper_2_1_{number = 3, name = (null)}
ThreadDemo[5066:1233824] blockOper_2_2_{number = 3, name = (null)}
ThreadDemo[5066:1233824] blockOper_2_3_{number = 3, name = (null)}
... ...
ThreadDemo[5066:1233824] blockOper_2_999_{number = 3, name = (null)}
ThreadDemo[5066:1233822] blockOper_1_0_{number = 4, name = (null)}
... ...
ThreadDemo[5066:1233822] blockOper_1_997_{number = 4, name = (null)}
ThreadDemo[5066:1233822] blockOper_1_998_{number = 4, name = (null)}
ThreadDemo[5066:1233822] blockOper_1_999_{number = 4, name = (null)}
通过打印结果我们可以看到,添加依赖之后,依赖任务必须等待被依赖任务执行完毕之后才会开始执行。⚠️,就算依赖任务的优先级再高,也是被依赖任务先执行,同时,和优先级不同,依赖关系不受队列的局限,只要是我依赖于你,那你必须先执行完,我才执行。
队列的最大并发数
就是说,这个队列最多可以有多少任务同时执行,或者说最多开辟多少条线程,如果设置为1,那就一次只能执行一个任务,但是,不要以为这和GCD的串行队列一样,就算最大并发数为1,队列任务的执行顺序依然取决于很多因素。
2. 线程锁
锁 是什么意思?
我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁 应运而生。
2.1 OSSpinLock 自旋锁
#import
__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 准备上锁");
OSSpinLockLock(&oslock);
sleep(4);
NSLog(@"线程1");
OSSpinLockUnlock(&oslock);
NSLog(@"线程1 解锁成功");
NSLog(@"--------------------------------------------------------");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 准备上锁");
OSSpinLockLock(&oslock);
NSLog(@"线程2");
OSSpinLockUnlock(&oslock);
NSLog(@"线程2 解锁成功");
});
我们来修改一下代码:
__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
......
//OSSpinLockUnlock(&oslock);
......
在 OSSpinLock1
图中可以发现:当我们锁住线程1时,在同时锁住线程2的情况下,线程2会一直等待(自旋锁不会让等待的进入睡眠状态),直到线程1的任务执行完且解锁完毕,线程2会立即执行;而在 OSSpinLock2
图中,因为我们注释掉了线程1中的解锁代码,会绕过线程1,直到调用了线程2的解锁方法才会继续执行线程1中的任务,正常情况下,lock
和unlock
最好成对出现。
OS_SPINLOCK_INIT: 默认值为 0
,在 locked
状态时就会大于 0
,unlocked
状态下为 0
OSSpinLockLock(&oslock):上锁,参数为 OSSpinLock
地址
OSSpinLockUnlock(&oslock):解锁,参数为 OSSpinLock
地址
OSSpinLockTry(&oslock):尝试加锁,可以加锁则立即加锁并返回 YES
,反之返回 NO
这里顺便提一下trylock
和lock
使用场景:
当前线程锁失败,也可以继续其它任务,用 trylock 合适
当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock
2.2 dispatch_semaphore 信号量
dispatch_semaphore_t signal = dispatch_semaphore_create(1); //传入值必须 >=0, 若传入为0则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 等待ing");
dispatch_semaphore_wait(signal, overTime); //signal 值 -1
NSLog(@"线程1");
dispatch_semaphore_signal(signal); //signal 值 +1
NSLog(@"线程1 发送信号");
NSLog(@"--------------------------------------------------------");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 等待ing");
dispatch_semaphore_wait(signal, overTime);
NSLog(@"线程2");
dispatch_semaphore_signal(signal);
NSLog(@"线程2 发送信号");
});
dispatch_semaphore_create(1): 传入值必须 >=0
, 若传入为 0
则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_semaphore_wait(signal, overTime):可以理解为 lock
,会使得 signal
值 -1
dispatch_semaphore_signal(signal):可以理解为 unlock
,会使得 signal
值 +1
关于信号量,我们可以用停车来比喻:
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值(signal)就相当于剩余车位的数目,dispatch_semaphore_wait
函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次 dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait 剩余车位就减少一个;当剩余车位为 0 时,再来车(即调用 dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
运行结果:
可以发现,因为我们初始化信号量的时候是大于 0
的,所以并没有阻塞线程,而是直接执行了 线程1 线程2。
我们把 信号量初始值改为 0
:
可以看到这时候我们设置的 overTime
生效了。
2.3 pthread_mutex 互斥锁
ibireme 在《不再安全的 OSSpinLock》这篇文章中提到性能最好的 OSSpinLock
已经不再是线程安全的并把自己开源项目中的 OSSpinLock
都替换成了 pthread_mutex
。
特意去看了下源码,总结了下常见用法:
#import
static pthread_mutex_t pLock;
pthread_mutex_init(&pLock, NULL);
//1.线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 准备上锁");
pthread_mutex_lock(&pLock);
sleep(3);
NSLog(@"线程1");
pthread_mutex_unlock(&pLock);
});
//1.线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 准备上锁");
pthread_mutex_lock(&pLock);
NSLog(@"线程2");
pthread_mutex_unlock(&pLock);
});
pthread_mutex 中也有个pthread_mutex_trylock(&pLock)
,和上面提到的 OSSpinLockTry(&oslock)
区别在于,前者可以加锁时返回的是 0
,否则返回一个错误提示码;后者返回的 YES
和NO
pthread_mutex(recursive) 递归锁
经过上面几种例子,我们可以发现:加锁后只能有一个线程访问该对象,后面的线程需要排队,并且 lock 和 unlock 是对应出现的,同一线程多次 lock 是不允许的,而递归锁允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。
例子:
static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
pthread_mutex_init(&pLock, &attr);
pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用
//1.线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
pthread_mutex_lock(&pLock);
if (value > 0) {
NSLog(@"value: %d", value);
RecursiveBlock(value - 1);
}
pthread_mutex_unlock(&pLock);
};
RecursiveBlock(5);
});
上面的代码如果我们用 pthread_mutex_init(&pLock, NULL)
初始化会出现死锁的情况,递归锁能很好的避免这种情况的死锁;
2.4 NSLock 互斥锁
NSLock是对mutex普通锁的封装,是Foundation框架中以对象形式暴露给开发者的一种锁,(Foundation框架同时提供了NSConditionLock
,NSRecursiveLock
,NSCondition
)
NSLock API 很少也很简单:
lock、unlock:不多做解释,和上面一样
trylock:能加锁返回 YES 并执行加锁操作,相当于 lock,反之返回 NO
** lockBeforeDate:这个方法表示会在传入的时间内尝试加锁,若能加锁则执行加锁**操作并返回 YES,反之返回 NO
NSLock *lock = [NSLock new];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 尝试加速ing...");
[lock lock];
sleep(3);//睡眠5秒
NSLog(@"线程1");
[lock unlock];
NSLog(@"线程1解锁成功");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 尝试加速ing...");
BOOL x = [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
if (x) {
NSLog(@"线程2");
[lock unlock];
}else{
NSLog(@"失败");
}
});
#### 2.5 NSCondition 条件锁
NSCondition是对mutex和cond的封装
我们先来看看 API:
wait:进入等待状态
waitUntilDate::让一个线程等待一定的时间
signal:唤醒一个等待的线程
broadcast:唤醒所有等待的线程
NSCondition *cLock = [NSCondition new];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"start");
[cLock lock];
[cLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
NSLog(@"线程1");
[cLock unlock];
});
- 唤醒一个等待线程
NSCondition *cLock = [NSCondition new];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lock];
NSLog(@"线程1加锁成功");
[cLock wait];
NSLog(@"线程1");
[cLock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lock];
NSLog(@"线程2加锁成功");
[cLock wait];
NSLog(@"线程2");
[cLock unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
NSLog(@"唤醒一个等待的线程");
[cLock signal];
});
2.6 NSRecursiveLock 递归锁
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致。
递归锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。
NSLock *rLock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[rLock lock];
if (value > 0) {
NSLog(@"线程%d", value);
RecursiveBlock(value - 1);
}
[rLock unlock];
};
RecursiveBlock(4);
});
这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod
是递归调用的。所以每次进入这个 block 时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。
将 NSLock 替换为 NSRecursiveLock:
NSRecursiveLock 方法里还提供了两个方法,用法和上面介绍的基本没什么差别,这里不过多介绍了:
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
2.7 @synchronized 互斥锁
@synchronized是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
sleep(2);
NSLog(@"线程1");
}
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
NSLog(@"线程2");
}
});
2.8 NSConditionLock 条件锁
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
相比于 NSLock 多了个 condition
参数,我们可以理解为一个条件标示。
例子:
NSConditionLock *cLock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([cLock tryLockWhenCondition:0]){
NSLog(@"线程1");
[cLock unlockWithCondition:1];
}else{
NSLog(@"失败");
}
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lockWhenCondition:3];
NSLog(@"线程2");
[cLock unlockWithCondition:2];
});
//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lockWhenCondition:1];
NSLog(@"线程3");
[cLock unlockWithCondition:3];
});
运行结果:
- 我们在初始化 NSConditionLock 对象时,给了他的标示为
0
- 执行
tryLockWhenCondition:
时,我们传入的条件标示也是0
,所 以线程1 加锁成功 - 执行
unlockWithCondition:
时,这时候会把condition
由0
修改为1
- 因为
condition
修改为了1
, 会先走到 线程3,然后 线程3 又将condition
修改为3
- 最后 走了 线程2 的流程
从上面的结果我们可以发现,NSConditionLock 还可以实现任务之间的依赖。
线程锁总结
锁的概念定义:(参考维基百科)
- 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
- 自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种
忙等待
。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。 - 互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
- 读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
- 信号量(semaphore):是一种更高级的同步机制,
互斥锁
可以说是semaphore在仅取值0/1时的特例。信号量
可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。 - 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
3.runloop机制
- 首先说介绍下NSRunLoopOSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
- NSRunLoop 与线程的关系
- 其实runLoop就是一个do ... while()函数,每个runLoop对应一个线程他们是一一对应的关系,关系保存在一个全局的Dictionary里边,线程刚创建时没有RunLoop,如果不主动获取,是不会有的,RunLoop的创建发生在第一次获取时,RunLoop的销毁发生在线程结束,只能在一个线程的内部获取它的RunLoop(主线程除外)主线程默认有个RunLoop.
- Thread包含一个CFRunLoop,一个CFRunLoop包含一种CFRunLoopMode,mode包含CFRunLoopSource,CFRunLoopTimer和CFRunLoopObserver。
- RunLoop只能运行在一种mode下,如果要换mode当前的loop也需要停下重启成新的。利用这个机制,ScrollView过程中NSDefaultRunLoopMode的mode会切换UITrackingRunLoopMode来保证ScrollView的流畅滑动不受只能在NSDefaultRunLoopMode时处理的事件影响滑动。同时mode还是可定制的。
NSDefaultRunLoopMode:默认,空闲状态
UITrackingRunLoopMode:ScrollView滑动时UIInitializationRunLoopMode:启动时NSRunLoopCommonModes:Mode集合 Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes来解决
//将timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRefstatic CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
介绍完线程和RunLoop的关系后,主要看下RunLoop和RunLoopModel, RunLoop runLoop 在CoreFoundation对外一共有五大类最为对外的接口:
RunLoop
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
1. 其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
2. CFRunLoopSourceRef 是事件产生的地方,Source包含了两个部分:Source0 和 Source1。source0:处理如UIEvent,CFSocket这样的事件source1:Mach port驱动,CFMachport,CFMessagePortSource0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。Source1 包含了一个 mach_port 和一个回调(函数指针),被用于和通过内核和其他 mach_port 相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
3. CFRunLoopTimerRefNSTimer是对RunLoopTimer的封装是基于时间的触发器,它和 NSTimer 是toll-free bridge 的。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调
4. CFRunLoopObserverRefCocoa框架中很多机制比如CAAnimation等都是由RunLoopObserver触发的。observer到当前状态的变化进行通知观察者,每个 Ovserver 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
RunLoopModelCFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array ...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; //
Current Runloop Mode CFMutableSetRef _modes; // Set ...
};
这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。RunLoop 会自动将 _commonModeItems 加入到具有 "Common" 标记的所有Mode里。应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。
RunLoop底层实现从上面代码可以看到,RunLoop的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这几个概念,下面稍微介绍一下OSX/iOS的系统架构。
苹果官方将整个系统大致划分为上述4个层次:应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard等。应用框架层即开发人员接触到的 Cocoa 等框架。核心框架层包括各种核心框架、OpenGL 等内容。Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。
CFRunLoop {
current mode = kCFRunLoopDefaultMode common modes = {
UITrackingRunLoopMode kCFRunLoopDefaultMode } common mode items = { // source0 (manual) CFRunLoopSource {order =-1,
{ callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1,
{ callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
// source1 (mach port) CFRunLoopSource {order = 0,
{port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}} CFRunLoopSource {order = 0, {port = 16647}} CFRunLoopSource {order =-1, { callout = PurpleEventCallback}} CFRunLoopSource {order = 0, {port = 2407, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}} CFRunLoopSource {order = 0, {port = 1c03, callout = __IOHIDEventSystemClientAvailabilityCallback}} CFRunLoopSource {order = 0, {port = 1b03, callout = __IOHIDEventSystemClientQueueCallback}} CFRunLoopSource {order = 1, {port = 1903, callout = __IOMIGMachPortPortCallback}}
// Ovserver CFRunLoopObserver {order = -2147483647, activities = 0x1,
// Entry callout = _wrapRunLoopWithAutoreleasePoolHandler} CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting callout = _UIGestureRecognizerUpdateObserver} CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit callout = _afterCACommitHandler} CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit callout = _wrapRunLoopWithAutoreleasePoolHandler} // Timer CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0, next fire date = 453098071 (-4421.76019 @ 96223387169499), callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)} }, modes = { CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} }, sources1 = (null), observers = { CFRunLoopObserver >{activities = 0xa0, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} )}, timers = (null), },
CFRunLoopMode { //这是一个占位的 Mode,没有实际作用。
sources0 = { CFRunLoopSource {order = -1, { callout = PurpleEventSignalCallback}} },
sources1 = { CFRunLoopSource {order = -1, { callout = PurpleEventCallback}} },
observers = (null), timers = (null), },
CFRunLoopMode {
sources0 = (null), sources1 = (null), observers = (null), timers = (null), } }}
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考 这里 。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。####手势识别当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
NSRunLoop的应用
AFNetworking 创建一个常驻服务线程去处理数据返回使用NSOperation+NSURLConnection并发模型都会面临NSURLConnection下载完成前线程退出导致NSOperation对象接收不到回调的问题。AFNetWorking解决这个问题的方法是按照官方的guid上写的NSURLConnection的delegate方法需要在connection发起的线程runloop中调用,于是AFNetWorking直接借鉴了Apple自己的一个Demo的实现方法单独起一个global thread,内置一个runloop,所有的connection都由这个runloop发起,回调也是它接收,不占用主线程,也不耗CPU资源。##TableView中实现平滑滚动延迟加载图片利用CFRunLoopMode的特性,可以将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动UITrackingRunLoopMode这个mode时不会被加载而影响到。
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
使用RunLoop阻塞线程并且同时能够处理其他source事件
CFRunLoopRunInMode(kCFRunLoopDefaultMode,second, NO);
CFRunLoopStop(CFRunLoopGetMain());