Objective-C多线程详解(NSThread、NSOperation、GCD)

时间:2021-07-08 21:12:30

进程和线程

程序:一个由源代码生成的可执行应用(比如qq,微信…)

进程:进程是指在系统中正在运行的一个应用程序。一个正在运行的程序可以看成一个进程,进程负责去向手机系统申请资源,同时将这些资源调度给我们的线程

线程:1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程),可以看成是可以运行的代码段,这些代码段需要的资源,比如内存,都需要去向进程申请。线程是进 程的基本执行单元,一个进程(程序)的所有任务都在线程中执行。

单线程:只有一个现成的程序叫做单线程程序,如果是单线程则肯定是主线程。

多线程:拥有多个线程的程序叫做多线程程序,1个进程中可以开启条多线程每条线程可以并行(同时)执行不同的任务。比如同时开启3条线程分别下载3个文件,多线程技术可以提高程序的执行效率,但相对占用资源。其实多线程的原理仍是单线程,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。

除了主线程之外的线程都是子线程,程序中只有一个主线程。主线程是顺序执行的,上一个任务执行完,才会执行下一个任务。子线程之间是并行执行的,也就是同时执行的。

注意: 我们规定我们的UI添加和刷新代码必须放在我们的主线程中去做

    [NSThread mainThread]; //获取到主线程
[NSThread currentThread]; //获取到当前代码运行的线程

1,NSThread

NSThread适合轻量级多线程开发,控制线程顺序比较难,同时线程总数无法控制(每次创建并不能重用之前的线程,只能创建一个新的线程)。

优点:NSThread相对比较轻量级
缺点:需要自己管理线程生命周期,线程同步,线程同步对数据加锁有一定的系统开销;

NSThread实现的三种方式:
1.

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadInitDoSomething) object:nil];

[thread start];

这种方法需要手动开启线程。
2.

[NSThread detachNewThreadSelector:@selector(threadDetachDosomething) toTarget:self withObject:nil];

这种便利构造的方法,不需要手动开启。
3.

[self performSelectorInBackground:@selector(backGround) withObject:nil]; 

这种方式是NSObject对象自带的开启后台线程的方法。

2,NSOperation

优点:不需要手动关系线程,可以把精力放在自己要执行的操作上面,NSOperation是一个抽象类,不能被直接初始化,NSOperation我们一般使用它的子类NSInvocationOperation,NSBlockOperation或者继承NSOperation的自定义任务,我们经常将任务和队列NSOperationQueue进行搭配使用,一个对象一个任务,更利于任务的管理,还有一个优点在于可以明确的确定依赖关系。
缺点:他是一个OC对象,那么相对于C函数效率要低,而且基于GCD,那么GCD将提供比他更加全面的功能。
NSOperation使用:
1.

NSInvocationOperation *incocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationDoSomething) object:nil];
[incocation start];

这种方式利用的Target-Action的设计模式,让响应者去执行任务。
2.

NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d, %@", [NSThread isMainThread], [NSThread currentThread]);
}];
[block start];

这种方式利用OC里面经典的语法block(语法块)。但是和上者一样,如果单独使用NSOperation的子类对象必须手动的开启任务。
3.

//NSOperationQueue里面只有串行的时候线程优先级才是可行的
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//最大并发量,如果最大病发量为1时,那么队列里面的任务将串行,也就是执行完一个任务才能执行下一个,如果不为1,那就是并发进行。
queue.maxConcurrentOperationCount = 1;

//添加block块任务
NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"--0--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
}];
//设置任务的优先级,只有队列为串行的时候优先级才能起到绝对的作用
[block setQueuePriority:NSOperationQueuePriorityVeryHigh];
NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"--1--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
}];
[block1 setQueuePriority:NSOperationQueuePriorityNormal];
NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"--2--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
}];
[block2 setQueuePriority:NSOperationQueuePriorityVeryLow];
NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"--3--%d, %@", [NSThread isMainThread], [NSThread currentThread]);
}];
[block3 setQueuePriority:NSOperationQueuePriorityVeryHigh];

//设置以来关系,只有执行完block之后才会去执行block1,这叫做任务block1依赖于block,在项目开发中经常用到
[block1 addDependency:block];
//添加任务,任务为NSOperation对象的子类,添加到任务队列中之后会自动去执行,不需要start;
[queue addOperation:block];
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];

NSOperationQueue在开发中经常会使用到,比如我们做多任务下载的时候,使用自定义NSOperation子类和NSOperationQueue结合使用,每个NSOperation对象是一个任务,而NSOperationQueue却完美的担任了任务关系器的角色。抽时间会把demo上传到github上面,请及大家及时关注。任务之间的依赖也是NSOperation的一大完美特征。

3,GCD

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。是基于 C 的 API,函数级别的多线程方法,效率更高。有如下特点:

1.GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
2.GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
3.GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。

4.GCD 使用后不用程序去管理线程的开闭,GCD会在系统层面上去动态检测系统状态,开闭线程。

总的来说就是效率高,更容易的利用多核处理器并行处理任务。

GCD 队列(dispatch queue)大体上分2种:

serial (串行队列): 一次只能执行一个任务,必须上一个任务执行完,下一个任务才开始,遵循FIFO 原则->哪个任务先进入这个队列就先执行(先进先出)
concurrent (并行队列):队列会根据当前系统的情况(内存…)来创建子线程,同时将这些任务进行分发

或者分成以下三种:

1)运行在主线程的Main queue,通过dispatch_get_main_queue获取。

2)并行队列global dispatch queue,通过dispatch_get_global_queue获取,由系统创建三个不同优先级的dispatch queue。并行队列的执行顺序与其加入队列的顺序相同。

3)串行队列serial queues一般用于按顺序同步访问,可创建任意数量的串行队列,各个串行队列之间是并发的。

当想要任务按照某一个特定的顺序执行时,串行队列是很有用的。串行队列在同一个时间只执行一个任务。我们可以使用串行队列代替锁去保护共享的数据。和锁不同,一个串行队列可以保证任务在一个可预知的顺序下执行。

serial queues通过dispatch_queue_create创建,可以使用函数dispatch_retain和dispatch_release去增加或者减少引用计数。
下面给大家详细介绍GCD的使用:

主队列:

dispatch_queue_t queue = dispatch_get_main_queue();

主队列是串行队列,任务是从上到下一个一个执行的。

dispatch_async(queue, ^{
NSLog(@"这是第一个任务,当前线程是:%@, 是否主线程 :%d ", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});

上面这个函数意思是在主队列里面异步执行block里面的任务;

dispatch_sync(queue, ^{
NSLog(@"这是第一个任务,当前线程是:%@, 是否主线程 :%d ", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});

上面这一行代码意思是在主线成中同步执行block块里面的任务,但是这样做会让主线程假死,无法执行任何操作,且不论你在任何队列里面同步执行一系列的任务,都会在主线程去执行。但是不会出现主线程假死,所以同步我们很少去用。

全局队列:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

全局队列是并行队列,任务时并行的,充分利用了现在多核cpu的优势,第一个参数为队列的优先级,第二参数为苹果预留参数现在没有实际作用我们一般填写0。

dispatch_async(queue, ^{
NSLog(@"这是第一个任务,当前线程是:%@, 是否主线程 :%d ", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});

如果将一系列的任务加到全局队列里面时,任务就并发执行,从而没发预知那个任务先完成。

自定义队列:

dispatch_queue_t queue = dispatch_queue_create("com.zouhao", DISPATCH_QUEUE_CONCURRENT);

自定义队列可以是并行的也可以是串行的,第一个参数是队列的名称也可以称之为标示吧,第二个参数是决定队列是串行还是并行的DISPATCH_QUEUE_CONCURRENT代表并行DISPATCH_QUEUE_SERIAL代表串行。

dispatch_async(queue, ^{
NSLog(@"这是第一个任务,当前线程是:%@, 是否主线程 :%d ", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});

下面来看看GCD里面的一些比较常规的函数:GCD延时执行,只执行一次,重复执行,分组任务,Barrier,函数指针。

GCD延时执行:

/第一个参数是从现在开始,第二个参数是时间,
dispatch_time_t delayInNanoSeconds =dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
//推迟两纳秒执行
dispatch_queue_t concurrentQueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//第一个参数只延迟时间,第二个参数是在那个队列里,第三个是任务,
dispatch_after(delayInNanoSeconds, concurrentQueue, ^(void){
NSLog(@"Grand Center Dispatch!");
});

GCD重复执行:

//有时候项目需求我们重复执行一个方法多次,比如我们常见的项目了面的倒计时的计时器,很多都是用GCD实现的。
dispatch_queue_t queue = dispatch_queue_create("com.zouhao", DISPATCH_QUEUE_SERIAL);
dispatch_apply(3, queue, ^(size_t index) {
NSLog(@"%zu, %@", index, [NSThread currentThread]);
});

这里我们看看dispatch_apply函数的几个参数分别是什么意思,第一个参数是重复执行多少次,第二个参数是在那个队列里面执行任务,第三个是任务,block里面的参数意思是第几次执行。
GCD只执行一次:

//只执行一次,单例对象创建的时候经常需要用到它
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"只会执行一次");
});

这里看看diapatch_once函数后面跟着两个参数,第一个参数传了一个地址,第二个参数是任务,当执行到block块里面时,就会向第一个参数指针指向的地址写入信息,当下一次在执行这个代码时发现内存已经被写入过就不会再执行block了。

分组任务(dispatch_group):

在追加到Dispatch Queue中的多个任务处理完毕之后想执行结束处理,这种需求会经常出现。如果只是使用一个Serial Dispatch Queue(串行队列)时,只要将想执行的处理全部追加到该串行队列中并在最后追加结束处理即可,但是在使用Concurrent Queue 时,可能会同时使用多个Dispatch Queue时,源代码就会变得很复杂。在这种情况下,就可以使用Dispatch Group。有时候我们开发的时候或许有需求需要将队列里面的一些任务加到一个分组里面进行管理。当分组里面的任务执行完之后我们可能需要做一些其他的逻辑。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
NSLog(@"第一个任务 %d, %@", [NSThread isMainThread], [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"第二个任务 %d, %@", [NSThread isMainThread], [NSThread currentThread]);
});

这样我们就讲所有的队列里面的任务用分组进行管理了,这个的队列可以使多个,当分组里面的任务执行完成之后我们用到了一个新的函数。

//这个通知不能写在还所有任务之上,必须保证先在队列里面加入任务之后他才能够用,说简单的就是如果队列里面没有任务的时候他就会默认队列任务被执行完然后走通知
dispatch_group_notify(group, queue, ^{
NSLog(@"分组里面的最后一个任务了!");
})
;

但是这个任务不能第一个加到分组里,要不然这个时候分组为空,默认所有任务都执行完,直接对调通知的这个block。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("gcd-group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 1000; i++) {
if (i == 999) {
NSLog(@"11111111");
}
}
});
dispatch_group_async(group, queue, ^{
NSLog(@"22222222");
});
dispatch_group_async(group, queue, ^{
NSLog(@"33333333");
});
dispatch_group_notify(group, queue, ^{
NSLog(@"done");
});

控制台的输出:
Objective-C多线程详解(NSThread、NSOperation、GCD)

因为向Concurrent Dispatch Queue 追加处理,多个线程并行执行,所以追加处理的执行顺序不定。执行顺序会发生变化,但是此执行结果的done一定是最后输出的。
无论向什么样的Dispatch Queue中追加处理,使用Dispatch Group都可以监视这些处理执行的结果。一旦检测到所有处理执行结束,就可以将结束的处理追加到Dispatch Queue中,这就是使用Dispatch Group的原因。

GCD Barrier:
在访问数据库或者文件的时候,我们可以使用Serial Dispatch Queue可避免数据竞争问题。并发中的数据竞争,通常的方法是加锁和解锁来实现同步机制。iOS提供了一种加锁的方式,就是采用内置的synchronization block。这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁也就释放了。其实使用GCD可以简单高效的代替同步块或者锁对象,可以使用,串行同步队列,将读操作以及写操作都安排在同一个队列里,即可保证数据同步,代码如下:

#import <Foundation/Foundation.h>
@interface YXPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
#import "YXPerson.h"
@interface YXPerson ()
@end
static NSString *_name;
static dispatch_queue_t _queue;
@implementation YXPerson
- (instancetype)init
{
if (self = [super init]) {
_queue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}

- (void)setName:(NSString *)name
{
dispatch_sync(_queue, ^{
_name = [name copy];
});
}

- (NSString *)name
{
__block NSString *tempName;
dispatch_sync(_queue, ^{
tempName = _name;
});
return tempName;
}
@end

这样写的思路是:把写操作与读操作都安排在同一个同步串行队列里面执行,这样的话,所有针对属性的访问操作就都同步了。

这种方法还不是最优的,它只可以实现单读、单写。整体来看,我们最终要解决的问题是,在写的过程中不能被读,以免数据不对,但是读与读之间并没有任何的冲突。多个getter方法(也就是读取)是可以并发执行的,而getter(读)与setter(写)方法是不能并发执行的,利用这个特点,还能写出更快的代码来,这次注意,不用串行队列,而改用并行队列:

#import <Foundation/Foundation.h>
@interface YXPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

#import "YXPerson.h"
@interface YXPerson ()
@end
static NSString *_name;
static dispatch_queue_t _concurrentQueue;
@implementation YXPerson
- (instancetype)init
{
if (self = [super init]) {
_concurrentQueue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)setName:(NSString *)name
{
dispatch_barrier_async(_concurrentQueue, ^{
_name = [name copy];
});
}
- (NSString *)name
{
__block NSString *tempName;
dispatch_sync(_concurrentQueue, ^{
tempName = _name;
});
return tempName;
}
@end

在这个代码中的dispatch_barrier_async,可以翻译成栅栏(barrier),它可以往队列里面发送任务(块,也就是block),这个任务有栅栏(barrier)的作用。
在队列中,barrier块必须单独执行,不能与其他block并行。这只对并发队列有意义,并发队列如果发现接下来要执行的block是个barrier block,那么就一直要等到当前所有并发的block都执行完毕,才会单独执行这个barrier block代码块,等到这个barrier block执行完毕,再继续正常处理其他并发block。在上面的代码中,setter方法中使用了barrier block以后,对象的读取操作依然是可以并发执行的,但是写入操作就必须单独执行了。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"这是第一个读取数据的任务。。。线程是:%@, 是否主线程:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
NSLog(@"这是第二个读取数据的任务。。。线程是:%@, 是否主线程:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
NSLog(@"这是第三个任务读取数据的任务。。。线程是:%@, 是否主线程:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"正在给数据库里面写东西,不要打扰我");
});
dispatch_async(queue, ^{
NSLog(@"这是第四个读取数据的任务。。。线程是:%@, 是否主线程:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
NSLog(@"这是第五个读取数据的任务。。。线程是:%@, 是否主线程:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});
dispatch_async(queue, ^{
NSLog(@"这是第六个任务读取数据的任务。。。线程是:%@, 是否主线程:%d", [NSThread currentThread], [[NSThread currentThread] isMainThread]);
});

很明显的可以看出来虽然是异步执行,但是在dispatch_barrierh函数上面的方法执行之后dispatch_barrierh下面的的任务将处于等候状态,直到dispatch_barrierh函数里面的任务完成之后再去执行。

GCD函数指针:
我们在调用GCD函数的时候发现很多方法很相似,只是函数名多了_f,这就是我们的函数指针。为什么我们明明有了直接的函数调用还要出现函数指针实现任务体呢?很明显block是OC里面的语法块,但是函数确是C语言里面的语法,这样来看函数指针这种模式必然比较低层,那么效率必然会比block这种模式要高,但是GCD本生封装已经很完美,如果想单单通过讲block换成函数指针提高很多的效率是办不到的,只是在一些要求精细的项目里面会起到细微的提高效率的作用。下面这些就是GCD函数指针:

 dispatch_async_f(dispatch_get_main_queue(), "haha", func);
dispatch_sync_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
dispatch_after_f(<#dispatch_time_t when#>, <#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
dispatch_apply_f(<#size_t iterations#>, <#dispatch_queue_t queue#>, <#void *context#>, <#void (*work)(void *, size_t)#>)
dispatch_barrier_async_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
dispatch_barrier_sync_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
dispatch_group_async_f(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
dispatch_group_notify_f(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
dispatch_once_f(<#dispatch_once_t *predicate#>, <#void *context#>, <#dispatch_function_t function#>)
dispatch_set_finalizer_f(<#dispatch_object_t object#>, <#dispatch_function_t finalizer#>)

4,Pthreads

其实这个框架专职做iOS的人很人知道也很少人用到,这里也只是提一下拿来充个数,为了让大家了解一下就好了。这是一套在很多操作系统上都通用的多线程API,当然在 iOS 中也是可以的。不过这是基于C语言的框架,使用起来你懂得,感兴趣的可以了解一下。