-
Notifications
You must be signed in to change notification settings - Fork 105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
多线程与 Runloop #16
Comments
并发编程一、简介1. 线程和进程、任务(Thread, Process and Task)进程(process):指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出时,这个进程就结束了。 线程(thread):指的是一个独立的代码(汇编指令)执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads 。(⭐️强烈推荐读一读这篇文章-->POSIX Threads Programming: What is a Thread?) 任务(task):指的是我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。
线程一般有三个状态:
参考: 2. 串行和并行(Serial VS. Concurrent)从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。
这两种,均遵循 FIFO 的入队顺序原则。
3. 同步和异步(Sync VS. Async)串行与并行针对的是队列,而同步与异步,针对的则是线程。 4. 队列和线程(Queue VS. Thread)一个队列由一个或多个任务组成,当这些任务要开始执行时,系统会分别把他们分配到某个线程上去执行。当有多个系统核心时,为了高效运行,这些核心会将多个线程分配到各核心上去执行任务,对于系统核心来说并没有任务的概念。 对于一个并行队列来说,其中的任务可能被分配到多个线程中去执行,即这个并行队列可能对应多个线程。对于串行队列,它每次对应一个线程,这个线程可能不变,可能会被更换。 每一时刻,一个线程都只能执行一个任务。一个线程也可能是闲置或者挂起的,因此线程存在时不一定就在执行任务。 队列和线程可以说是两个层级的概念。队列是为了方便使用和理解的抽象结构,而线程是系统级的进行运算调度的单位,他们是上下层级之间的关系。 在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。 正如我们上面所说的,串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理。 参考: 5. 并发与并行(Concurrency VS. Parallel)下面这段话摘自维基百科词条 Concurrency (computer science):
5.1 它们最关键的点就是:是否是『同时』
它们最关键的点就是:是否是『同时』。 5.2 “并行”概念是“并发”概念的一个子集如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。 在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。 我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。 5.3 是不是说并发就是一个线程,并行是多个线程?并发和并行都可以是很多个线程,就看这些线程能不能同时被(多个)cpu执行,如果可以就说明是并行,而并发是多个线程被(一个)cpu 轮流切换着执行。 5.4 从原理上理解并发和并行的本质区别并行可以在计算机的多个抽象层次上运用,这里仅讨论任务级并行(程序设计层面),不讨论指令级并行等。 并发指能够让多个任务在逻辑上同时执行的程序设计,而并行则是指在物理上真正的同时执行。并行是并发的子集,属于并发的一种实现方式。通过时间片轮转实现的多任务同时执行是通过调度算法实现逻辑上的同步执行,属于并发,他们不是真正物理上的同时执行,不属于并行。当通过多核 CPU 实现并发时,多任务是真正物理上的同时执行,才属于并行。 参考
二、多线程技术1. 什么是多线程?下面这句话摘自 并发编程 - objc.io:
2. 为什么会出现多线程?为什么要使用多线程技术?使用多线程可以充分利用现在的多核 CPU、减少 CPU 的等待时间、防止主线程阻塞等。除了性能上的提升,对于批量任务,使用多线程也能使代码逻辑更加清晰。 不过如果某个进程内有大量的 CPU 密集型线程,那么多线程对效率的提升没有半点好处,反而会因为线程上下文的频繁切换增大 CPU 开销(对于单核 CPU 来说更加影响效率)。相对来说,多线程更适合 I/O 密集型任务,正在处理 I/O 的线程大部分时间都处在等待状态,它们不占用 CPU 资源。 参考:
三、iOS 中的并发编程模型在其他许多语言中,为了提高应用的并发性,我们往往需要自行创建一个或多个额外的线程,并且手动地管理这些线程的生命周期,这本身就已经是一项非常具有挑战性的任务了。此外,对于一个应用来说,最优的线程个数会随着系统当前的负载和低层硬件的情况发生动态变化。因此,一个单独的应用想要实现一套正确的多线程解决方案就变成了一件几乎不可能完成的事情。而更糟糕的是,线程的同步机制大幅度地增加了应用的复杂性,并且还存在着不一定能够提高应用性能的风险。 然而,值得庆幸的是,在 iOS 中,苹果采用了一种比传统的基于线程的系统更加异步的方式来执行并发任务(GCD 和 NSOperation)。与直接创建线程的方式不同,我们只需定义好要调度的任务,然后让系统帮我们去执行这些任务就可以了。我们可以完全不需要关心线程的创建与销毁、以及多线程之间的同步等问题,苹果已经在系统层面帮我们处理好了,并且比我们手动地管理这些线程要高效得多。 因此,我们应该要听从苹果的劝告,珍爱生命,远离线程。不过话又说回来,尽管队列是执行并发任务的首先方式,但是毕竟它们也不是什么万能的灵丹妙药。所以,在以下三种场景下,我们还是应该直接使用线程的:
四、iOS 中的几种多线程方案
GCD 和 NSOperation 的对比:
推荐阅读: 五、RunLoop在苹果的Threading Programming Guide 文档中,提到线程管理中可能需要自己设置 run loop:
那么什么是 run loop 呢?下面是苹果 Run Loops文档中关于 run loop 的介绍:
更多细节见:Run Loop 总结 六、多线程下的线程安全问题
更多细节见 线程安全问题总结。 七、参考
|
GCD(《Objective-C高级编程》学习总结)一、简介1. 什么是 GCD
2. 多线程编程
二、GCD 中的 API1. Dispatch Queue1.1 什么是 Dispatch Queue?什么是队列?(画图理解)1.2 Serial Dispatch Queue 和 Concurrent Dispatch QueueSerial Dispatch Queue 是要等待上一个执行完,再执行下一个的,也就是串行队列。 Concurrent Dispatch Queue 是不需要上一个执行完,就能执行下一个的,叫做并行队列。 这两种,均遵循 FIFO 原则。 1.3 队列和线程的区别与联系串行与并行针对的是队列,而同步与异步,针对的则是线程。最大的区别在于,同步线程要阻塞当前线程,必须要等待同步线程中的任务执行完,返回以后,才能继续执行下一任务;而异步线程则是不用等待。 问题:使用 GCD Concurrent Queue 和 Serial Queue 异步执行任务时,系统是怎么管理线程的? 每创建一个 Serial Queue 并异步执行队列中的任务,系统就会创建一个对应的线程。所以,要控制异步执行 Serial Queue 的数量,避免创建过多的线程。 而 Concurrent Queue 异步执行多个并发任务是由系统管理的,并行执行的处理数量取决于当前系统的状态,iOS 和 OS X的核心——XNU 内核决定应当使用的线程数,并只生成所需的线程执行处理。所以,使用 Concurrent Queue 时不需要担心线程过多的问题。 2.
|
dispatch_sync | dispatch_async | |
---|---|---|
串行队列 | 阻塞当前线程,将串行任务添加到队列后,在当前线程顺序执行 | 不阻塞当前线程,将串行任务添加到队列后,新建一个线程顺序执行 |
并发队列 | 阻塞当前线程,在当前线程并发执行 | 不阻塞当前线程,新建多个线程并发执行 |
主队列 | 阻塞主线程,将串行任务添加到队列后,出现死锁 | 不阻塞主线程,将串行任务添加到队列后,在主线程中顺序执行 |
“dispatch_sync
+并发”的组合为什么会是并发执行?详见iOS中的多线程技术中的分析。
9. dispatch_apply
函数
函数声明:
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
功能:
Submits a single block to the dispatch queue and causes the block to be executed the specified number of times.
10. dispatch_suspend
函数和 dispatch_resume
函数
dispatch_suspend
函数用来暂停 Dispatch Queue 中的任务执行。dispatch_resume
函数用来恢复 Dispatch Queue 中暂停的任务,使之继续执行。
11. Dispatch Semaphore(信号量)
什么是信号量?(见 《深入理解计算机系统》第 12.5.2 节。)
信号量就像是一个交通信号灯一样,信号量计数为 0 时等待,信号量计数大于 0 时,减去 1 而且不等待。
当并行执行的任务更新数据时,会产生数据不一致的情况,有时程序还会异常退出。虽然使用 Serial Dispatch Queue 和 dispatch_barrier_async
函数可以避免此类问题,但是 Dispatch Semaphore 可以提供粒度更细的隔离控制。
相关考题:ShannonChenCHN/algorithm-and-data-structure#33 (comment)
11.1 使用 Dispatch Semaphore 实现互斥锁:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 初始为 1,实现互斥锁
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
NSLog(@"子线程中准备添加第 %@ 个元素", @(i));
// semaphore 计数 -1
// 如果 -1 之后结果小于 0,这个函数不会立即返回,而是会等待 dispatch_semaphore_signal 发出信号
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSObject *obj = [NSObject new];
NSLog(@"已有%@个元素,成功添加第%@个元素", @(array.count), @(i));
[array addObject:obj];
// semaphore 计数 +1
// 如果 +1 之前计数小于 0 ,就唤醒调一个用 dispatch_semaphore_wait 等待的线程,并返回非 0,否则就直接返回 0
dispatch_semaphore_signal(semaphore);
});
}
11.2 注意点
使用 Dispatch Semaphore 时有一个很容易被忽略、也是最容易造成 App 崩溃的地方,就是信号量的释放。
Important
Calls to
dispatch_semaphore_signal
must be balanced with calls towait()
. Attempting to dispose of a semaphore with a count lower thanvalue
causes anEXC_BAD_INSTRUCTION
exception.
dispatch_semaphore_t semephore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semephore, DISPATCH_TIME_FOREVER);
//重新赋值或者将semephore = nil都会造成崩溃,因为此时信号量还在使用中
semephore = dispatch_semaphore_create(0);
// BUG IN CLIENT OF LIBDISPATCH: Semaphore object deallocated while in use
参考:
11.3 应用案例
AFNetworking 3.1.0
、YYText 1.0.1
和 GPUImage 0.1.7
中都有用到过 Dispatch Semaphore。
12. dispatch_once
函数
dispatch_once
函数主要是用来执行那些在程序整个生命周期中只需要执行一次的代码。- 实际上,
dispatch_once
函数的价值不仅仅是保证一次执行,还保证了多线程中的安全执行。
13. Dispatch I/O
14. Dispatch Source
Dispatch Source 的种类有:
- DISPATCH_SOURCE_TYPE_DATA_ADD:变量增加
- DISPATCH_SOURCE_TYPE_DATA_OR:变量 OR
- DISPATCH_SOURCE_TYPE_MACH_SEND:MACH 端口发送
- DISPATCH_SOURCE_TYPE_MACH_RECV:MACH 端口接收
- DISPATCH_SOURCE_TYPE_PROC:检测到与进程相关的事件
- DISPATCH_SOURCE_TYPE_READ:可读取文件映像
- DISPATCH_SOURCE_TYPE_TIMER:定时器
...
我们可以使用 DISPATCH_SOURCE_TYPE_TIMER
创建计时器/定时器:
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
// 15秒后执行,且不重复执行,允许延迟1秒
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC);
// 时间到了之后要处理的任务
dispatch_source_set_event_handler(timer, ^{
NSLog(@"wakeup!");
// 取消定时器
dispatch_source_cancel(timer);
});
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"canceled");
});
NSLog(@"start timer!");
dispatch_resume(timer);
三、其它
1. GCD 和 Runloop
SDWebImage 中有这样一个宏定义:
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
它的作用是判断一下当前队列是不是主队列,如果是则直接执行,如果不是则异步将任务加到主队列中执行。
在下面这个例子中:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"before submits a block on the main queue : %@", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"invoke th block on the main queue");
});
NSLog(@"after submits a block on the main queue : %@", [NSThread currentThread]);
}
输出结果如下:
2021-06-12 14:37:16.232115+0800 before submits a block on the main queue : <NSThread: 0x6000018d05c0>{number = 1, name = main}
2021-06-12 14:37:16.232376+0800 after submits a block on the main queue : <NSThread: 0x6000018d05c0>{number = 1, name = main}
2021-06-12 14:44:47.174962+0800 invoke th block on the main queue
我们可以看到提交到主队列的 block 是最后执行的。
根据苹果在dispatch_get_main_queue
函数的文档中所说,在 iOS 应用程序中,当我们把 block 添加到主队列中之后,系统会通过三种方式来回调提交的 block 任务:
The system automatically creates the main queue and associates it with your application’s main thread. Your app uses one (and only one) of the following three approaches to invoke blocks submitted to the main queue:
- Calling
dispatch_main
- Calling
UIApplicationMain
(iOS) orNSApplicationMain
(macOS)- Using a
CFRunLoopRef
on the main thread
通过断点调试上面的示例代码,我们可以看到 block 的调用堆栈,原来是 RunLoop 调用的:
在苹果开源的 libdispatch(也可以在线阅读)中,我们可以看到 _dispatch_main_queue_callback_4CF
的实现:
_dispatch_main_queue_callback_4CF
_dispatch_main_queue_drain
_dispatch_continuation_pop_inline
_dispatch_continuation_invoke_inline
_dispatch_client_callout
至于 RunLoop 的调用 _dispatch_main_queue_callback_4CF
的时机需要看 CFRunLoop 的实现了,详见 RunLoop 总结。
2. GCD 和 -performSelector:
系列方法
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
下面的例子中:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"before submits a block on the main queue : %@", [NSThread currentThread]);
// __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ ()
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"dispatch main queue callback");
});
// 先直接返回,然后再在 Runloop 中回调
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ ()
[self performSelector:@selector(print_0) withObject:nil afterDelay:0];
// 先直接返回,然后再在 Runloop 中回调
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
[self performSelectorOnMainThread:@selector(print_1) withObject:nil waitUntilDone:NO];
NSLog(@"after submits a block on the main queue : %@", [NSThread currentThread]);
}
- (void)print_0 {
NSLog(@"this is invoked by -performSelector:withObject:afterDelay:");
}
- (void)print_1 {
NSLog(@"this is invoked by -performSelectorOnMainThread:withObject:waitUntilDone:");
}
@end
输出结果是:
2021-06-12 17:00:45.444193+0800 before submits a block on the main queue : <NSThread: 0x6000006ec880>{number = 1, name = main}
2021-06-12 17:00:45.444753+0800 after submits a block on the main queue : <NSThread: 0x6000006ec880>{number = 1, name = main}
2021-06-12 17:01:09.531265+0800 this is invoked by -performSelectorOnMainThread:withObject:waitUntilDone:
2021-06-12 17:01:27.739875+0800 invoke th block on the main queue
2021-06-12 17:01:40.501056+0800 this is invoked by -performSelector:withObject:afterDelay:
可以看到,最先被调用的是 -performSelectorOnMainThread:withObject:waitUntilDone:
,然后是 dispatch main queue 中的 block,最后才是 performSelector:withObject:afterDelay:
。
这三种方式最终都是通过 RunLoop 来回调的,它们在 RunLoop 中的回调函数分别是:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
更多细节见 RunLoop 总结。
参考:
- Perform on Next Run Loop: What's Wrong With GCD? - Stack Overflow
- Grand Central Dispatch (GCD) vs. performSelector - Stack Overflow
四、GCD 的实现
详见 GCD 的实现。
参考
- 《Objective-C 高级编程》
- 《Effective Objective-C 2.0》
- Concurrency Programming Guide - Apple Developer
- swift-corelibs-libdispatch
- 深入理解 GCD - bestswifter
- 《Mac OS X 背后的故事》
NSOperation 和 NSOperationQueue一、简介后台异步执行任务一般有 GCD 和 NSOperation 这两种选择。 二、任务、线程和进程
一个进程可以包含几个不同线程,一个线程可以同时执行多个不同的任务。
三、什么是 NSOperation?NSOperation是一个抽象的基类,表示一个独立的计算单元,可以为子类提供有用且线程安全的建立状态,优先级,依赖和取消等操作。 NSOperation 的 3 种使用形式
四、NSOperation 和 GCD 的对比简单来说,GCD 是苹果基于 C 语言开发的,一个用于多核编程的解决方案,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。而 Operation Queues 则是一个建立在 GCD 的基础之上的,面向对象的解决方案。它使用起来比 GCD 更加灵活,功能也更加强大。
五、NSOperation 的应用场景很多执行任务类型的案例都很好的运用了NSOperation,包括网络请求,图像压缩,自然语言处理或者其他很多需要返回处理后数据的、可重复的、结构化的、相对长时间运行的任务。 在很多的优秀开源项目中都能看到 NSOperation 的身影,比如 SDWebImage、AFNetworking 等。 六、NSOperationQueue 与 NSOperation 的结合使用
七、如何使用 NSOperation1. 状态的管理NSOperation包含了一个十分优雅的状态机来描述每一个操作的执行。
2. 启动、暂停和取消操作2.1 启动(1)手动调用 start 方法 如果我们想要自定义一个并发执行的 operation ,那么我们就必须要编写一些额外的代码来让这个 operation 异步执行。比如,为这个 operation 创建新的线程、调用系统的异步方法或者其他任何方式来确保 start 方法在开始执行任务后立即返回。 (2)添加到 queue 中后自动启动 在绝大多数情况下,我们都不需要去实现一个并发的 operation 。如果我们一直是通过将 operation 添加到 operation queue 的方式来执行 operation 的话,我们就完全没有必要去实现一个并发的 operation 。因为,**当我们将一个非并发的 operation 添加到 operation queue 后,operation queue 会自动为这个 operation 创建一个线程。**因此,只有当我们需要手动地执行一个 operation ,又想让它异步执行时,我们才有必要去实现一个并发的 operation 。 (3) start 方法和 main 方法 start :start 方法是一个 operation 的起点。这个方法的默认实现是更新 operation 的状态并调用 main 方法。这个方法的内部在执行任务前会检查 cancelled 和 finished 的值,以确保任务需要被执行。 main :负责执行 operation 对象中的非并发部分的操作,非必须实现。通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start 方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰。 2.2 暂停和恢复如果我们想要暂停和恢复执行 operation queue 中的 operation ,可以通过调用 operation queue 的 setSuspended: 方法来实现这个目的。不过需要注意的是,暂停执行 operation queue 并不能使正在执行的 operation 暂停执行,而只是简单地暂停调度新的 operation 。另外,我们并不能单独地暂停执行一个 operation ,除非直接 cancel 掉。 2.3 取消
3. 优先级通过设置 queuePriority 属性可以控制队列中操作执行的优先级:
queuePriority 属性决定队列中操作相互之间的依赖关系,因此使用 queuePriority 的前提是没有通过 addDependency 方法设置过操作之间的 dependency 4. 依赖性当一个任务需要在另一个任务执行完后在执行时,可以通过设置任务之间的 dependency 关系来实现。 比如说,对于服务器下载并压缩一张图片的整个过程,你可能会将这个整个过程分为两个操作(可能你还会用到这个网络子过程再去下载另一张图片,然后用压缩子过程去压缩磁盘上的图片)。显然图片需要等到下载完成之后才能被调整尺寸,所以我们定义网络子操作是压缩子操作的依赖,通过代码来说就是:
注意点:
5. completionBlock每当一个NSOperation执行完毕或者被取消,它就会调用它的completionBlock属性一次,这提供了一个非常好的方式让你能在视图控制器(View Controller)里或者模型(Model)里加入自己更多自己的代码逻辑。比如说,你可以在一个网络请求操作的completionBlock来处理操作执行完以后从服务器下载下来的数据。 注意:completionBlock 被回调时,不能确保是在主线程,所以需要你自己控制是否回到主线程。 八、如何自定义 NSOperation 子类我们可以通过重写 main 或者 start 方法 来定义自己的 operations 。 使用 main 方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start 来说要少一些,因为main方法执行完就认为operation结束了,所以一般可以用来执行同步任务。 如果你希望拥有更多的控制权,或者想在一个操作中可以执行异步任务,那么就重写 start 方法, 但是注意:这种情况下,你必须手动管理操作的状态, 只有当发送 isFinished 的 KVO 消息时,才认为是 operation 结束。 当实现了start方法时,默认会执行start方法,而不执行main方法。 为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter 来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。
为了能使用操作队列所提供的取消功能,你需要在适当的时机检查 isCancelled 属性。 九、总结我们应该尽可能地直接使用队列而不是线程,让系统去与线程打交道,而我们只需定义好要调度的任务就可以了。一般情况下,我们也完全不需要去自定义一个并发的 operation ,因为在与 operation queue 结合使用时,operation queue 会自动为非并发的 operation 创建一个线程。Operation Queues 是对 GCD 面向对象的封装,它可以高度定制化,对依赖关系、队列优先级和线程优先级等提供了很好的支持,是我们实现复杂任务调度时的不二之选。 参考
|
多线程的安全问题一、简介1. 什么是线程安全?线程安全就是指,多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。 2. 什么是原子性?原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为"不可被中断的一个或一系列操作"。 原子性(atomicity),就是指不被线程调度器中断的操作,同一时间只有一个线程进行操作,若存在多个线程同时操作的话,就存在线程安全的因素了,是非原子性的。 比如 二、并发编程中面临的挑战1. 资源共享并发编程中许多问题的根源就是在多线程中访问共享资源。资源可以是一个属性、一个对象,通用的内存、网络设备或者一个文件等等。在多线程中任何一个共享的资源都可能是一个潜在的冲突点,你必须精心设计以防止这种冲突的发生。 竞态条件:在多线程里面访问一个共享的资源,如果没有一种机制来确保在线程 A 结束访问一个共享资源之前,线程 B 就不会开始访问该共享资源的话,资源竞争的问题就总是会发生。如果你所写入内存的并不是一个简单的整数,而是一个更复杂的数据结构,可能会发生这样的现象:当第一个线程正在写入这个数据结构时,第二个线程却尝试读取这个数据结构,那么获取到的数据可能是新旧参半或者没有初始化。为了防止出现这样的问题,多线程需要一种互斥的机制来访问共享资源。 线程同步(synchronize):同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。这里的同步千万不要理解成那个同时进行,应是指协同、协助、互相配合。线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的! 线程互斥(mutex):是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 2. 互斥锁互斥访问的意思就是同一时刻,只允许一个线程访问某个特定资源。为了保证这一点,每个希望访问共享资源的线程,首先需要获得一个共享资源的互斥锁,一旦某个线程对资源完成了操作,就释放掉这个互斥锁,这样别的线程就有机会访问该共享资源了。
3. 死锁互斥锁解决了竞态条件的问题,但很不幸同时这也引入了一些其他问题,其中一个就是死锁。当多个线程在相互等待着对方的结束时,就会发生死锁,这时程序可能会被卡住。 维基百科上关于死锁的介绍:
死锁的四个条件是:
看看下面的代码,它交换两个变量的值: void swap(A, B)
{
lock(lockA);
lock(lockB);
int a = A;
int b = B;
A = b;
B = a;
unlock(lockB);
unlock(lockA);
} 大多数时候,这能够正常运行。但是当两个线程使用相反的值来同时调用上面这个方法时: swap(X, Y); // 线程 1
swap(Y, X); // 线程 2 此时程序可能会由于死锁而被终止。线程 1 获得了 X 的一个锁,线程 2 获得了 Y 的一个锁。 接着它们会同时等待另外一把锁,但是永远都不会获得。 所以,你在线程之间共享的资源越多,你使用的锁也就越多,同时程序被死锁的概率也会变大。这也是为什么我们需要尽量减少线程间资源共享,并确保共享的资源尽量简单的原因之一。 关于死锁的实际案例分析见这里。 参考: 4. 资源饥饿(Starvation)我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。 当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。 5. 优先级反转背景知识:
(1)什么是优先级反转优先级反转是指程序在运行时低优先级的任务阻塞了高优先级的任务,有效的反转了任务的优先级。由于 GCD 提供了拥有不同优先级的后台队列,甚至包括一个 I/O 队列,所以我们最好了解一下优先级反转的可能性。 百度百科的定义:
(2)优先级反转是怎么发生的高优先级和低优先级的任务之间共享资源时,就可能发生优先级反转。当低优先级的任务获得了共享资源的锁时,该任务应该迅速完成,并释放掉锁,这样高优先级的任务就可以在没有明显延时的情况下继续执行。然而高优先级任务会在低优先级的任务持有锁的期间被阻塞。如果这时候有一个中优先级的任务(该任务不需要那个共享资源),那么它就有可能会抢占低优先级任务而被执行,因为此时高优先级任务是被阻塞的,所以中优先级任务是目前所有可运行任务中优先级最高的。此时,中优先级任务就会阻塞着低优先级任务,导致低优先级任务不能释放掉锁,这也就会引起高优先级任务一直在等待锁的释放。 (3)如何解决objc.io中给出的建议是:通常就是不要使用不同的优先级。通常最后你都会以让高优先级的代码等待低优先级的代码来解决问题。当你使用 GCD 时,总是使用默认的优先级队列(直接使用,或者作为目标队列)。如果你使用不同的优先级,很可能实际情况会让事情变得更糟糕。 百度百科中给出的建议:
三、atomic 和 nonatomic共享状态,多线程共同访问某个对象的property,在iOS编程里是很普遍的使用场景,我们就从Property的多线程安全说起。 我们可以简单的将property分为值类型和对象类型,值类型是指primitive type,包括int, long, bool等非对象类型,另一种是对象类型,声明为指针,可以指向某个符合类型定义的内存区域。
当我们访问属性 userName 的时候,访问的有可能是 userName 本身,也有可能是 userName 所指向的内存区域。 当我们讨论多线程安全的时候,其实是在讨论多个线程同时访问一个内存区域的安全问题。针对同一块区域,我们有两种操作,读(load)和写(store),读和写同时发生在同一块区域的时候,就有可能出现多线程不安全。 1. 多线程是如何同时访问内存的?从上图中可以看出,我们只有一个地址总线,一个内存。即使是在多线程的环境下,也不可能存在两个线程同时访问同一块内存区域的场景,内存的访问一定是通过一个地址总线串行排队访问的。 几个结论:
2. atomic 和 nonatomic 的区别是什么?atomic 就一定是安全的吗?atomic 的作用只是给 getter 和 setter 加了个锁,atomic 只能保证代码进入 getter 或者 setter 函数内部时是安全的,一旦出了 gette r和 setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。 所以,我们更倾向于使用基于队列的并发编程 API :GCD 和 operation queue 。它们通过集中管理一个被大家协同使用的线程池,来解决上面遇到的问题。 3. atomic 的实现原理atomic 的作用只是给 getter 和 setter 加了个锁。 nonatomic 属性的 getter 和 setter 方法: @property(nonatomic, retain) NSString *userName;
//Generates roughly
- (NSString *) userName {
return _userName;
}
- (void)setUserName:(NSString *)userName {
[userName retain];
[_userName release];
_userName = userName;
} atomic 属性的 getter 和 setter 方法: @property(retain) NSString *userName; // 默认是 atomic
//Generates roughly
- (NSString *)userName {
NSString *retval = nil;
@synchronized(self) {
retval = [[userName retain] autorelease];
}
return retval;
}
- (void)setUserName:(NSString *)userName {
@synchronized(self) {
[userName retain];
[_userName release];
_userName = userName;
}
}
四、如何做到多线程安全?做到多线程安全的关键是 atomicity(原子性),只要做到原子性,小到一个primitive type变量的访问,大到一长段代码逻辑的执行,原子性能保证代码串行的执行,能保证代码执行到一半的时候,不会有另一个线程介入。 原子性是个相对的概念,它所针对的对象,粒度可大可小。 1. 加锁我们在做多线程安全的时候,并不是通过给 property 加 atomic 关键字来保障安全,而是将 property 声明为 nonatomic(nonatomic没有getter,setter的锁开销),然后自己对操作属性/数据的代码片段进行加锁。值得注意的是,读和写都需要加锁。
比如下面这段代码就是通过加锁(而不是声明 atomic 关键字)来保证对属性 //thread A write
[_lock lock];
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
} else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock]; //thread B read
[_lock lock];
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
[_lock unlock]; 2. Atomic Operations其实除了各种锁之外,iOS上还有另一种办法来获取原子性,使用Atomic Operations,相比锁的损耗要小一个数量级左右,在一些追求高性能的第三方Framework代码里可以看到这些Atomic Operations的使用( Atomic Operation只能应用于32位或者64位的数据类型,在多线程使用NSString或者NSArray这类对象的场景,还是得使用锁。 大部分的Atomic Operation都有OSAtomicXXX,OSAtomicXXXBarrier两个版本,Barrier就是前面提到的memory barrier,在多线程多个变量之间存在依赖的时候使用Barrier的版本,能够保证正确的依赖顺序。 六、总结1. 多线程安全比多线程性能更重要对于平时编写应用层多线程安全代码,推荐使用更易用的 @synchronized,NSLock,或者dispatch_semaphore_t,多线程安全比多线程性能更重要,应该在前者得到充分保证,犹有余力的时候再去追求后者。 2. 尽量避免多线程的设计并发编程中,无论是看起来多么简单的 API ,它们所能产生的问题会变得非常的难以观测,而且要想调试这类问题往往也都是非常困难的。我们应该尽可能避免盲目使用多线程技术,而不是去追求高明的使用锁的技能。 3. 尽可能地把具体的线程控制交给系统去处理正如 Concurrent Programming: APIs and Challenges 中所说的:
不论使用 pthread 还是 NSThread 来直接对线程操作,都是相对糟糕的编程体验,这种方式并不适合我们以写出良好代码为目标的编码精神。 直接使用线程可能会引发的一个问题是,如果你的代码和所基于的框架代码都创建自己的线程时,那么活动的线程数量有可能以指数级增长。这在大型工程中是一个常见问题。例如,在 8 核 CPU 中,你创建了 8 个线程来完全发挥 CPU 性能。然而在这些线程中你的代码所调用的框架代码也做了同样事情(因为它并不知道你已经创建的这些线程),这样会很快产生成成百上千的线程。代码的每个部分自身都没有问题,然而最后却还是导致了问题。使用线程并不是没有代价的,每个线程都会消耗一些内存和内核资源。 参考 |
多线程安全问题——锁一、简介二、iOS 保证线程安全的几种方式iOS 保证线程安全的几种方式有:
1. NSLock
NSLock 遵循 NSLocking 协议,lock 方法是加锁,unlock 方法是解锁,tryLock 方法是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。 使用 lock 方法添加的互斥锁会使得线程阻塞,阻塞的过程又分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入 waiting 状态,此时线程就不占用CPU资源了,等锁可用的时候,这个线程会立即被唤醒。 tryLock 方法并不会阻塞线程。[lock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码。 2. @synchronized
@synchronized(object) 指令使用的 object 为该锁的唯一标识,只有当标识相同时,才满足互斥,所以如果线程 2 中的 @synchronized(self) 改为@synchronized(self.view),则线程2就不会被阻塞。 @synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制。但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。@synchronized 还有一个好处就是不用担心忘记解锁了。 如果在 @sychronized(object){} 内部 object 被释放或被设为 nil,从测试的结果来看,的确没有问题,但如果 object 一开始就是 nil,则失去了锁的功能。不过虽然 nil 不行,但 @synchronized([NSNull null]) 是完全可以的。 @synchronized{} 的实现原理 实际上, _objc_sync_enter
...// {} 中的代码
_objc_sync_exit
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
} 结论:
一些建议:
延伸阅读: 3. dispatch_semaphoredispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的有三个函数:
一个
4. NSConditionNSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后可以保护任务中访问的资源,线程检查器可以根据条件决定是否继续执行任务。当条件不满足时,当前线程就会被阻塞。等到其它线程中的同一个锁执行 signal 或者 broadcast 方法时,线程被唤醒,再根据条件决定是否继续运行之后的任务。 几个常用方法:
NSCondition 的使用步骤:
使用伪代码来表示的话,就是下面这样:
当一个线程在等待一个条件时,也就是调用 NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
while (array.count == 0) {
NSLog(@"线程1处于等待状态, %@", [NSThread currentThread]);
[lock wait];
}
[array removeAllObjects];
NSLog(@"array removeAllObjects");
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
NSLog(@"线程2, %@", [NSThread currentThread]);
[array addObject:@1];
NSLog(@"array addObject:@1");
[lock signal];
[lock unlock];
});
5. NSConditionLockNSConditionLock 是一种条件锁,NSConditionLock 和 NSLock 一样,都遵循 NSLocking 协议,方法也很类似,只是多了一个 condition 属性,以及每个操作都多了一个更新 condition 属性的方法。 只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。而 unlockWithCondition: 并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值。
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1准备加锁");
[lock lockWhenCondition:1];
NSLog(@"线程1");
sleep(2);
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1); // 以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:0]) {
NSLog(@"线程2");
[lock unlockWithCondition:2];
NSLog(@"线程2解锁成功");
} else {
NSLog(@"线程2尝试加锁失败");
}
});
//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2); // 以保证让线程3的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程3");
[lock unlock];
NSLog(@"线程3解锁成功");
} else {
NSLog(@"线程3尝试加锁失败");
}
});
//线程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3); // 以保证让线程4的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程4");
[lock unlockWithCondition:1];
NSLog(@"线程4解锁成功");
} else {
NSLog(@"线程4尝试加锁失败");
}
});
6. NSRecursiveLockNSRecursiveLock 是递归锁,他和 NSLock 的区别在于,NSRecursiveLock 可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题),NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。 NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value:%d", value);
RecursiveBlock(value - 1);
}
[lock unlock];
};
RecursiveBlock(2);
}); 如上面的示例,如果用 NSLock 的话,lock 先锁上了,但未执行解锁的时候,就会进入递归的下一层,而再次请求上锁,阻塞了该线程,线程被阻塞了,自然后面的解锁代码不会执行,而形成了死锁。而 NSRecursiveLock 递归锁就是为了解决这个问题。 7. 自旋锁 OSSpinLockOSSpinLock 是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法。NSLock 请求加锁失败的话,会先轮询,但一秒过后便会使线程进入 waiting 状态,等待唤醒。而 OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。 使用示例: __block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
NSLog(@"线程1");
sleep(10);
OSSpinLockUnlock(&theLock);
NSLog(@"线程1解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
OSSpinLockLock(&theLock);
NSLog(@"线程2");
OSSpinLockUnlock(&theLock);
}); 拿上面的输出结果和上文 NSLock 的输出结果做对比,会发现 sleep(10) 的情况,OSSpinLock 中的“线程 2”并没有和”线程 1解锁成功“在一个时间输出,而是有一点时间间隔,而 NSLock 这里是同一时间输出,所以 OSSpinLock 一直在做着轮询,而不是像 NSLock 一样先轮询,再 waiting 等唤醒。 OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。 不过最近YY大神在自己的博客不再安全的 OSSpinLock中说明了OSSpinLock已经不再安全,在 macOS 10.12 中已经被 deprecate 了。 8. pthread_mutexpthread pthread_mutex 是 C 语言下多线程加互斥锁的方式,使用时需要通过 几个相关函数:
8.1 普通锁跟 NSLock 的效果类似:
8.2 递归锁
这是 三、总结ibireme 在 不再安全的 OSSpinLock 中,通过 benchmark (https://github.com/ibireme/tmp/blob/master/iOSLockBenckmark/iOSLockBenckmark/ViewController.m)对不同的锁做出了性能对比分析:
值得注意的是,OSSpinLock 性能最高,但它已经不再安全,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,由于它会处于轮询的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这样就很容易导致优先级反转的问题。 另外,如果不考虑性能,只是图个方便的话,那就使用 @synchronized。 最后用一句来解释线程安全怎么解决:解决线程安全的问题,无非就是加锁,等待(阻塞),解锁。 参考 |
Run Loops
一、简介1. 什么是 Runloop?Runloop 是什么?Runloop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。 举个例子,一个应用开始运行以后,如果不对它进行任何操作,这个应用就像静止了一样,不会自发的有任何动作发生,但是如果我们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。其实,这就是 Runloop 的功劳。 2. 为什么需要 Runloop?一般来说一个线程一次只能执行一个任务,任务执行完成这个线程就会退出。
这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop,安卓里面的 Looper 机制。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。 3. iOS 中 RunloopRunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。 OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。 二、RunLoop 与线程的关系一些线程执行的任务是一条直线,起点到终点;而另一些线程要干的活则是一个圆环,不断循环,直到通过某种方式将它终止。比如,简单的 Hello World 命令行程序就是一种直线执行的线程,一旦执行完毕,它的生命周期便结束了,像昙花一现那样;而像 iOS 应用的主线程那样的圆形线程 ,一直运行直到退出应用。 实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop。每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。 主线程的 run loop 默认是启动的。对其它线程来说,run loop 默认是没有启动的。另外,苹果不允许直接创建 RunLoop,不过我们可以通过: NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 来获取到当前线程的 run loop。 从 CFRunLoopRef 的源码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。 运行 App,点击暂停按钮,我们可以看到除了主线程中的 Run Loop 之外,还有一个叫做 三、RunLoop 的构成在 CoreFoundation 里面关于 RunLoop 有5个类:
他们的关系如下: 一个 Thread 包含一个 CFRunLoop,一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。 上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。 1. RunLoop 的 ModeCFRunLoopMode 和 CFRunLoop 的结构大致如下:
APP 启动后系统默认注册了5个Mode:
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。 同时苹果还公开提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。 2. CFRunLoopTimerCFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。 3. CFRunLoopSourceCFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
4. CFRunLoopObserver我们可以通过如下 API 注册 observer:
每次 loop 只会以一种 mode 运行,以该 mode 运行的时候,就只执行和该 mode 相关的任务,只通知该 mode 注册过的 observer。 CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
四、Runloop 的运行逻辑下面是官方文档中的描述:
简化版的核心逻辑: __CFRunLoopRun() {
while (alive) {
performTask() //执行任务
callout_to_observer() //通知外部观察者
sleep() //休眠
}
}
performTask() {
__CFRunLoopDoBlocks(rl, rlm); // 执行 CFRunLoopPerformBlock() 插入的 block
__CFRunLoopDoSources0(rl, rlm, stopAfterHandle); // 执行 source0
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply); // 执行 source1
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time()); // 执行注册的 timer,比如 NSTimer
_dispatch_main_queue_callback_4CF(msg); // 执行加入到 dispatch_main_queue 中的 block
}
callout_to_observer() {
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 通知 Observer 即将调用 DoTimers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 通知 Observer 即将处理 Sources
DoObservers-Activity... // 其他事件
}
sleep() {
if (__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY)) {
goto handle_msg;
}
} CFRunLoop 核心部分源码解读:
五、RunLoop 的底层实现RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。 为了实现消息的发送和接收, RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。 六、我们看不到的 Runloop 都做了些什么1. AutoreleasePool下面是摘录的一段描述(注意:下面的描述跟现在的实现有些不同):
通过设置 设置 另外,我通过一个 demo 观察了 2. 事件响应苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件,并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。 随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。 3. 手势识别当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。 4. 界面更新当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。 苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,这个Observer的回调函数是 5. 定时器下面是苹果官方文档中的介绍:
(1)NSTimer NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。 (2)CADisplayLink CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。 6. PerformSelecter当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。 因此,如果是在子线程上调用上面的方法,我们首先需要确认当前线程是否已经开启了 Runloop。 7. 关于GCD当调用 但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。 8. 关于网络请求(1)网络架构分层
(2)NSURLConnection 的工作过程 七、Runloop 在实际开发中的应用(When and How)对 runloop 的应用大致可以分为两类,一是开发者通过 runloop 执行自己的任务,比如 mainQueue,timer 等。另一类就是通过 runloop 观测分析主线程的运行状态。
1. 以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?一个 Timer 一次只能加入到一个 RunLoop 中。我们日常使用的时候,通常就是加入到当前的 runLoop 的 default mode 中,而 ScrollView 在用户滑动时,主线程 RunLoop 会转到 UITrackingRunLoopMode 以保证 ScrollView 的流畅滑动:只能在NSDefaultRunLoopMode 模式下处理的事件会影响ScrollView的滑动。因此这个时候, Timer 就不会运行。 有如下两种解决方案: 第一种: 设置RunLoop Mode,例如NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个Mode的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。 在 AFNetworking 3.0 中,就采用了第一种方法,代码如下:
2. AFNetworking 2.x使用 NSOperation + NSURLConnection 的并发模型都会面临 NSURLConnection 下载完成前线程退出导致 NSOperation 对象接收不到回调的问题。 AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
从上面的代码可以看出,AFN创建了一个新的线程命名为 AFNetworking ,然后在这个线程中创建了一个 RunLoop ,在上面2.3章节 RunLoop 运行机制中提到了,一个RunLoop中如果source/timer/observer 都为空则会退出,并不进入循环。所以,AFN在这里为 RunLoop 添加了一个 NSMachPort ,这个port开启相当于添加了一个Source1事件源(当没有 source 或者 timer 要处理时,run loop 会通过调用
3. 实现 UITableView 中平滑滚动延迟加载图片当没有任何用户交互时,默认的 Mode 是 利用 CFRunLoopMode 的特性,可以将图片的加载放到 Run Loop 的 NSDefaultRunLoopMode 中,这样在滚动列表时(UITrackingRunLoopMode), 图片加载也不会影响到列表滚动的流畅度:
4. 处理崩溃让程序继续运行如果App运行遇到 Exception 就会直接崩溃并且退出,其实真正让应用退出的并不是产生的异常,而是当产生异常时,系统会结束掉当前主线程的 RunLoop ,RunLoop 退出主线程也就退出了,所以应用才会退出。 有时候,我们可以让应用在崩溃时依然可以正常运行: (2)收集崩溃日志 把下面的代码添加到 Exception 的handle方法中,此时获取了当前线程(主线程)的 RunLoop ,让这个 RunLoop 在所有的 Mode 下面一直不停的跑,保证主线程不会退出,我们的应用也就存活下来了。 CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
for (NSString *mode in allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
} 5. AsyncDisplayKitAsyncDisplayKit(现在已经被迁移到了 Texture) 是 Facebook 推出的用于保持界面流畅性的框架。 AsyncDisplayKit 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer。开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。然后再在某个时刻将相关属性同步到主线程的 UIView/CALayer 去。 ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。 6. 使用 NSInputStream 异步逐行读取文件在 objc.io 的第二期中有一个应用 NSInputStream 逐行读取文件的例子,与 URL connections 类似,输入的 streams 通过 run loop 来传递它的事件。这里采用 main run loop 来分发事件,然后将数据处理过程派发至后台操作线程里去处理。
7. 利用 RunLoop 原理来监控卡顿如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。 所以,如果我们要利用 RunLoop 原理来监控卡顿的话,就是要关注这两个阶段。RunLoop 在进入睡眠之前和唤醒后的两个 loop 状态定义的值,分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,也就是要触发 Source0 回调和接收 mach_port 消息两个状态。 参考 |
“死锁现场”触发场景:点击城市列表搜索框,快速输入文字,出现 APP 卡死的 bug。 追踪卡死后,点击 Xcode 的 pause 按钮,可以通过查看函数调用堆栈找出线索。 分析:每个服务对应一个 task,每个 task 有一个 connection 引用和一个 token。 threadStateManager 是一个单例,管理着各个 task 的状态,比如取消 task、判断是否被取消。 取消 task 的过程:
方法调用栈: 发起请求的过程:
方法调用栈: 结论在子线程发送/接受请求时需要对 connection 和 threadStateManager(单例) 加锁,但是同时在主线程取消服务时,也需要对 threadStateManager(单例) 和同一个 connection 加锁。这样就会出现死锁的情况。 伪代码如下: void send(connection) {
lock(connectionLock);
lock(singletonLock);
// do something with connection and singleton
// ...
unlock(singletonLock);
unlock(connectionLock);
}
void cancel(connection) {
lock(singletonLock);
lock(connectionLock);
// do something with connection and singleton
// ...
unlock(connectionLock);
unlock(singletonLock);
}
dispatch_async(someQueue, ^{
send();
});
cancel();
|
多线程开发实践典型案例参考: |
GCD 的实现1. Dispatch Queue通常,应用程序中线程管理的代码要在系统级实现,也就是 iOS 和 OS X 的核心 XNU 内核上。 GCD 的 Dispatch Queue 的实现包括三个部分:
用于实现 Dispatch Queue 而使用的框架/库:
我们平时开发使用的 GCD 的 API 都在libdispatch 库(开源)中。 2. GCD 各个 API 的实现3. GCD 的线程池是如何实现的参考: |
pthreads(POSIX)
#include <stdio.h>
#include <unistd.h> // notice this! you need it!
int main(){
printf("Hello,");
sleep(5); // format is sleep(x); where x is # of seconds.
printf("World");
return 0;
} 参考 |
多线程知识点总结
问题
相关示例代码合集
The text was updated successfully, but these errors were encountered: