线程是程序执行的最小单位。它被包含在进程之中,是进程中的实际运作单位。
多线程是指程序可以同一时间运行多个线程,以更加合理地利用系统资源。
iOS中跟UI显示相关的操作都在main线程中。为了不阻塞main线程(卡住UI),通常把耗时工作放在其他线程。
iOS多线程有3种使用方式:NSThread、GCD(Grand Central Dispatch)、NSOperation
因为NSThread要自己管理线程的生命周期和同步、加锁问题,这会导致一定的性能开销,所以开发中通常使用GCD和NSOperation
NSThread
动态方法创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
// 手动启动线程
[thread start];
静态方法创建
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
隐式创建
[self performSelectorInBackground:@selector(run) withObject:nil];
获取当前线程
NSThread *current = [NSThread currentThread];
在主线程上执行操作
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];
多线程的安全隐患
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
线程同步互斥、加锁
为了防止因多线程抢夺资源造成的数据安全问题,需要使用互斥锁来实现多线程的同步(不同线程的任务按一定的顺序执行)
- (void)began{
// 座位一共15个
self.seat = 15;
// 开启一个线程
NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(reserveSeat) object:nil];
thread1.name = @"thread1";
[thread1 start];
// 开启一个线程
NSThread *thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(reserveSeat) object:nil];
thread2.name = @"thread2";
[thread2 start];
}
-(void)reserveSeat{
// 我们必须座位预定完 也就是一直循环 直到seat属性没有值
while (true) {
// 注意,锁一定要是所有线程共享的对象
// 如果代码中只有一个地方需要加锁,大多都使用 self
@synchronized(self) {
// 判断如果座位大于0 客户就可以预订
if(self.seat > 0)
{
NSLog(@"预定%d号座位 ------%@",self.seat,[NSThread currentThread]);
self.seat --;
}else{
NSLog(@"没有座位了 ------%@",[NSThread currentThread]);
break;
}
}
}
}
GCD
纯C语言线程库,提供了非常多强大的函数
GCD是苹果公司为多核的并行运算提出的解决方案
GCD会自动利用更多的CPU内核(比如双核、四核)
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
GCD是纯C语言的,因此我们在编写GCD相关代码的时候,面对的函数,而不是方法。
GCD中的函数大多数都以dispatch开头。
使用GCD之前需要理解以下几个概念:
串行队列:队列中的任务是一个一个按存放顺序取出来的,前一个任务没执行完,后一个任务不能取出。
并行队列:队列中的任务可以同时取出,前一个任务刚开始执行,后一个任务就能取出了
注意:队列只是管理任务的调度,不负责执行
同步执行:会阻塞线程,直到block中的任务执行完毕。并且只在main线程中执行
异步执行:当前线程会直接往下执行,它不会阻塞当前线程。只要有空闲的线程(只要系统允许,就能创建出新的空闲线程),就拿来执行
线程池:管理系统的线程。GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用的话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护
GCD的使用
自定义串行队列
dispatch_queue_t queue0 = dispatch_queue_create("com.deeepthinking", DISPATCH_QUEUE_SERIAL);
// 同步执行
dispatch_sync(queue0, ^{
});
// 异步执行
dispatch_async(queue0, ^{
});
“com.deeepthinking”是你自定义队列的标识,用来debug用,可以自己设置。
自定义并行队列
dispatch_queue_t queue0 = dispatch_queue_create("com.deeepthinking", DISPATCH_QUEUE_CONCURRENT);
// 同步执行
dispatch_sync(queue0, ^{
});
// 异步执行
dispatch_async(queue0, ^{
});
串行队列+同步执行,任务一个一个执行;
串行对列+异步执行,任务一个一个执行;
并行队列+同步执行,任务一个一个执行;
并行队列+异步执行,任务同时进行;
所以要实现真正的并发,要使用并行队列+异步执行的方式。
将任务放到主队列中:
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(main_queue, ^{
});
主队列是个特殊的串行队列,通常用来刷新UI
不管是同步执行还是异步执行,主队列是一定在main线程中执行的。但是同步执行会阻塞main线程,有可能还会造成死锁,所以将任务放到主线程执行时,要用异步执行。比如获取图片并更新到UI:
dispatch_queue_t queue1 = dispatch_queue_create("dpt", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue1, ^{
UIImage *image = [self downloadImage];
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
使用全局队列:
这是系统提供的一个并行队列,方便开发者调用。通常我们做并发任务都是加入到这个队列。
dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
dispatch_async(queue1, ^{
UIImage *image = [self downloadImage];
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
除此之外,GCD还有其他一些功能
一次性执行,用来做单例的实例初始化正好:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
延迟执行
// 延迟2秒执行:
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// code to be executed on the main queue after delay
});
合并汇总执行,就是任务执行完,会等其他任务执行完,然后执行总任务:
// 合并汇总结果
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
// 并行执行的线程一
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
// 并行执行的线程二
});
dispatch_group_notify(group, dispatch_get_global_queue(0,0), ^{
// 汇总结果
});
NSOperation
NSOperation 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。 大家可以看到 NSOperation 和 NSOperationQueue 分别对应 GCD 的任务和队列 。操作步骤也很好理解:
将要执行的任务封装到一个 NSOperation 对象中。
将此任务添加到一个 NSOperationQueue 对象中。
然后系统就会自动在执行任务。
NSOperation 只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务。分别是:NSInvocationOperation 和 NSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会默认同步执行。
NSInvocationOperation *invoOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationRun) object:nil];
[invoOperation start];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"#current thread:%@", [NSThread currentThread]);
}];
[blockOperation start];
NSBlockOperation 还有一个方法addExecutionBlock: ,通过这个方法可以给 Operation 添加多个执行 Block。这样 Operation 中的任务会并发执行:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
[operation start];
除了上面的两种 Operation 以外,我们还可以自定义 Operation。自定义 Operation 需要继承 NSOperation 类,并实现其 main() 方法,因为在调用 start() 方法的时候,内部会调用 main() 方法完成相关逻辑。所以如果以上的两个类无法满足你的欲望的时候,你就需要自定义了。你想要实现什么功能都可以写在里面。除此之外,你还需要实现 cancel() 在内的各种方法。
NSOperation允许我们调用-(void)cancel取消一个操作的执行。当然,这个操作并不是我们所想象的取消。这个取消的步骤是这样的,如果这个操作在队列中没有执行,那么这个时候取消并将状态finished设置为YES,那么这个时候的取消就是直接取消了。如果这个操作已经在执行了,那么我们只能等其操作完成。当我们调用cancel方法的时候,他只是将isCancelled设置为YES。所以,在我们的操作中,我们应该在每个操作开始前,或者在每个有意义的实际操作完成后,先检查下这个属性是不是已经设置为YES。如果是YES,则后面操作都可以不用在执行了。
[blockOperation cancel];
操作完成时,将会调用下面这个方法,这样也非常方便的让我们对view进行更新或者添加自己的业务逻辑代码:
[blockOperation setCompletionBlock:^{
NSOperationQueue *queue = [NSOperationQueue mainQueue];
[queue addOperationWithBlock:^{
// 更新UI
}];
}];
NSOperation 有一个非常实用的功能,那就是添加依赖:
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
队列
NSOperation 对象的 start() 方法来启动任务,这样做他们默认是同步执行的。就算是 addExecutionBlock 方法,还是会占用当前线程。这是就要用到队列NSOperationQueue 了。
使用NSOperationQueue默认是并行队列异步执行的:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
[queue addOperation:operation];
在使用NSOperationQueue过程中,不用管串行、并行、同步、异步这些名词。NSOperationQueue 有一个参数 maxConcurrentOperationCount 最大并发数,用来设置最多可以让多少个任务同时执行。当你把它设置为 1 的时候,就是串行队列。