在IOS中为什么使用实现多线程的三种方法及实现多线程的三种方法实现的三种

iOS多线程安全的概念在很多地方都会遇到,为什么不安全,不安全又该怎么去定义,其实是个值得深究的话题。共享状态,多线程共同访问某个对象的property,在iOS编程里是很普遍的使用场景,我们就从Property的多线程安全说起。Property当我们讨论property多线程安全的时候,很多人都知道给property加上atomic attribute之后,可以一定程度的保障多线程安全,类似:@property (atomic, strong) NSString*
事情并没有看上去这么简单,要分析property在多线程场景下的表现,需要先对property的类型做区分。我们可以简单的将property分为值类型和对象类型,值类型是指primitive type,包括int, long, bool等非对象类型,另一种是对象类型,声明为指针,可以指向某个符合类型定义的内存区域。上述代码中userName明显是个对象类型,当我们访问userName的时候,访问的有可能是userName本身,也有可能是userName所指向的内存区域。比如:self.userName = @"peak";
是在对指针本身进行赋值。而[self.userName rangeOfString:@"peak"];
是在访问指针指向的字符串所在的内存区域,这二者并不一样。所以我们可以大致上将property分为三类:分完类之后,我们需要明白这三类property的内存模型。Memory Layout当我们讨论多线程安全的时候,其实是在讨论多个线程同时访问一个内存区域的安全问题。针对同一块区域,我们有两种操作,读(load)和写(store),读和写同时发生在同一块区域的时候,就有可能出现多线程不安全。所以展开讨论之前,先要明白上述三种property的内存模型,可用如下图示:以64位系统为例,指针NSString*是8个字节的内存区域,int count是个4字节的区域,而@“Peak”是一块根据字符串长度而定的内存区域。当我们访问property的时候,实际上是访问上图中三块内存区域。self.userName = @"peak";
是修改第一块区域。self.count = 10;
是在修改第二块区域。[self.userName rangeOfString:@"peak"];
是在读取第三块区域。不安全的定义明白了property的类型以及他们对应的内存模型,我们再来看看不安全的定义。Wikipedia如是说: A piece of code is thread-safe if it manipulates shared data structures only in a manner that guarantees safe execution by multiple threads at the same time这段定义看起来还是有点抽象,我们可以将多线程不安全解释为:多线程访问时出现意料之外的结果。这个意料之外的结果包含几种场景,不一定是指crash,后面再一一分析。先来看下多线程是如何同时访问内存的。不考虑CPU cache对变量的缓存,内存访问可以用下图表示:从上图中可以看出,我们只有一个地址总线,一个内存。即使是在多线程的环境下,也不可能存在两个线程同时访问同一块内存区域的场景,内存的访问一定是通过一个地址总线串行排队访问的,所以在继续后续之前,我们先要明确几个结论:结论一:内存的访问时串行的,并不会导致内存数据的错乱或者应用的crash。结论二:如果读写(load or store)的内存长度小于等于地址总线的长度,那么读写的操作是原子的,一次完成。比如bool,int,long在64位系统下的单次读写都是原子操作。接下来我们根据上面三种property的分类逐一看下多线程的不安全场景。值类型Property先以BOOL值类型为例,当我们有两个线程访问如下property的时候:@property (nonatomic, assgin) BOOL
//thread 1
bool isDeleted = self.isD
//thread 2
self.isDeleted =
线程1和线程2,一个读(load),一个写(store),对于BOOL isDeleted的访问可能有先后之分,但一定是串行排队的。而且由于BOOL大小只有1个字节,64位系统的地址总线对于读写指令可以支持8个字节的长度,所以对于BOOL的读和写操作我们可以认为是原子的,所以当我们声明BOOL类型的property的时候,从原子性的角度看,使用atomic和nonatomic并没有实际上的区别(当然如果重载了getter方法就另当别论了)。如果是int类型呢?@property (nonatomic, assgin)
//thread 1
int curCount = self.
//thread 2
self.count = 1;
同理int类型长度为4字节,读和写都可以通过一个指令完成,所以理论上读和写操作都是原子的。从访问内存的角度看nonatomic和atomic也并没有什么区别。atomic到底有什么用呢?据我所知,用处有二:用处一:生成原子操作的getter和setter。设置atomic之后,默认生成的getter和setter方法执行是原子的。也就是说,当我们在线程1执行getter方法的时候(创建调用栈,返回地址,出栈),线程B如果想执行setter方法,必须先等getter方法完成才能执行。举个例子,在32位系统里,如果通过getter返回64位的double,地址总线宽度为32位,从内存当中读取double的时候无法通过原子操作完成,如果不通过atomic加锁,有可能会在读取的中途在其他线程发生setter操作,从而出现异常值。如果出现这种异常值,就发生了多线程不安全。用处二:设置Memory Barrier对于Objective C的实现来说,几乎所有的加锁操作最后都会设置memory barrier,atomic本质上是对getter,setter加了锁,所以也会设置memory barrier。官方文档表述如下:Note: Most types of locks also incorporate a memory barrier to ensure that any preceding load and store instructions are completed before entering the critical section.memory barrier有什么用处呢?memory barrier能够保证内存操作的顺序,按照我们代码的书写顺序来。听起来有点不可思议,事实是编译器会对我们的代码做优化,在它认为合理的场景改变我们代码最终翻译成的机器指令顺序。也就是说如下代码:self.intA = 0;
self.intB = 1; //line 2
编译器可能在一些场景下先执行line2,再执行line1,因为它认为A和B之间并不存在依赖关系,虽然在代码执行的时候,在另一个线程intA和intB存在某种依赖,必须要求line1先于line2执行。如果设置property为atomic,也就是设置了memory barrier之后,就能够保证line1的执行一定是先于line2的,当然这种场景非常罕见,一则是出现变量跨线程访问依赖,二是遇上编译器的优化,两个条件缺一不可。这种极端的场景下,atomic确实可以让我们的代码更加多线程安全一点,但我写iOS代码至今,还未遇到过这种场景,较大的可能性是编译器已经足够聪明,在我们需要的地方设置memory barrier了。是不是使用了atomic就一定多线程安全呢?我们可以看看如下代码:@property (atomic, assign)
//thread A
for (int i = 0; i & 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread A: %d\n", self.intA);
//thread B
for (int i = 0; i & 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread B: %d\n", self.intA);
即使我将intA声明为atomic,最后的结果也不一定会是20000。原因就是因为self.intA = self.intA + 1;不是原子操作,虽然intA的getter和setter是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load),+1(add),赋值(store)三步操作,当前线程store的时候可能其他线程已经执行了若干次store了,导致最后的值小于预期值。这种场景我们也可以称之为多线程不安全。指针Property指针Property一般指向一个对象,比如:@property (atomic, strong) NSString*
无论iOS系统是32位系统还是64位,一个指针的值都能通过一个指令完成load或者store。但和primitive type不同的是,对象类型还有内存管理的相关操作。在MRC时代,系统默认生成的setter类似如下:- (void)setUserName:(NSString *)userName {
if(_uesrName != userName) {
[userName retain];
[_userName release];
_userName = userN
不仅仅是赋值操作,还会有retain,release调用。如果property为nonatomic,上述的setter方法就不是原子操作,我们可以假设一种场景,线程1先通过getter获取当前_userName,之后线程2通过setter调用[_userName release];,线程1所持有的_userName就变成无效的地址空间了,如果再给这个地址空间发消息就会导致crash,出现多线程不安全的场景。到了ARC时代,Xcode已经替我们处理了retain和release,绝大部分时候我们都不需要去关心内存的管理,但retain,release其实还是存在于最后运行的代码当中,atomic和nonatomic对于对象类的property声明理论上还是存在差异,不过我在实际使用当中,将NSString*设置为nonatomic也从未遇到过上述多线程不安全的场景,极有可能ARC在内存管理上的优化已经将上述场景处理过了,所以我个人觉得,如果只是对对象类property做read,write,atomic和nonatomic在多线程安全上并没有实际差别。指针Property指向的内存区域这一类多线程的访问场景是我们很容易出错的地方,即使我们声明property为atomic,依然会出错。因为我们访问的不是property的指针区域,而是property所指向的内存区域。可以看如下代码:@property (atomic, strong) NSString*
//thread A
for (int i = 0; i & 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
self.stringA = @"string";
NSLog(@"Thread A: %@\n", self.stringA);
//thread B
for (int i = 0; i & 100000; i ++) {
if (self.stringA.length &= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
NSLog(@"Thread B: %@\n", self.stringA);
虽然stringA是atomic的property,而且在取substring的时候做了length判断,线程B还是很容易crash,因为在前一刻读length的时候self.stringA = @"a very long string";,下一刻取substring的时候线程A已经将self.stringA = @"string";,立即出现out of bounds的Exception,crash,多线程不安全。同样的场景还存在对集合类操作的时候,比如:@property (atomic, strong) NSArray*
//thread A
for (int i = 0; i & 100000; i ++) {
if (i % 2 == 0) {
self.arr = @[@"1", @"2", @"3"];
self.arr = @[@"1"];
NSLog(@"Thread A: %@\n", self.arr);
//thread B
for (int i = 0; i & 100000; i ++) {
if (self.arr.count &= 2) {
NSString* str = [self.arr objectAtIndex:1];
NSLog(@"Thread B: %@\n", self.arr);
同理,即使我们在访问objectAtIndex之前做了count的判断,线程B依旧很容易crash,原因也是由于前后两行代码之间arr所指向的内存区域被其他线程修改了。所以你看,真正需要操心的是这一类内存区域的访问,即使声明为atomic也没有用,我们平常App出现莫名其妙难以重现的多线程crash多是属于这一类,一旦在多线程的场景下访问这类内存区域的时候,要提起十二分的小心。如何避免这类crash后面会谈到。Property多线程安全小结:简而言之,atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。如何做到多线程安全?讨论到这里,其实怎么做到多线程安全也比较明朗了,关键字是atomicity(原子性),只要做到原子性,小到一个primitive type变量的访问,大到一长段代码逻辑的执行,原子性能保证代码串行的执行,能保证代码执行到一半的时候,不会有另一个线程介入。原子性是个相对的概念,它所针对的对象,粒度可大可小。比如下段代码:if (self.stringA.length &= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
是非原子性的。但加锁以后://thread A
[_lock lock];
for (int i = 0; i & 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
self.stringA = @"string";
NSLog(@"Thread A: %@\n", self.stringA);
[_lock unlock];
//thread B
[_lock lock];
if (self.stringA.length &= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
[_lock unlock];
整段代码就具有原子性了,就可以认为是多线程安全了。再比如:if (self.arr.count &= 2) {
NSString* str = [self.arr objectAtIndex:1];
是非原子性的。而//thread A
[_lock lock];
for (int i = 0; i & 100000; i ++) {
if (i % 2 == 0) {
self.arr = @[@"1", @"2", @"3"];
self.arr = @[@"1"];
NSLog(@"Thread A: %@\n", self.arr);
[_lock unlock];
//thread B
[_lock lock];
if (self.arr.count &= 2) {
NSString* str = [self.arr objectAtIndex:1];
[_lock unlock];
是具有原子性的。注意,读和写都需要加锁。这也是为什么我们在做多线程安全的时候,并不是通过给property加atomic关键字来保障安全,而是将property声明为nonatomic(nonatomic没有getter,setter的锁开销),然后自己加锁。如何使用哪种锁?iOS给代码加锁的方式有很多种,常用的有:@synchronized(token)NSLockdispatch_semaphore_tOSSpinLock这几种锁都可以带来原子性,性能的损耗从上至下依次更小。我个人建议是,在编写应用层代码的时候,除了OSSpinLock之外,哪个顺手用哪个。相较于这几个锁的性能差异,代码逻辑的正确性更为重要。而且这几者之间的性能差异对用户来说,绝大部分时候都感知不到。当然我们也会遇到少数场景需要追求代码的性能,比如编写framework,或者在多线程读写共享数据频繁的场景,我们需要大致了解锁带来的损耗到底有多少。官方文档有个数据,使用Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5测试,获取mutex有大概0.2ms的损耗,我们可以认为锁带来的损耗大致在ms级别。Atomic Operations其实除了各种锁之外,iOS上还有另一种办法来获取原子性,使用Atomic Operations,相比锁的损耗要小一个数量级左右,在一些追求高性能的第三方Framework代码里可以看到这些Atomic Operations的使用。这些atomic operation可以在/usr/include/libkern/OSAtomic.h中查到:比如_intA ++;
是非原子性的。而OSAtomicIncrement32(&(_intA));
是原子性的,多线程安全的。Atomic Operation只能应用于32位或者64位的数据类型,在多线程使用NSString或者NSArray这类对象的场景,还是得使用锁。大部分的Atomic Operation都有OSAtomicXXX,OSAtomicXXXBarrier两个版本,Barrier就是前面提到的memory barrier,在多线程多个变量之间存在依赖的时候使用Barrier的版本,能够保证正确的依赖顺序。对于平时编写应用层多线程安全代码,我还是建议大家多使用@synchronized,NSLock,或者dispatch_semaphore_t,多线程安全比多线程性能更重要,应该在前者得到充分保证,犹有余力的时候再去追求后者。尽量避免多线程的设计无论我们写过多少代码,都必须要承认多线程安全是个复杂的问题,作为程序员我们应该尽可能的避免多线程的设计,而不是去追求高明的使用锁的技能。后面我会写一篇文章,介绍函数式编程及其核心思想,即使我们使用非函数式的编程语言,比如Objective C,也能极大的帮助我们避免多线程安全的问题。总结iOS下多线程不安全的分析至此结束了,如何编写多线程安全的代码,说到底还是在于对memory layout和原子性的理解,也希望这篇文章将atomic和nonatomic的真正区别解释清楚了:)。欢迎关注公众号:MrPeakTech4415 条评论分享收藏文章被以下专栏收录不定期更新一些iOS相关的深度技术文章,来自工作当中的技术积累和心得,文章均同步于个人技术博客mrpeak.cn。也欢迎通过二维码关注个人公众号。在IOS中为什么使用多线程及多线程实现的三种方法
转载 & & 投稿:mrr
这篇文章给大家介绍在IOS中为什么使用多线程及多线程实现的三种方法,基本上使用这三种方法实现多线程(NSThread Grand Centeral Dispatch(GCD) NSOperation和NSOperationQueue),感兴趣的朋友可以参考下本篇文章
多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径。
在系统级别内,程序并排执行,程序分配到每个程序的执行时间是基于该程序的所需时间和其他程序的所需时间来决定的。
然而,在每个程序内部,存在一个或者多个执行线程,它同时或在一个几乎同时发生的方式里执行不同的任务。
概要提示:
iPhone中的线程应用并不是无节制的,官方给出的资料显示,iPhone OS下的主线程的堆栈大小是1M,第二个线程开始就是512KB,并且该值不能通过编译器开关或线程API函数来更改,只有主线程有直接修改UI的能力
一、线程概述
有些程序是一条直线,起点到终点——如简单的hello world,运行打印完,它的生命周期便结束了,像是昙花一现。
有些程序是一个圆,不断循环直到将它切断——如操作系统,一直运行直到你关机。
一个运行着的程序就是一个进程或者叫做一个任务,一个进程至少包含一个线程,线程就是程序的执行流。
Mac和IOS中的程序启动,创建好一个进程的同时,一个线程便开始运作,这个线程叫做主线程。主线成在程序中的位置和其他线程不同,它是其他线程最终的父线程,且所有的界面的显示操作即AppKit或UIKit的操作必须在主线程进行。
系统中每一个进程都有自己独立的虚拟内存空间,而同一个进程中的多个线程则公用进程的内存空间。
每创建一个新的进成,都需要一些内存(如每个线程有自己的stack空间)和消耗一定的CPU时间。
当多个进成对同一个资源出现争夺的时候需要注意线程安全问题
创建一个新的线程就是给进程增加一个执行流,所以新建一个线程需要提供一个函数或者方法作为线程的进口。
1.使用NSThread
NSThread提供了创建线程的路径,还可以提供了监测当前线程是否是主线程的方法使用NSThread创建一个新的线程有两种方式:
1.创建一个NSThread的对象,调用Start方法——使用一个目标对象的方法初始化一个NSThread对象,或者创建一个继承自NSThread的子类,实现起main方法?,然后在直接创建这个子类的对象。
2.使用detachNewThreadSelector:toTarget:withObject:这个类方法创建一个子线程,这个比较直接,直接使用目标对象的方法作为线程启动入口
2.使用NSObject
使用NSObject直接就加入了对多线程的支持,允许对象的某个方法在后台运行。
[my0bj performSelectorInBackground:@selector(doSomething) withObject:nil];
3.POSIX Thread
由于Mac和IOS都是基于Darwin系统,Darwin系统的UNX内核,是基于mach和BSD的,继承了BSD的POSIX接口,所以可以直接使用POSIX线程的相关接口开实现线程
创建线程的接口为 pthread_create, 当然在创建线程之前可以创建好相关线程的属性
——————————————————————————————————————
NSOperation&NSOperationQueue
很多时候我们使用多线程,需要控制线程的并发数,毕竟线程也是需要消耗系统资源的,当程序中同时运行的线程过多时,系统必然变慢,所以很多时候我们会控制同时运行线程的数目
NSOperation可以封装我们的操作,然后将创建好的NSOperation对象放到NSOperationQueue队列中,OperationQueue便开始启动新的线程去执行队列中的操作,OperationQueue的并发数时可以通过如下方式进行设置的:
- (void)setMaxConcurrentOperationCount:(NSInteger)count
GCD时Grand central Dispatch的缩写,是一系列BSD层面的接口。在mac10.6和IOS4.0以后才引入的且现在NSOperation和NSOperationQueue的多线程的实现就是基于GCD的。目前这个特性也被移植到 FreeBSD上了,可以查看libdispatch这个开源项目。
dispatch_queue_t imageDownloadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
当然,GCD除了处理多线程外还有很多非常好的功能,其建立在强大的kqueue之上,效率也能够得到保障。
在多线程简介中,我已经说明过了,为了提高界面的流畅度以及用户体验。我们务必要把耗时的操作放到别的线程中去执行,千万不要阻塞主线程。
下面小编给大家带来三种ios多线程编程方法:
Grand Centeral Dispatch(GCD)
NSOperation和NSOperationQueue
1.NSThread
这是最轻量级的多线程的方法,使用起来最直观的多线程编程方法。但是因为需要自己管理线程的生命周期,线程同步。经常使用NSThread进行调试,在实际项目中不推荐使用。
//获取当前线程
NSThread *current = [NSThread currentThread];
//获取主线程
NSThread *main = [NSThread mainThread];
NSLog(@"当前线程 --- %@",current);
NSLog(@"主线程 --- %@",main);
控制台输出结果:
22:30:29.572 多线程demo[] 当前线程 --- &NSThread: 0x7fc0e1401dc0&{number = 1, name = main}
22:30:29.572 多线程demo[] 主线程 --- &NSThread: 0x7fc0e1401dc0&{number = 1, name = main}
从结果我们看出当前的线程就是主线程, number 相当于线程的id, name 是线程的名称,主线程的number就是1
阻塞线程:
//阻塞线程3秒
[NSThread sleepForTimeInterval:3];
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
2.GCD(Grand Central Dispatch)
GCD是基于C语言底层API实现的一套多线程并发机制,非常的灵活方便,在实际的开发中使用很广泛。
简单来说CGD就是把 操作 放在 队列 中去执行。
你只需定义好操作和队列就可以了,不需要直接控制线程的创建和销毁,线程的生命周期由队列来管理。
队列:负责操作的调度和执行,有先进先出(FIFO)的特点。也就是说先加入队列的操作先执行,后加入的后执行。
队列有两种:
串行队列:
队列中的操作只会按顺序执行,你可以想象成单窗口排队。
并行队列:
队列中的操作可能会并发执行,这取决与操作的类型,你可以想象成多窗口排队。
//创建串行队列
dispatch_queue_t q = dispatch_queue_create("my_serial_queue", DISPATCH_QUEUE_SERIAL);
//创建并行队列
dispatch_queue_t q = dispatch_queue_create("my_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
my_serial_queue和my_concurrent_queue是队列的名字标签,为了与其他的队列区分,在一个项目里面必须是唯一的。
DISPATCH_QUEUE_SERIAL表示串行队列
DISPATCH_QUEUE_CONCURRENT表示并行队列
操作同样也分两种类型:
同步操作:只会按顺序执行,执行顺序是确定的。
异步操作:在串行队列中执行顺序确定,在并行队列中执行顺序不确定
使用block来定义操作要执行的代码,q是已经定义好的,操作要加入的队列
//定义同步操作
dispatch_sync(q, ^{
//要执行的代码
//定义异步操作
dispatch_async(q, ^{
//要执行的代码
下面我们看一下同步,异步操作加入到串行和并行队列里面,执行的顺序和特点:1.同步操作不管加入到何种队列,只会在主线程按顺序执行
dispatch_queue_t q_serial = dispatch_queue_create("my_serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t q_concurrent = dispatch_queue_create("my_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i & 5; ++i) {
dispatch_sync(q_serial, ^{
NSLog(@"串行队列里的同步任务 %@ %d", [NSThread currentThread], i);
for (int i = 0; i & 5; ++i) {
dispatch_sync(q_concurrent, ^{
NSLog(@"并行队列里的同步任务 %@ %d", [NSThread currentThread], i);
下面是控制台输出结果:
00:40:36.862 01.GCD演练[] 串行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 0
00:40:36.863 01.GCD演练[] 串行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 1
00:40:36.863 01.GCD演练[] 串行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 2
00:40:36.863 01.GCD演练[] 串行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 3
00:40:36.863 01.GCD演练[] 串行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 4
00:40:36.863 01.GCD演练[] 并行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 0
00:40:36.863 01.GCD演练[] 并行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 1
00:40:36.863 01.GCD演练[] 并行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 2
00:40:36.864 01.GCD演练[] 并行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 3
00:40:36.864 01.GCD演练[] 并行队列里的同步任务 &NSThread: 0x7ff&{number = 1, name = main} 4
2.异步操作只在非主线程的线程执行,在串行队列中异步操作会在新建的线程中按顺序执行。
dispatch_queue_t q_serial = dispatch_queue_create("my_serial_queue", DISPATCH_QUEUE_SERIAL);
for(int i = 0; i & 5; ++i){
dispatch_async(q_serial, ^{
NSLog(@"串行队列 -- 异步任务 %@ %d", [NSThread currentThread], i);
因为是异步操作,所以会新建一个线程。又因为加入到串行队列中,所以所有的操作只会按顺序执行。
01:03:22.372 01.GCD演练[] 串行队列 -- 异步任务 &NSThread: 0x7fb593d42f50&{number = 2, name = (null)} 0
01:03:23.373 01.GCD演练[] 串行队列 -- 异步任务 &NSThread: 0x7fb593d42f50&{number = 2, name = (null)} 1
01:03:24.374 01.GCD演练[] 串行队列 -- 异步任务 &NSThread: 0x7fb593d42f50&{number = 2, name = (null)} 2
01:03:25.375 01.GCD演练[] 串行队列 -- 异步任务 &NSThread: 0x7fb593d42f50&{number = 2, name = (null)} 3
01:03:26.376 01.GCD演练[] 串行队列 -- 异步任务 &NSThread: 0x7fb593d42f50&{number = 2, name = (null)} 4
3.异步操作,并行队列
dispatch_queue_t q_concurrent = dispatch_queue_create("my_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for(int i = 0; i & 5; ++i){
dispatch_async(q_concurrent, ^{
NSLog(@"并行队列 -- 异步任务 %@ %d", [NSThread currentThread], i);
理论上并行队列会给每一个异步操作新建线程,然后让所有的任务并发执行。但是实际上系统能创建的线程数量是有限的,当创建的线程达到最大线程数以后,后面的异步操作就需要等待前面的操作执行完毕才能得到执行。哪个线程操作执行完毕,就把等待的异步任务安排到哪个线程。直到所有的操作执行完毕。你可以把上述代码的循环次数改成5000就可以观察到此现象。
01:14:15.282 01.GCD演练[] 并行队列 -- 异步任务 &NSThread: 0x7fb3b841b0a0&{number = 4, name = (null)} 3
01:14:15.282 01.GCD演练[] 并行队列 -- 异步任务 &NSThread: 0x7fb3b8514da0&{number = 3, name = (null)} 0
01:14:15.282 01.GCD演练[] 并行队列 -- 异步任务 &NSThread: 0x7fb3b8604db0&{number = 5, name = (null)} 2
01:14:15.282 01.GCD演练[] 并行队列 -- 异步任务 &NSThread: 0x7fb3b86119d0&{number = 2, name = (null)} 1
01:14:15.285 01.GCD演练[] 并行队列 -- 异步任务 &NSThread: 0x7fb3b87011f0&{number = 6, name = (null)} 4
3.NSOperation & NSOperationQueue
虽然GCD的功能已经很强大了,但是它使用的API依然是C语言的。在某些时候,在面向对象的objective-c中使用起来非常的不方便和不安全。
所以苹果公司把GCD中的操作抽象成NSOperation对象,把队列抽象成NSOperationQueue对象。
抽象为NSOperation & NSOperationQueue以后的好处有一下几点:
代码风格统一了,我们不用在面向对象的objective-C中写面对过程的C语言代码了。
我们知道在GCD中操作的执行代码都是写在匿名的block里面,那么我们很难做到给操作设置依赖关系以及取消操作。这些功能都已经封装到NSOperation对象里面了。^-^
NSOperationQueue对象比GCD中队列更加的强大和灵活,比如:设置并发操作数量,取消队列中所有操作。
NSOperation分为NSInvocationOperation和NSBlockOperation
NSInvocationOperation的使用
//首先定义一个NSOperationQueue对象
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction:) object:@"这里可以穿参数"];
[queue addOperation:op];//把操作加入队列中即开始执行
- (void)operationAction:(id)obj
NSLog(@"%@ - obj : %@", [NSThread currentThread], obj);
02:55:19.067 多线程demo[] &NSThread: 0x7f9dfa443510&{number = 2, name = (null)} - obj : 这里可以穿参数
NSBlockOperation的使用
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
[self operationAction:@"这是NSBlockOperation"];
[queue addOperation:op];
02:56:11.812 多线程demo[] &NSThread: 0x7fa983f10a50&{number = 2, name = (null)} - obj : 这是NSBlockOperation
设置依赖关系(执行顺序)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction:) object:@"op1"];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction:) object:@"op2"];
//op2在op1之后执行
[op2 addDependency:op1];//这里需要注意,一定要在addOperation之前设置依赖关系
[queue addOperation:op1];
[queue addOperation:op2];
02:57:40.283 多线程demo[] &NSThread: 0x7fb663e132d0&{number = 2, name = (null)} - obj : op1
02:57:40.284 多线程demo[] &NSThread: 0x7fb663e132d0&{number = 2, name = (null)} - obj : op2
没有设置依赖关系的输出:
03:00:45.939 多线程demo[] &NSThread: 0x7fe951d0d8a0&{number = 2, name = (null)} - obj : op2
03:00:45.939 多线程demo[] &NSThread: 0x7fe951c24720&{number = 3, name = (null)} - obj : op1
到这里你应该发现了,在NSOperation & NSOperationQueue中,我们不需要再像GCD那样定义操作的类型和队列的类型和控制操作的执行顺序了,你只需要直接设定操作的执行顺序就可以了。
您可能感兴趣的文章:
大家感兴趣的内容
12345678910
最近更新的内容
常用在线小工具

我要回帖

更多关于 多线程有几种实现方法 的文章

 

随机推荐