一、进程与线程
进程是系统中正在运行的应用程序,是CPU分配资源和调度的单位,线程是CPU调度(执行任务)的最小单位,一个进程内必须至少包含一个线程。
不同进程拥有不同的内存资源,而在同一进程的不同线程则是共享进程的资源(这也就导致可能出现线程安全问题)。
二、多线程编程
为什么需要使用多线程?在一个APP的项目中,用户所看到的UI界面都是在主线程进行改动,如果我们在主线程的某一个方法中执行了一项耗时操作(比如大量for循环、大批量网络请求等),由于同一线程内的程序是串行顺序执行,因此会导致主线程堵塞,界面卡死。如果我们将这些耗时操作放到一个新的线程中处理,透过线程的并行执行,可以避免主线程阻塞,等到子线程处理完成后,可以利用线程间的通信方法通知主线程。
线程的运行方式有两种:线程同步和线程异步。线程同步是指线程按顺序执行,通常发生在不同线程需要访问一个加锁的数据时;线程异步就是线程并行,通过CPU的快速调度,实现同时运行的效果。
-
iOS中线程的实现方案
技术方案 | 简介 | 语言 | 线程生命周期 |
---|---|---|---|
pthread | 通用的多线程API 适用于Unix\Linux\Windows等系统 跨平台\可移植 使用难度大 |
C | 程序员管理 |
NSThread | 面向对象 简单易用,可以直接操作线程对象 |
OC | 程序员管理 |
GCD | 为了替代NSThread,更底层 有效利用系统多核 |
C | 自动管理 |
NSOperation | 基于GCD 面向对象 比GCD多了一些简单易用的功能 |
OC | 自动管理 |
-
线程的状态
线程可以分为几个状态:新建、就绪、运行、阻塞、死亡。新建线程的时候,会在内存中创建一个线程,这时的线程还不可以被调度。当进入就绪阶段后,线程会被放入可调度线程池,接受cpu的调度。当cpu调度时,线程进入运行阶段。在运行阶段调用sleep或等待同步锁时,会进入阻塞状态,此时线程被移出可调度线程池,直到阻塞条件结束回到就绪状态。当线程内的方法执行结束后或其他异常导致强制退出时,进入死亡状态,进行线程的销毁。
-
线程安全
线程安全问题是指当不同线程同时对一个数据进行请求并修改时,会造成数据处理错误的情况。下面一个存钱取钱的例子便是一个线程安全问题:
现在有两个人(线程):A(线程A)和B(线程B),当A和B在不同的地方同时向银行卡(进程的内存资源)请求余额信息(数据),此时银行卡会告诉他们余额是1000元,接着A先进行了存钱操作,并修改银行卡余额为2000(修改数据),但对于B而言,B所知道的银行卡余额是1000元,此时B再进行取钱操作,并修改银行卡余额为500元,这样银行卡的余额会被覆盖,变成只有500元,很明显得到了一个错误的数据。
为了解决上面提到的数据错误问题,我们需要对数据上锁,当一个线程要访问某个数据且进行修改时,把这个数据锁住,等到该线程修改完毕再解锁让其他线程取用,这个就是互斥锁。
从上面的流程来看,互斥锁使得这些线程按照访问顺序执行,也就是前面提到的线程同步。下面我们用代码实现一个互斥锁(使用NSThread)。
ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController:UIViewController
@end
ViewController.m
#import "ViewController.h"
@interface ViewController:UIViewController
@property (nonatomic, strong)NSThread *thread1;
@property (nonatomic, strong)NSThread *thread2;
@property (nonatomic, strong)NSThread *thread3;
@property (nonatomic, assign)NSInteger num;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// 设置初始数据
self.num = 100;
// 开辟多线程,三个线程都执行func,func内会对self.num进行修改,因而产生线程安全问题
self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(func) object:nil];
self.thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(func) object:nil];
self.thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(func) object:nil];
// 设置线程名称
self.thread1.name = @"线程A";
self.thread2.name = @"线程B";
self.thread3.name = @"线程C";
// 启动线程
[self.thread1 start];
[self.thread2 start];
[self.thread3 start];
}
- (void)func
{
while (1)
{
@synchronize(self) // 加上互斥锁,self是锁对象(要使用全局的对象),通常用self即可
{
NSInteger count = self.num;
if (count > 0) {
self.num = count - 1;
for (int i; i < 1000000; i ++) // 耗时操作
{}
// 打印当前线程名称和num
NSLog(@"%@-----%zd", [NSThread currentThread].name, self.num);
} else {
// 打印当前线程名称和num`
NSLog(@"%@-----%zd", [NSThread currentThread].name, self.num);
break;
}
}
}
}
@end
-
NSThread
线程创建
/*
* 参数说明:
* 第一个参数:目标对象
* 第二个参数:方法选择器(希望线程执行的方法)
* 第三个参数:前一个参数(方法)中要传入参数
* 特点:需要启动线程,可以获得线程对象进行详细设置
*/
NSThread *thread = [NSThread alloc] initWithTarget:self selector:@selector(func) object:nil];
// 直接分离出一条子线程
/*
* 参数说明:
* 第一个参数:方法选择器(希望线程执行的方法)
* 第二个参数:目标对象
* 第三个参数:第一个参数(方法)中要传入参数
* 特点:不需要启动线程,无法得到线程对象进行详细设置
*/
[NSThread detachNewThreadSelector:@selector(func) toTarget:self withObject:nil];
// 开启后台线程
/*
* 参数说明:
* 第一个参数:方法选择器(希望线程执行的方法)
* 第二个参数:第一个参数(方法)中要传入参数
* 特点:不需要启动线程,无法得到线程对象进行详细设置
*/
[self performSelectorInBackground:@selector(func) withObject:nil];
线程启动
[thread start];
得到主线程和当前线程
// 得到主线程
NSThread *mainThread = [NSThread mainThread];
// 得到当前线程
NSThread *currentThread = [NSThread currentThread];
// 判断线程是否为主线程
/*
* 1.取得当前线程的number,主线程的number == 1
* 2.透过isMainThread方法
*/
// 判断number == 1
[NSThread currentThread].number == 1;
// isMainThread方法
[thread isMainThread];
设置线程名称
thread.name = @"线程1";
设置线程优先级
// 优先级是介于0~1的数,0代表低优先级,1代表高优先级,默认是0.5
// 优先级也意味着cpu调用线程的概率,优先级越高,cpu调用的概率就越大
thread.threadPriority = 1;
线程通信
// 子线程向主线程通信
/*
* 参数说明:
* 第一个参数:方法选择器(回到主线程要执行什么方法)
* 第二个参数:前一个参数(方法)中要传递的参数
* 第三个参数:是否等待该方法执行结束才继续往下执行,YES代表等待
*/
[self performSelectorOnMainThread:@selector(func) withObject:nil waitUntilDone:YES];
// 两个线程间通信(不限主线程)
/*
* 参数说明:
* 第一个参数:方法选择器(切换到新线程要执行什么方法)
* 第二个参数:想要回到的线程
* 第三个参数:前一个参数(方法)中要传递的参数
* 第四个参数:是否等待该方法执行结束才继续往下执行,YES代表等待
*/
[self performSelector:@selector(func) onThread:[NSThread mainThread] withObject:nil waitUntilDone:YES];
使用NSThread创建的线程,其生命周期仅限于执行的方法,当方法执行结束后,该线程会被释放。
-
GCD
GCD的核心概念
1. 任务:要做什么
2. 队列:存放任务
使用步骤
1. 创建队列
// 并发队列
/*
* 参数说明:
* 第一个参数:队列的名字,C语言字符串
* 第二个参数:队列类型,这里是并发队列
* 自动开启多线程,同时执行任务
* 开多少条线程不是由任务的数量决定,是GCD内部自己决定的
* 仅在异步函数才有效
*/
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
//全局并发队列
/*
* 参数说明:
* 第一个参数:优先级,一般传入默认的优先级即可
* 第二个参数:未来接口预留参数,现在传0即可
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 串行队列
/*
* 参数说明:
* 第一个参数:队列的名字,C语言字符串
* 第二个参数:队列类型,这里是串行队列,默认是串行,如果写NULL也是串行
* 任务必须一个接着一个执行
*/
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
// 主队列,特殊的串行队列
/*
* 与主线程相关联,凡是放在主队列里的任务都要在主线程中串行执行
* 当同步函数和主队列一起使用时会发生死锁,因为同步函数要等到dispatch_sync函数执行结束才往下运行,而被压进主队列的任务有会马上被拿出来给主线程执行,此时主线程就会出现死锁的情况。
*/
dispatch_queue_t queue = dispatch_get_main_queue();
2. 封装任务
// 使用函数来封装任务
// 同步
/*
* 参数说明:
* 第一个参数:队列
* 第二个参数:想要封装的任务
* 只能在当前线程中执行任务,不具备开启新线程的能力
* 必须等待当前任务完成,才能执行下面的任务
*/
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 异步
/*
* 参数说明:
* 第一个参数:队列
* 第二个参数:想要封装的任务
* 可以在新的线程中执行任务,具备开启新线程的能力
* 不必等待当前任务完成,可以直接执行下面的任务
*/
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
线程间通信(透过嵌套执行)
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 在子线程进行某些操作
NSLog(@"%@",[NSThread currentThread]);
...
dispatch_async(dispatch_get_main_queue(), ^{
// 传递数据到主线程处理
...
NSLog(@"%@",[NSThread currentThread]);
});
});
其他常用函数
// 一次性代码:整个程序运行过程中只会执行一次,线程安全的(内部已加锁)
// 可以应用在单例模式
// 实现原理:透过判断onceToken的值来决定是否执行,onceToken==0代表没有执行过
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"Once----");
});
// 延迟执行
/*
* 参数说明:
* 第一个参数:设置延迟时间(GCD的时间单位是ns)
* 第二个参数:队列(决定任务在哪个队列执行)
* 第三个参数:设置任务
*/
// 实现原理:先等两秒,再将任务放到队列
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", [NSThread currentThread]);
});
// 快速迭代
/*
* 参数说明:
* 第一个参数:遍历的次数
* 第二个参数:队列(不可以使用主队列,会发生死锁;如果使用普通的串行队列,则只会在主线程执行)
* 第三个参数:设置任务,这个block需要接受一个参数,类似于for循环的int i
* 会开启多条子线程和主线程并发的执行任务。
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t i){
NSLog(@"%zd-----%@", i, [NSThread currentThread]);
});
// 栅栏函数
// 由于异步执行时的顺序不能保证,有时候我们希望先执行某些异步操作后,先执行某个任务再执行后续任务,这时候需要栅栏函数来进行拦截。
// 下面的代码段能够保证先并发执行打印1、2(1、2顺序不保证),然后打印stop,再并发执行打印3、4(3、4顺序不保证)
// 不能使用全局并发队列(不能拦截)
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1-------%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2-------%@",[NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"---stop---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3-------%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"4-------%@",[NSThread currentThread]);
});
队列组
// 使用队列组可以监听任务的执行情况
// 下面代码实现所有打印数字结束后(打印数字的任务是并发执行,顺序不保证),再打印stop
// 创建队列组和队列
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue1 = dispatch_queue_create("test1.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("test2.queue", DISPATCH_QUEUE_CONCURRENT);
// 封装任务,添加到队列并监听任务的执行情况
// dispatch_group_async函数是对dispatch_group_enter、dispatch_async、dispatch_group_leave三个函数的封装,需要注意的是dispatch_group_leave必须写在dispatch_async的block中,具体封装如下:
//dispatch_group_enter(group);
//dispatch_async(queue1, ^{
// NSLog(@"1-------%@",[NSThread currentThread]);
// dispatch_group_leave(group);
//});
dispatch_group_async(group, queue1, ^{
NSLog(@"1-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue1, ^{
NSLog(@"2-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue1, ^{
NSLog(@"3-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue2, ^{
NSLog(@"4-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue2, ^{
NSLog(@"5-------%@",[NSThread currentThread]);
});
// 拦截通知,等待所有任务执行完毕才执行,这个函数的queue1只是决定block里任务放在哪个队列中
// dispatch_group_notify内部是异步执行
dispatch_group_notify(group, queue1, ^{
NSLog(@"---stop---%@",[NSThread currentThread]);
});
-
NSOperation
NSOperation的核心概念
1. NSOperation:操作,抽象类,只能将操作封装到其子类中。
2. NSOperationQueue:队列
使用步骤
1. 创建队列
// 自定义队列:并发队列,可以设定成串行队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 主队列:串行队列,和主线程相关
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
2. 将要执行的操作封装到一个NSOperation对象中
// NSInvocationOperation
// 封装操作对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(func) object:nil];
// 执行操作,压入队列不需要执行操作,由队列自动调用,如果直接执行会在当前线程执行,不会开启新的线程
[operation start];
// NSBlockOperation
// 封装操作对象
NSBlockOperation *operation = [NSBlockOperation blockWithOperation:^{
NSLog(@"1-----%@", [NSThread currentThread]);
}];
// 追加任务,当一个操作对象中的任务数量大于1的时候,会开启新的线程来执行`
[operation addExecutionBlock:^{
NSLog(@"2----%@", [NSThread currentThread]);
}];
// 执行操作,压入队列不需要执行操作,由队列自动调用,如果直接执行会在当前线程执行,不会开启新的线程
[operation start];
3. 将NSOperation对象添加到NSOperationQueue中
[queue addOperation:operation];
// 使用NSBlockOperation有较为简便的写法,不需要前面的封装操作
// 实现原理:先将block封装成NSBlockOperation,再将NSBlockOperation压入队列
[queue addObjectWithBlock:^{
NSLog(@"1-----%@", [NSThread currentThread]);
}];
设置队列的最大并发数量
// num为数字,当设置成1的时候相当于串行队列,但线程数可能不为1,只是代表同时只有一个线程在运行
// 默认为-1,代表系统认为要开多少就开多少,不受限制
// 设置最大并发数==1时,只能控制单任务的操作顺序执行,如果一个操作中有追加的任务,会不受最大并发数的影响
queue.maxConcurrentOperationCount = num;
队列的挂起和取消
// 暂停队列,要等到当前操作结束才能暂停,当前操作不可分割
[queue setSuspended:YES];
// 恢复队列
[queue setSuspended:NO];
// 取消队列,只能取消队列中等待执行的操作,正在执行的操作不能中断
// 如果想要取消当前操作,可以在操作中判断isCancelled属性,因为cancelAllOperations方法会修改所有队列中操作的的isCancelled属性
[queue cancelAllOperations];
自定义NSOperation子类
// 继承自NSOperation
// 重写main方法
// 使用的时候可以直接用alloc
// 好处:可以复用大量操作
// 需要注意的是,当main中执行了一段耗时任务时,建议判断一下isCancelled属性
- (void)main
{
NSLog(@"main----%@", [NSThread currentThread]);
}
操作队列的依赖和监听
// operation1依赖于operation2,代表operation2先于operation1执行
// 设置依赖必须在添加到队列前设置
// 不能设置循环依赖
// 可以设置跨队列依赖
[operation1 addDependency:operation2];
// 监听任务执行完毕
operation.completionBlock = ^{
NSLog(@"---Finish---");
};
线程中通信
NSBlockOperation *operation = [NSBlockOperation blockWithOperation:^{
// 执行某些任务
NSLog(@"1-----%@", [NSThread currentThread]);
...
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 主线程内执行某些任务`
NSLog(@"main-----%@", [NSThread currentThread]);
...
}];
}];
// 把操作添加到队列
[queue addOperation:operation];
-
GCD和NSOperation的比较
- GCD是C语言的API,NSOperation是Objective-C的对象。
- 在GCD中,任务用block封装,是一个轻量级的数据结构;NSOperation的操作用NSOperation封装,是一个重量级的数据结构。
- NSOperationQueue可以取消操作,GCD则无法。
- NSOperation可以指定依赖关系。
- NSOperation可以通过KVO对NSOperation对象进行控制。
- NSOperation可以制定操作的优先级。
- 可以自定义NSOperation来实现操作复用。
网友评论