多线程编程基础
iOS 多线程
进程与线程
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,是正在运行的程序的实例(An instance of a computer program that is being executed)。
进程的概念主要有两点:
- 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(Text Region)、数据区域(Data Region)和堆栈(Stack Region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区存储着活动过程调用的指令和本地变量。
- 进程是一个执行中的程序。
线程
线程是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针、寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属一个进程的其它线程共享进程所拥有的全部资源。
进程与线程的关系与区别
进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或正在使用它们。另外,进程也是抢占处理机的基本单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。
与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程的资源。线程只由相关堆栈、寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其它线程的相关变量。
通常在一个进程中可以包含多个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更叫高效的提高系统内多个程序之间并发执行的程度。
线程和进程区别:
- 地址空间和其它资源:进程之间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
- 通信:进程间通信主要方式有管道、系统IPC(消息队列、信号、共享存储)、套接字(Socket)。而线程间可以直接读写进程数据段来进行通信(需要进程同步和互斥手段的辅助,以保证数据的异质性)
- 调度和切换:线程上下文切换比进程快的多。
- 在多线程OS中,进程不是一个可执行的实体
线程安全
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其它线程不能进行访问直到该线程读取完,其它线程才可以使用。
iOS中的多线程
目前在iOS中有四种多线程解决方案:
- Pthreads
- NSThread
- GCD
- NSOperation & NSOperationQueue
Pthreads
POSIX线程,简称Pthreads,是线程的POSIX标准。该标准定义了创建和操作线程的一整套API。在类Unix操作系统中,都使用Pthreads作为操作系统的线程。
NSThread
NSThread是经常Apple封装的完全面向对象的。你可以直观方便的操控线程对象,但是生命周期需要手动管理。
GCD
GCD是Apple为多核的并行运算提出的的解决方案,能够自动合理地利用更多的CPU内核,并自动管理线程的声明周期。GCD使用C进行编写,并使用了Block。
####任务
即代码所要完成的操作。
任务的执行方式有两种:同步执行和异步执行。
同步执行操作,它会阻塞当前线程并等待任务执行完毕,然后当前线程才会继续往下运行。
异步执行操作,则不会阻塞当前线程,当前线程会直接往下执行。
队列
用于存放要执行的任务。
串行队列中的任务,GCD会遵循FIFO原则来执行,串行队列的同步执行任务,会在当前线程一个一个执行,而异步执行任务,则会在它线程中一个一个执行。
并行队列中的任务执行顺序则要复杂一点,任务会根据同步或异步有不同的执行方式。并行队列中的同步执行任务会在当前线程中一个一个执行,而异步执行则会开很多线程一起执行。
如何创建队列?
- 主队列:特殊的串行队列,主要用于刷新UI,任何需要刷新UI的工作都必须在主队列中执行。
dispatch_queue_t mainQueue = ispatch_get_main_queue();
自己创建的队列:
创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("CustomSerialQueue",DISPATCH_QUEUE_SERIAL);
创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("CustomConcurrentQueue",DISPATCH_QUEUE_CONCURRENT);
- 全局队列:这是一个并行队列,并行任务一般都加入到这个队列。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)
如何创建任务?
创建同步任务
dispatch_sync(,^{// execute code));创建异步任务
dispatch_async(,^{// execute code});
队列组
队列组可以将很多队列添加到一个组里,当组中的所有任务都执行完了,队列组将会通知给用户。
dispatch_group_enter 和 dispatch_group_leave 分别表示一个任务追加到队列组和一个任务执行完毕离开了队列组。
只有当group中未执行完毕的任务数量为0时,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 的任务。
1 | dispatch_group_t group = dispatch_group_create(); |
GCD的信号量机制
并发队列可以分配多个线程,同时处理不同的任务,虽然提升了效率,但是多线程的并发是通过时间片轮转的方法实现的,线程的创建、销毁、上下文切换等会消耗资源。适当的并发可以提高效率,但是无节制的并发,则会抢占CPU资源,造成性能下降。此外,提交给并发队列的任务中,有些任务内部会有全局的锁,会导致线程休眠、阻塞,一旦这类任务过多,并发队列还需要创建新的线程来执行其它任务,会造成线程数量的增加。
因此控制并发队列中的线程数量就成了不能忽视的问题。
GCD并发线程数量控制
GCD中的信号量(dispatch_semaphore)是一个整形值,有初始计数值,可以接收通知信号和等待信号。当信号量收到通知信号时,计数+1;当信号量收到等待信号时,计数-1。如果信号量为0,线程会被阻塞,直到信号量大于0,才会继续执行。
使用信号量机制可以实现线程的同步,也可以控制最大并发数。
使用GCD信号量机制实现并发线程数量控制
1 | dispatch_queue_t workConcurrentQueue = dispatch_queue_create(@"WORK_CONCURRENT_QUEUE",DISPATCH_QUEUE_CONCURRENT); |
使用GCD信号量机制实现线程同步
有时候我们会遇到需要异步执行一些耗时任务,并在这些任务完成后进行一些额外的操作,相当于将异步执行任务转化为同步执行任务。
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
使用GCD信号量机制实现线程安全
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其它变量的值也和预期的是一样的,就是线程安全的。
1 | dispatch_semaphore_t semaphoreLock = dispatch_semaphore_create(1); |
GCD的一些使用场景
使用GCD实现延迟执行
1 | dispatch_queue_t queue = dispatch_ger_gloabl_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
使用GCD实现单例模式
1 | static id _instance; |
任务同步
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。
dispatch_barrier_async 函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
1 | dispatch_queue_t queue = dispatch_queue_create("com.codelei.queue", DISPATCH_QUEUE_CONCURRENT); |
直到 Task 1 和 Task 2 执行完成后,才会执行使用 dispatch_barrier_asynce 追加的任务 Task 3,然后在 Task 3 执行完成后,并行队列会正常执行。
NSOperation & NSOperationQueue
NSOperation是Apple对GCD的封装,完全面向对象。NSOperation和NSOperationQueue分别对应GCD的任务和队列。
添加任务
NSOperation只是一个抽象类,并不能直接封装任务。它有两个子类NSInvocationOperation和NSBlockOperation用来完成封装任务的操作。创建一个Operation后,需要调用start方法来启动任务,它默认在当前队列同步执行。如果需要在执行途中取消执行一个任务,调用cancel方法即可。
NSInvocationOperation
1
2NSInvocationOperation *invocationOperation = [[NSInvocation alloc] initWithTarget:self selector:@selector(executeMethod)];
invocationOperation start];NSBlockOperation
1
2
3
4
5NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
[blockOperation start];上面提到,NSInvocationOperation和NSBlockOperation创建的任务默认在当前线程执行,但是NSBlockOperation可以通过
addExecutionBlock:方法向Operation中添加多个可执行的Block。这样的Operation中的任务会并发执行,它会在主线程和其它多个线程执行这些任务。1
2
3
4
5
6
7NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
[blockOpertation addExecutionBlock:^{
// execute code
}];
[blockOperation start];NOTE:
addExecutionBlock:方法必须在start方法之前执行,否则会报错。自定义的Operation
自定义Operation类需要继承Operation类,并实现其
main()方法,因为在调用start()方法的时候,内部会调用main()方法完成相关逻辑。
创建队列
通过调用一个NSOperation类的start()方法来启动的任务,默认在当前线程同步执行。如果要避免占用当前线程,就需要使用到队列NSOperationQueue。只要将Operation添加到队列,就会自动调用任务的start()方法。
主队列
添加到主队列中的任务时串行执行的。
1
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
其它队列
其它队列中的任务会在其它线程并行执行。
1
2
3
4
5NSOperationQueue *queue = [NSOperatin alloc] init];
NSBlockOperatin *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}]
[queue addOperation:blockOperation];如果需要任务在队列中串行执行,可以通过设置NSOperationQueue的
maxConcurrentOperationCount来实现。1
2NSOperationQueue *queue = [NSOperatin alloc] init];
[queue setMaxConcurrentOperationCount:1];你还可以通过
addOperationWithBlock:方法来向队列中添加新任务。1
2
3
4NSOperationQueue *queue = [NSOperatin alloc] init];
[queue addOperationWithBlock:^{
// cxecute code
}]
添加任务依赖
1 | NSBlockOperation *blockOperationFirst = [NSBlockOperation blockOperationWithBlock:^{ |
NOTE:
- 添加相互依赖会造成死锁。
- 使用
removeDependency方法来移除依赖关系
其它方法
NSOperation类的一些其它方法
1
2
3
4
5
6
7
8
9
10// 判断任务是否正在执行
BOOL exccuting;
// 判断任务是否完成
BOOL finished;
// 设置任务完成后的后续操作
void (^completionBlock) (void);
// 取消任务
- (void)cancle;
// 阻塞当前线程直到此任务执行完毕
- (void)waitUntilFinished;NSOperationQueue
1
2
3
4
5
6
7
8// 获取队列的任务数量
NSUInteger operationCount;
// 取消队列中的所有任务
- (void)cancelAllOperations;
// 阻塞当前线程直到此队列中的所有任务执行完毕
- (void)waitUntilAllOperationsAreFinished;
// 暂停或继续队列
BOOL suspended;