进程与线程
进程:计算机操作系统分配资源的单位,是指系统中正在运行的应用程序,进程之间相互独立,运行在受保护的内存空间,比如同时打开XCode、QQ,系统就会启动两个进程;
线程:进程的基本执行单元,一个进程中的任务都在线程中执行;
并发与并行
并发:并发的关键是具有处理多个任务的能力,不一定要同时;
并行:并行的关键是你有同时处理多个任务的能力。
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你一手筷子,一手电话,说一句话,咽一口饭。这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这就需要两张嘴,也就是多核CPU,这说明你支持并行。
同步与异步
同步方法就是我们平时调用的哪些方法。因为任何有编程经验的人都知道,比如在第一行调用foo()方法,那么程序运行到第二行的时候,foo方法肯定是执行完了。
所谓的异步,就是允许在执行某一个任务时,函数立刻返回,但是真正要执行的任务稍后完成。比如我们在点击保存按钮之后,要先把数据写到磁盘,然后更新UI。同步方法就是等到数据保存完再更新UI,而异步则是立刻从保存数据的方法返回并向后执行代码,同时真正用来保存数据的指令将在稍后执行。
多线程优缺点
- 优点:能适当提高程序的执行效率,能适当提高资源利用率(CPU、内存利用率)
- 缺点:创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间
如果开启大量的线程,会降低程序的性能,线程越多,CPU在调度线程上的开销就越大。
程序设计更加复杂:比如线程之间的通信、多线程的数据共享等问题。
iOS中多线程解决方案
1. pthread
pthread 是一套通用的多线程的 API,可以在Unix / Linux / Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用较为复杂,我们在 iOS 开发中几乎不使用 pthread,我们可以稍作了解。
2. NSThread
NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用[NSThread currentThread]来显示当前的进程信息。
创建方式
-
先创建再启动
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething1:) object:@"NSThread1"]; [thread1 start];
-
创建线程后自动启动
[NSThread detachNewThreadSelector:@selector(doSomething2:) toTarget:self withObject:@"NSThread2"];
-
隐式创建线程,直接启动
[self performSelectorInBackground:@selector(doSomething3:) withObject:@"NSThread3"];
相关方法
-
类方法
// 当前线程 [NSThread currentThread]; // 打印结果:{number = 1, name = main} NSLog(@"%@",[NSThread currentThread]); //休眠多久 [NSThread sleepForTimeInterval:2]; //休眠到指定时间 [NSThread sleepUntilDate:[NSDate date]]; //退出线程 [NSThread exit]; //判断当前线程是否为主线程 [NSThread isMainThread]; //判断当前线程是否是多线程 [NSThread isMultiThreaded]; //主线程的对象 NSThread *mainThread = [NSThread mainThread];
-
实例方法
//线程是否在执行 thread.isExecuting; //线程是否被取消 thread.isCancelled; //线程是否完成 thread.isFinished; //是否是主线程 thread.isMainThread; //线程的优先级,取值范围0.0到1.0,默认优先级0.5,1.0表示最高优先级,优先级高,CPU调度的频率高 thread.threadPriority;
线程间通信
在开发中,线程往往不是孤立存在的,多个线程之间需要经常进行通信我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。我们先来了解一下官方关于 NSThread 的线程间通信的方法。
// 在主线程上执行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
// equivalent to the first method with kCFRunLoopCommonModes
// 在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
下面通过一个经典的下载图片 DEMO 来展示线程之间的通信。具体步骤如下:
1.开启一个子线程,在子线程中下载图片。
2.回到主线程刷新 UI,将图片展示在 UIImageView 中。
代码如下:
/**
* 创建一个线程下载图片
*/
- (void)downloadImageOnSubThread {
// 在创建的子线程中调用downloadImage下载图片
[NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}
/**
* 下载图片,下载完之后回到主线程进行 UI 刷新
*/
- (void)downloadImage {
NSLog(@"current thread -- %@", [NSThread currentThread]);
// 1. 获取图片 imageUrl
NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"];
// 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
// 通过二进制 data 创建 image
UIImage *image = [UIImage imageWithData:imageData];
// 3. 回到主线程进行图片赋值和界面刷新
[self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
}
/**
* 回到主线程进行图片赋值和界面刷新
*/
- (void)refreshOnMainThread:(UIImage *)image {
NSLog(@"current thread -- %@", [NSThread currentThread]);
// 赋值图片到imageview
self.imageView.image = image;
}
线程安全
多线程安全隐患的原因:一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好比几个人在同一时修改同一个表格,造成数据的错乱。
解决方法:
-
添加互斥锁:
@synchronized(锁对象) { // 需要锁定的代码 }
iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock等等各种方式。判断的时候锁对象要存在,如果代码中只有一个地方需要加锁,大多都使用self作为锁对象,这样可以避免单独再创建一个锁对象。加了互斥做的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠。
- 自旋锁:
加了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方式,一直等待锁定的代码执行完成。相当于不停尝试执行代码,比较消耗性能。
属性修饰atomic本身就有一把自旋锁:
nonatomic 非原子属性,同一时间可以有很多线程读和写
atomic 原子属性(线程安全),保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值),atomic 本身就有一把锁(自旋锁)
atomic:线程安全,需要消耗大量的资源
nonatomic:非线程安全,不过效率更高,一般使用nonatomic
下面通过一个售票实例来看一下锁的作用:
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread01;
@property(nonatomic,strong)NSThread *thread02;
@property(nonatomic,strong)NSThread *thread03;
@property(nonatomic,assign)NSInteger numTicket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 总票数为30
self.numTicket = 30;
self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread01.name = @"售票员01";
self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread02.name = @"售票员02";
self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread03.name = @"售票员03";
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.thread01 start];
[self.thread02 start];
[self.thread03 start];
}
// 售票
-(void)saleTicket
{
while (1) {
// 锁对象,本身就是一个对象,所以self就可以了
// 锁定的时候,其他线程没有办法访问这段代码
@synchronized (self) {
// 模拟售票时间,我们让线程休息0.05s
[NSThread sleepForTimeInterval:0.05];
if (self.numTicket > 0) {
self.numTicket -= 1;
NSLog(@"%@卖出了一张票,还剩下%zd张票",[NSThread currentThread].name,self.numTicket);
}else{
NSLog(@"票已经卖完了");
break;
}
}
}
}
@end
我们可以看到没有加锁时有的票被多卖了,显然不对,接下来看看加锁的结果:
加上互斥锁后,就不会出现数据错乱的情况了。
GCD
GCD是苹果公司为多核的并行运算提出的解决方案,它可以自动管理线程的生命周期(创建线程、调度任务、销毁线程),我们只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。
GCD中的任务与队列
任务:GCD以block为基本单位,一个block中的代码可以为一个任务。
任务有两种执行方式: 同步函数 和 异步函数,他们之间的区别是:
- 同步:只能在当前线程中执行任务,不具备开启新线程的能力,任务立刻马上执行,会阻塞当前线程并等待 Block中的任务执行完毕,然后当前线程才会继续往下运行
- 异步:可以在新的线程中执行任务,具备开启新线程的能力,但不一定会开新线程,当前线程会直接往下执行,不会阻塞当前线程
队列:装载线程任务的队形结构。(系统以先进先出的方式调度队列中的任务执行)。在GCD中有两种队列:串行队列和并发队列。
- 串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
- 并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步(dispatch_async)函数下才有效。
GCD的使用分为两步:
- 添加任务;
- 将任务放到指定的队列中,GCD自动将任务取出放到对应的线程中执行。
GCD的创建
- 创建队列
使用dispatch_queue_create来创建队列对象,传入两个参数,第一个参数表示队列的唯一标识符,可为空。第二个参数用来表示队列的类型,串行队列(DISPATCH_QUEUE_SERIAL)或并发队列(DISPATCH_QUEUE_CONCURRENT)。
串行队列:
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
并发队列:
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);
全局并发队列:GCD默认已经提供了全局并发队列,供整个应用使用,可以无需手动创建:
/**
第一个参数:优先级 也可直接填后面的数字
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
第二个参数: 预留参数 0
*/
dispatch_queue_t quque1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主队列:GCD 提供了的一种特殊的串行队列,主队列负责在主线程上调度任务,如果在主线程上已经有任务正在执行,主队列会等到主线程空闲后再调度任务。通常是返回主线程更新UI的时候使用。dispatch_get_main_queue():
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 耗时操作放在这里
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程进行UI操作
});
});
- 执行任务
同步(Synchronize)使用dispatch_sync;
/*
第一个参数:队列
第二个参数:block,在里面封装任务
*/
dispatch_sync(queue, ^{
});
异步(asynchronous)使用dispatch_async;
dispatch_async(queue, ^{
});
GCD的使用:队列和任务的组合
当在主队列中加入同步函数的时候,会造成死锁。
//1.获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
//2.同步函数
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
主队列在执行dispatch_sync,随后队列中新增一个任务block。因为主队列是同步队列,所以block要等dispatch_sync执行完才能执行,但是dispatch_sync是同步派发,要等block执行完才算是结束。在主队列中的两个任务互相等待,导致了死锁。
解决方案:其实在通常情况下我们不必要用dispatch_sync,因为dispatch_async能够更好的利用CPU,提升程序运行速度。只有当我们需要保证队列中的任务必须顺序执行时,才考虑使用dispatch_sync。在使用dispatch_sync的时候应该分析当前处于哪个队列,以及任务会提交到哪个队列。
注意:GCD中开多少条线程是由系统根据CUP繁忙程度决定的,如果任务很多,GCD会开启适当的子线程,并不会让所有任务同时执行。
- GCD线程间的通信非常简单,使用同步或异步函数,传入主队列即可(就像上面介绍主队列时那样):
-(void)downloadImage
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
// 获得图片URL
NSURL *url = [NSURL URLWithString:@"//upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
// 将图片URL下载为二进制文件
NSData *data = [NSData dataWithContentsOfURL:url];
// 将二进制文件转化为image
UIImage *image = [UIImage imageWithData:data];
NSLog(@"%@",[NSThread currentThread]);
// 返回主线程 这里用同步函数不会发生死锁,因为这个方法在子线程中被调用。
// 也可以使用异步函数
dispatch_sync(dispatch_get_main_queue(), ^{
self.imageView.image = image;
NSLog(@"%@",[NSThread currentThread]);
});
});
}
GCD其他常用方法
1. 栅栏函数(控制任务的执行顺序)
当任务需要异步进行,但是这些任务需要分成两组来执行,第一组完成之后才能进行第二组的操作。这时候就用了到GCD的栅栏方法dispatch_barrier_async:
-(void)barrier
{
//1.创建队列(并发队列)
dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
}
});
//栅栏函数
dispatch_barrier_async(queue, ^{
NSLog(@"这是一个栅栏函数,34任务在12之后进行");
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
}
});
}
2. 延迟执行
/*
第一个参数:延迟时间
第二个参数:要执行的代码
如果想让延迟的代码在子线程中执行,也可以更改在哪个队列中执行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"---%@",[NSThread currentThread]);
});
当然,除了GCD以外我们还有其他的方法:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
3.使代码只执行一次
//整个程序运行过程中只会执行一次
//onceToken用来记录该部分的代码是否被执行过
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"-----");
});
这个用法一般用在单例模式中。
4.dispatch_apply(快速迭代)
dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API,该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等到全部的处理执行结束:
- (void)dispatchApplyTest1 {
//生成全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/**
* @param 10 指定重复次数 指定10次
* @param queue 追加对象的Dispatch Queue
* @param index 带有参数的Block, index的作用是为了按执行的顺序区分各个Block
*
*/
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu-----%@", index, [NSThread currentThread]);
});
NSLog(@"finished");
}
可以看到该函数开启了多个线程执行block里的操作,我们可以利用这个特性模拟循环完成快速迭代遍历(无序):
- (void)dispatchApplyTest2 {
//1.创建NSArray类对象
NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g", @"h", @"i", @"j"];
//2.创建一个全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//3.通过dispatch_apply函数对NSArray中的全部元素进行处理,并等待处理完成,
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
});
NSLog(@"finished");
}
队列组
异步执行几个耗时操作,当这几个操作都完成之后再回到主线程进行操作,这是就要用到队列组了,队列组可以用来管理队列中任务的执行。
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并行队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 执行队列组任务
dispatch_group_async(group, queue, ^{
});
//队列组中的任务执行完毕之后,执行该函数
dispatch_group_notify(group, queue, ^{
});
将两张图片分别下载完成后,合成一张图片并显示的例子:
-(void)GCDGroup
{
//下载图片1
//创建队列组
dispatch_group_t group = dispatch_group_create();
//1.开子线程下载图片
//创建队列(并发)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
//1.获取url地址
NSURL *url = [NSURL URLWithString:@"https://upload-images.jianshu.io/upload_images/1689172-61b8a20c108f539d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
//2.下载图片
NSData *data = [NSData dataWithContentsOfURL:url];
//3.把二进制数据转换成图片
self.image1 = [UIImage imageWithData:data];
NSLog(@"1---%@",self.image1);
});
//下载图片2
dispatch_group_async(group, queue, ^{
//1.获取url地址
NSURL *url = [NSURL URLWithString:@"https://upload-images.jianshu.io/upload_images/1689172-2a0505c7992fd970.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
//2.下载图片
NSData *data = [NSData dataWithContentsOfURL:url];
//3.把二进制数据转换成图片
self.image2 = [UIImage imageWithData:data];
NSLog(@"2---%@",self.image2);
});
//合成,队列组执行完毕之后执行
dispatch_group_notify(group, queue, ^{
//开启图形上下文
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
//画1
[self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
//画2
[self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
//根据图形上下文拿到图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
//关闭上下文
UIGraphicsEndImageContext();
//回到主线程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
NSLog(@"%@--刷新UI",[NSThread currentThread]);
});
});
}
GCD信号量(dispatch_semaphore)
信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。主要有三个方法:
//创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_create(long value)
//等待,降低信号量
dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout)
//提高信号量,这个函数会使传入的信号量dsema的值加1
dispatch_semaphore_signal(dispatch_semaphore_t semaphore)
关于信号量,可以用停车来比喻。
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
我们看个例子,假设现在系统有两个空闲资源可以被利用,但同一时间却有三个线程要进行访问,这时利用GCD信号量代码如下:
-(void)dispatchSignal{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 1");
sleep(1);
NSLog(@"complete task 1");
dispatch_semaphore_signal(semaphore);
});
//任务2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 2");
sleep(1);
NSLog(@"complete task 2");
dispatch_semaphore_signal(semaphore);
});
//任务3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 3");
sleep(1);
NSLog(@"complete task 3");
dispatch_semaphore_signal(semaphore);
});
}
我们可以看到任务1和任务3首先抢到了这两块资源,有任务完成后才轮到任务二。
接下来我们还是以售卖车票为例,用信号量怎么实现加锁功能:
dispatch_semaphore_t semaphore;
- (void)viewDidLoad {
[super viewDidLoad];
// 总票数为30
self.numTicket = 35;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread currentThread].name = @"售票员1";
[self saleTicket];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread currentThread].name = @"售票员2";
[self saleTicket];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread currentThread].name = @"售票员3";
[self saleTicket];
});
semaphore = dispatch_semaphore_create(1);
}
// 售票
-(void)saleTicket
{
while (1) {
[NSThread sleepForTimeInterval:0.05];
if (self.numTicket > 0) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
self.numTicket -= 1;
NSLog(@"%@卖出了一张票,还剩下%zd张票",[NSThread currentThread].name,self.numTicket);
}else{
NSLog(@"票已经卖完了");
break;
}
dispatch_semaphore_signal(semaphore);
}
}
信号量属于底层工具。它非常强大,但在多数需要使用它的场合,最好从设计角度重新考虑,看是否可以不用。应该优先考虑是否可以使用诸如操作队列这样的高级工具。通常可以通过增加一个分派队列dispatch_suspend,或者通过其他方式分解操作来避免使用信号量。信号量并非不好,只是它本身是锁,能不用锁就不要用。尽量用cocoa框架中的高级抽象,信号量非常接近底层。但有时候,例如需要把异步任务转换为同步任务时,信号量是最合适的工具。
NSOperation
NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能。NSOperation需要配合NSOperationQueue来实现多线程。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。
使用步骤:
- 将需要执行的操作封装到一个NSOperation对象中;
- 将NSOperation对象添加到NSOperationQueue中,系统会自动将NSOperationQueue中的NSOperation取出来,并将取出的NSOperation封装的操作放到一条新线程中执行。
NSOperation是个抽象类,实际运用时中需要使用它的子类,有三种方式:
- 使用子类NSInvocationOperation
- 使用子类NSBlockOperation
- 定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务。
NSOperation 的创建
- NSInvocationOperation
/*
第一个参数:目标对象
第二个参数:选择器,要调用的方法
第三个参数:方法要传递的参数
*/
NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//启动操作
[op start];
- NSBlockOperation
//1.封装操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
//要执行的操作,在主线程中执行
NSLog(@"1------%@",[NSThread currentThread]);
}];
//2.追加操作,追加的操作在子线程中执行,可以追加多条操作
[op addExecutionBlock:^{
NSLog(@"---download2--%@",[NSThread currentThread]);
}];
[op start];
NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。如果添加的操作多的话,blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。
- 自定义继承自 NSOperation 的子类
如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。重写main方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished。当 main 执行完返回的时候,这个操作就结束了。
定义:
// YSCOperation.h 文件
#import <Foundation/Foundation.h>
@interface JYHOperation : NSOperation
@end
//JYHOperation.m 文件
#import "JYHOperation.h"
@implementation JYHOperation
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:3];
NSLog(@"%@", [NSThread currentThread]);
}
}
}
@end
使用
- (void)useCustomOperation {
// 1.创建 Operation 对象
JYHOperation *op = [[JYHOperation alloc] init];
// 2.调用 start 方法开始执行
[op start];
}
在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类以及使用NSInvocationOperation的情况下,是在主线程执行操作,并没有开启新线程,接下来看看怎么将操作添加到队列中去。
创建NSOperationQueue
一共有两种队列:
- 主队列:通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行;
- 非主队列:通过 alloc init创建,非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行
将操作添加到队列的方式也有两种:
- 先创建操作,再将创建好的操作加入到创建好的队列中去:
-(void)addOperation:(NSOperation *)op;
- 无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中:
- (void)addOperationWithBlock:(void (^)(void))block;
将操作加入到操作队列后能够开启新线程,并发执行。并且将操作添加到NSOperationQueue中,就会自动启动,不需要再自己启动了。
NSOperationQueue控制串行、并行
NSOperationQueue有个关键属性 maxConcurrentOperationCount,叫做最大并发操作数,用来控制一个特定队列中可以有多少个操作同时参与并发执行。
- maxConcurrentOperationCount默认为-1,直接并发执行,所以加入到‘非队列’中的任务默认就是并发,开启多线程。
- 当maxConcurrentOperationCount为1时,则表示不开线程,也就是串行。
- 当maxConcurrentOperationCount大于1时,进行并发执行。
- 系统对最大并发数有一个限制,所以即使把maxConcurrentOperationCount设置的很大,系统也会自动调整。所以把最大并发数设置的很大是没有意义的。
- maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。
NSOperation 操作依赖
NSOperation能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。
NSOperation 提供管理依赖的接口:
- 添加依赖:
- (void)addDependency:(NSOperation *)op;
- 移除依赖:
- (void)removeDependency:(NSOperation *)op;
比如说有 A、B 两个操作,其中 A 执行完操作,B 才能执行操作。
如果使用依赖来处理的话,那么就需要让操作 B 依赖于操作 A:
- (void)addDependency {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}
NSOperation、NSOperationQueue 常用属性和方法
- NSOpreation
// 开启线程
- (void)start;
- (void)main;
// 判断线程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消当前线程
- (void)cancel;
//NSOperation任务是否在运行
@property (readonly, getter=isExecuting) BOOL executing;
//NSOperation任务是否已结束
@property (readonly, getter=isFinished) BOOL finished;
// 添加依赖
- (void)addDependency:(NSOperation *)op;
// 移除依赖
- (void)removeDependency:(NSOperation *)op;
// 优先级
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
// 操作监听
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 阻塞当前线程,直到该NSOperation结束。可用于线程执行顺序的同步
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
// 获取线程的优先级
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
// 线程名称
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
- NSOperationQueue
// 获取队列中的操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 队列中的操作数
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
// 最大并发数,同一时间最多只能执行三个操作
@property NSInteger maxConcurrentOperationCount;
// 暂停 YES:暂停 NO:继续
@property (getter=isSuspended) BOOL suspended;
// 取消所有操作
- (void)cancelAllOperations;
// 阻塞当前线程直到此队列中的所有任务执行完毕
- (void)waitUntilAllOperationsAreFinished;