类型 | 简介 | 实现语言 | 线程生命周期 |
---|---|---|---|
pthread |
posix 接口,适合跨平台开发,使用难度较大 |
c | 手动管理 |
NSThread | 面向对象,简单易用,可直接操作线程对象 | oc | 手动管理 |
GCD | apple封装底层线程技术,充分利用CPU多核 | c | 自动管理 |
NSOperation | 基于GCD实现的OC接口,比GCD更简单易用 | oc | 自动管理 |
pthread
pthread
线程为posix
接口,适合跨平台开发技术,基于c语言且需要手动管理,见《unix环境高级编程》
相关API如下:
pthread_create(pthread_t *pid, pthread_attr_t *attr,
(void *)(*func)(void *arg), (void *)arg) 创建一个线程
pthread_exit() 终止当前线程
pthread_cancel() 中断另外一个线程的运行
pthread_join() 阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init() 初始化线程的属性
pthread_attr_setdetachstate() 设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate() 获取脱离状态的属性
pthread_attr_destroy() 删除线程的属性
pthread_kill() 向线程发送一个信号
pthread_once() 线程被执行一次,由系统控制,可用于创建线程关联数据pthread_key_t
pthread_key_create() 创建线程关联数据key, NSThread封装保存线程信息就使用了线程关联数据
pthread_setspecific() 设置线程关联数据
pthread_getspecific() 获取线程关联数据
线程关联数据:
用于绑定到特定线程来作为线程私有数据(所有线程共享进程空间,即也可以放到到线程关联数据,但每个线程可指定自己相应的key),如errno
错误码就使用了线程关联数;NSThread
封装pthread
也使用了该结构,用来保存NSThread
对象;
线程泄露
若pthread
未设置detach
模式,不使用pthread_join
等待线程退出获取线程退出状态,就会导致线程泄露;见Thread Leaks
NSThread
NSThread
是pthread
的封装(见gnustep ./source/NSThread.m),面向对象技术;
- 基于thread封装,添加面向对象概念,性能较差,偏向底层
- 相对于GCD和NSOperation来说是较轻量级的线程开发
-
使用比较简单,但是需要手动管理创建线程的生命周期、同步、异步、加锁等问题
相应的API如下:
创建启动
//创建基于target:selector,进入就绪态,默认分离状态,线程退出由系统回收资源
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//基于block
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
//分离状态创建线程,进入就绪态,相当于调用[[NSThread alloc]initWithTarget: selector: object:]
+ (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;
//线程启动,将线程放入可调度线程池,具体启动时机由cpu调度
- (void)start API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//取消线程,只是内部标记线程处于取消状态,gnu实现中未使用pthread_cancel
- (void)cancel API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//线程退出,包装了pthread_exit,为类方法
+ (void)exit;
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
使用如下:
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"thread start");
}];
[thread start];
//或者使用detachNewThreadWithBlock
[NSThread detachNewThreadWithBlock:^{
NSLog(@"thread start");
}];
线程属性
@property (readonly, retain) NSMutableDictionary *threadDictionary;//线程字典
@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses //线程堆栈返回地址
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols //线程堆栈
@property (nullable, copy) NSString *name;线程名称
@property NSUInteger stackSize ;//线程使用栈区大小,默认是512K,可设置堆栈大小
@property (readonly, getter=isExecuting) BOOL executing;//线程正在执行
@property (readonly, getter=isFinished) BOOL finished;//线程执行结束
@property (readonly, getter=isCancelled) BOOL cancelled;//线程是否可以取消
@property double threadPriority ; //优先级,封装pthread_getschedparam
@property NSQualityOfService qualityOfService ; // 线程优先级,read-only after the thread is started
NSQualityOfServiceUserInteractive: // 最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
NSQualityOfServiceUserInitiated: // 次高优先级,主要用于执行需要立即返回的任务
NSQualityOfServiceDefault: // 默认优先级,当没有设置优先级的时候,线程默认优先级
NSQualityOfServiceUtility: // 普通优先级,主要用于不需要立即返回的任务
NSQualityOfServiceBackground: // 后台优先级,用于完全不紧急的任务
@property (readonly) BOOL isMainThread
@property (class, readonly) BOOL isMainThread // reports whether current thread is main
//获取/设定优先级(类方法)
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
通知
NSNotificationName const NSWillBecomeMultiThreadedNotification;
NSNotificationName const NSDidBecomeSingleThreadedNotification;
NSNotificationName const NSThreadWillExitNotification;
线程间通信
//主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;// equivalent to the first method with kCFRunLoopCommonModes
//指定线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait // equivalent to the first method with kCFRunLoopCommonModes
//后台线程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg
Example:子线程进行耗时操作,操作结束后再回到主线程去刷新 UI;
NSThread
GCD
GCD(Grand Cental Dispatch)
为apple多核处理下多线程编程技术,多线程编程更为简洁,且为系统级实现,由系统统一管理(不需要手动管理其生命周期),相比其他线程编程技术效率更高;
Dispatch queue
GCD
指定了两种dispatch queue
分发队列:串行队列(serial dispathc queue)
和并行队列(concurrent dispatch queue)
;
对于串行队列,顾名思义添加到队列中的任务会串行执行,且在一个线程,若使用dispatch_async
则会新创建一个线程(除主队列外);若使用dispatch_sync
则会使用调用线程;
对于并行队列,会多线程并发执行,且系统会根据队列任务数、处理器核心数、处理器负荷等当前系统的状态来决定并行处理的处理数;但若使用dispatch_sync
同步执行,则会使用当前调用线程同步执行;
『主线程』中,『不同队列』+『不同任务』简单组合的区别:
『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』 使用的区别:
注意避免死锁的情况,如主线程中,同步执行主队列任务;
//主线程中调用
1.
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"main queue, sync task done");
});
2.
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(main_queue, ^{
dispatch_sync(main_queue, ^{
NSLog(@"main queue, sync task done");
});
});
dispatch_sync
源码实现流程在主线程中执行并添加到主队列同步执行,会阻塞执行到如下:
//关键代码如下:
_dispatch_queue_push(dq, (void *)&dbss);
dispatch_semaphore_wait(dbss2.dbss2_sema, DISPATCH_TIME_FOREVER);
_dispatch_put_thread_semaphore(dbss2.dbss2_sema);
会阻塞等待信号量,但主线程后续队列无法执行,因此无法释放信号量导致一直阻塞,进而引发“死锁”;见dispatch_sync死锁问题研究
dispatch_queue_create
//label,队列名称,推荐使用appid
//attr, 分为DISPATH_QUEUE_SERIAL(该值即为NULL)串行队列或者DISPATCH_QUEUE_CONCURRENT并行队列
dispatch_queue_t dispatch_queue_create(const char *_Nullable label,
dispatch_queue_attr_t _Nullable attr);
//DISPATCH_SWIFT_UNAVAILABLE("Can't be used with ARC")
//ARC模式下不能使用该函数释放,及ARC模式下为自动释放
void dispatch_release(dispatch_object_t object);
对于dispatch_release
释放队列函数,苹果官方文档已说明:ARC模式下且macos10.8+ ios6.0+无需手动释放,且不能释放主队列及全局队列;对于需要手动释放的,则无需关注仍在队列中未完成的任务,因为block
任务会dispatch_retain
自动持有该队列(即使调用了dispatch_release
),也存在引用计数的概念;
If your app is built with a deployment target of macOS 10.8 and later or iOS v6.0 and later, dispatch queues are typically managed by ARC, so you do not need to retain or release the dispatch queues.
Your application does not need to retain or release the global (main and concurrent) dispatch queues; calling this function on global dispatch queues has no effect.
默认存在main dispatch queue
主队列(主线程执行的队列,因主线程只有一个,该队列为串行队列)和global dispatch queue
全局队列,且全局队列存在四个不同的等级:
dispatch_set_target_queue
dispatch_queue_create
创建的队列默认等级为默认优先级的全局队列等级一样;因此需要修改队列的等级使用dispatch_set_target_queue
;
//object, 指定修改的队列
//queue, 指定目标队列
void dispatch_set_target_queue(dispatch_object_t object,
dispatch_queue_t _Nullable queue);
Example:
dispatch_queue_t queue = dispatch_queue_create("myqueue", NULL);
dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
不可指定主队列及全局队列的优先级!
若多个串行队列指定为同一个目标队列,则原先并行执行的串行队列就会串行执行,且在同一个线程;
dispatch_after
dispatch_after
只是负责指定时间后添加任务到队列中,具体的任务执行由系统去调度;
//when类型为dispatch_time_t,可通过dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)获取
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t block);
dispatch_group
对于并发队列并行执行无法有效获取何时结束,dispatch_group
可对于同一group
下的队列所有任务完成后,再将指定的任务添加到指定队列(包括group下的队列),可汇合所有任务完成节点;
dispatch_group_t dispatch_group_create(void);//创建group
//异步执行添加指定队列的任务
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
//异步等待(不会阻塞当前线程)指定队列添加的任务执行完成(与执行该函数顺序无关,即dispatch_group_async可在该函数后面添加任务)后,添加任务到指定任务
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
//阻塞当前线程,一直等待或者等待指定timeout时间所有任务完成
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
//dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1
//dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。
//只有任务数为0时,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务
//使用下面函数,可不需要使用dispatch_group_async,使用dispatch_async
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
dispatch_barrier_async
与dispatch_group
不同,dispatch_barrier_async
会等待调用此函数前所有并行队列添加的任务完成后,执行该函数添加的任务,然后恢复并行队列的正常行为;
dispatch_apply
dispatch_apply
是按照指定次数添加任务到指定并发队列中,并阻塞等待所有任务完成,类似dispatch_sync
或者dispatch_group_wait
;
void dispatch_apply(size_t iterations,
dispatch_queue_t queue,
void (^block)(size_t));//传入iterations序号
dispatch_once
dispatch_once
保证添加的任务只会被执行一次,为线程安全,常用在单例或者整个程序只执行一次的代码;
- (void)shared {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码(这里面默认是线程安全的)
});
}
dispatch_supend dispatch_resume
void dispatch_suspend(dispatch_object_t object);
void dispatch_resume(dispatch_object_t object);
dispatch_suspend
可以挂起正在执行的队列,但已添加到队列并且执行的任务不受影响,挂起后,队列中尚未执行的任务就会停止执行,需要调用dispatch_resume
恢复才可以,也采用计数概念,dispatch_supend
会计数+1, dispatch_resume
会计数-1,只有计数为0时,才会完全恢复队列中的任务;
dispatch_semphore信号量
见《ios锁》
dispatch source
dispatch source
封装了kqueue
用来监听内核事件,如下:
kqueue
事件继承自FreeBSD,用于监听内核事件,与epoll
类似,通过epoll_wait
和kevent
系统调动阻塞等待事件,但不像select
需要轮训(也可以一直阻塞),并且不需要每次select
调用时从用户空间拷贝文件描述符至内核空间,还有不会线性扫描文件描述符数组,而是通过在内核注册事件回调来监听事件发生,因此在文件描述符较多时优势明显;
Select、poll、Epoll、KQueue区别
epoll内核源码分析
OSX/iOS中多路I/O复用总结
具体API如下:
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,//事件类型
uintptr_t handle,//内核监听的句柄,如套接字、文件描述符
unsigned long mask,//
dispatch_queue_t _Nullable queue);
void
dispatch_source_set_event_handler(dispatch_source_t source,
dispatch_block_t _Nullable handler);
void
dispatch_source_set_timer(dispatch_source_t source,//间隔定时器
dispatch_time_t start,//定时器起始时间dispatch_time()或者使用dispatch_wall_time
uint64_t interval,//重复间隔时间,可以使用DISPATCH_TIME_FOREVER不重复
uint64_t leeway);//延迟时间
//source默认是暂停状态,需要启动或者挂起
void
dispatch_resume(dispatch_object_t object);
Dispatch Source
使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用dispatch_resume
启动定时器。dispatch_after
只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block。
Dispatch Source定时器使用时也有一些需要注意的地方,不然很可能会引起crash:
循环引用:因为
dispatch_source_set_event_handler
回调是个block,在添加到source的链表上时会执行copy
并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel
取消timer。dispatch_resume
和dispatch_suspend
调用次数需要平衡,如果重复调用dispatch_resume则会崩溃,因为重复调用会让dispatch_resume
代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH("Over-resume of an object")导致崩溃。source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)后再重新创建;
当我们使用
dispatch_time
或者DISPATCH_TIME_NOW
时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用dispatch_walltime
可以让计时器按照真实时间间隔进行计时;对于后台线程(不是主线程,前台线程),
dispatch timer
不受RunLoop
影响,但NSTimer 是始终需要 Runloop 支持的;见iOS定时器,你真的会使用吗?dispatch_supen
为挂起Timer,需要和dispatch_resume
平衡使用;而dispatch_source_cancel
取消定时器;dispatch_source
在ARC模式下超过作用域会自动释放,会导致计时器不生效,需要强持有,如在dispatch_source_set_event_handler
里面持有timer
;
Dispatch Source Timer 的使用以及注意事项
深入浅出 GCD 之 dispatch_source
iOS多线程:『GCD』详尽总结
NSOperation NSOperationQueue
NSOperation NSOperationQueue
是GCD
的高级封装的面向对象的技术,可实现添加依赖关系、设定执行的优先级、取消执行操作,比GCD更简单易用、代码可读性更高,使用KVO
观察对操作执行状态的更改,如isExcuting、isFinished、isCancelled;
NSoperation
NSOperation
任务(或者操作)类似GCD
中的block
执行路径代码,该类为抽象类,子类分别为NSInvocationOperation
、NSBlockOperation
和自定义NSOpeartion
子类;
//NSInvocationOperation
//操作会在主线程执行,若操作中添加其他线程操作,则在其他线程执行
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
- (void)start;
//NSBlockOperation
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
//添加额外操作,blockOperationWithBlock操作完成后执行,可添加多个;
//操作执行线程是否开启新的线程,由操作个数及系统来决定
- (void)addExecutionBlock:(void (^)(void))block;
- (void)start;
//自定义子类
- (void)main;//需要重写该方法
- (void)start;//启动
NSOperationQueu
NSOperationQueue
操作队列,类似GCD
队列,分为主队列和自定义队列;
-
主队列:
添加到主队列的操作,都会在主线程执行,除非操作中新开启线程;
NSOperationQueue mainQueue = [NSOprationQueu mainQueue]; //添加操作 - (void)addOperation:(NSOperation *)op; - (void)addOperationWithBlock:(void (^)(void))block
-
自定义队列
添加的操作会自动开启子线程执行;同时包含了串行和并行执行;
NSOperationQueue queue = [NSOprationQueue alloc]init];
串行还是并行的关键属性:
maxConcurrentOperationCount(最大并行操作数)
,最大并行操作最大数为一个队列同时并发执行操作的最大数,而且操作并非只在一个线程执行(如最大并行数为1,则队列串行执行,但多个操作不一定都在同一个线程执行,但保证只在一个线程执行);maxConcurrentOperationCount
默认情况下为-1,表示不进行限制,可进行并发执行。maxConcurrentOperationCount
为1时,队列为串行队列。只能串行执行。maxConcurrentOperationCount
大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。
NSOperation操作依赖
NSOperation NSOperationQuque
最大的吸引点就是添加操作依赖,可以很方便的控制操作的执行顺序,具体的接口如下:
- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
@property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。
注意:需要addDependency
后再addOperation
,否则无法添加操作依赖关系!
NSOperation优先级
NSOperatio
提供了queuePriority
优先级属性,NSOperation 提供了queuePriority
(优先级)属性,queuePriority
属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal
。但是我们可以通过setQueuePriority:
方法来改变当前操作在同一队列中的执行优先级。
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
操作执行顺序:首先保证依赖被执行,其次再根据优先级决定执行顺序;
NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性);
那么,什么样的操作才是进入就绪状态的操作呢?
- 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。
举个例子,现在有4个优先级都是
NSOperationQueuePriorityNormal
(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这4个操作添加到队列中并发执行。
- 因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。
- 而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。
iOS多线程:『NSOperation、NSOperationQueue』详尽总结
Demo
https://github.com/FengyunSky/notes/blob/master/local/code/threadstack.tar