iOS 从实际出发理解多线程
一:前言
多线程很多开发者多多少少相信也都有了解,以前有些东西理解的不是很透,慢慢的积累之后,这方面的东西也需要自己好好的总结一下。多线程从我刚接触到iOS的时候就知道这玩意挺重要的,但那时也是能力有限,没办法很好的理解它,要是只是查它的概念性的东西,网上一搜一大把,我们再那样去总结就显得意义不大了。这篇文章从我刚开始构思着去写的时候,就希望自己能换个角度去写,想从实际问题出发总结多线程,那就从第三方以及自己看到的一些例子还有前段时间读的多线程和内存管理的书中分析理解总结一下多线程。
二:这几个概念很容易绕晕
一 进程:进程就是线程的容器,你打开一个App就是打开了一个进程,QQ有QQ的进程,微信有微信的进程,一个进程可以包含多个线程,要是把进程比喻成一条高速公路,线程就是高速路上的一条条车道,也正是因为有了这些车道,整个交通的运行效率变得更高,也正是因为有了多线程的出现,整个系统运行效率变得更高。
二 线程:线程就是在进程中我么开辟的一条条为我们做事的进程实体,总结的通俗一点,线程就是我们在进程上开辟的一条条做我们想做的事的通道。 一条线程在一个时间点上只能做一件“事”,多线程在同一时间点上,就能做多件“事”,这个理解,还是我们前面说的高速路的例子。
一条高速路是一个进程, 一条条车道就是不同的线程,在过收费站的时候,这条进程上要是只有一条线程,也就是一条高速路上只有一个车道,那你就只能排队一辆一辆的通过,同一时间不可能有两辆车一起过去,但要是你一个进程上有多个线程,也就是高速路上有几个车道,也就有多个窗口收费,这样的话同一时间就完全有可能两辆车一起交完费通过了,这样说相信也能理解这个进程和线程的关系了。
- 同步线程:同步线程会阻塞当前的线程去执行同步线程里面想做的“事”(任务),执行完之后才会返回当前线程。
- 异步线程:异步线程不会阻塞当前的线程去执行异步线程里面想做的“事”,因为是异步,所以它会重新开启一个线程去做想做的“事”。
三 队列:队列就是用来管理下面说的“任务”的,它采用的是先进先出(FIFO)的原则,它衍生出来的就是下面的它的分类并行和串行队列,一条线程上可以有多个队列。
- 并行队列:这个队列里面的任务是可以并发(同时)执行的,由于我们知道,同步执行任务不会开启新的线程,所以并行队列同步执行任务任务只会在一条线程里面同步执行这些任务,又由于同步执行也就是在当前线程中做事,这个时候就需要一件一件的让“事”(任务)做完在接着做下一个。但要是是并发队列异步执行,就对应着开启异步线程执行要做的“事”(任务),就会同一时间又许多的“事”被做着。
- 串行队列:这个队列里面的任务是串行也就是一件一件做的,串行同步会一件一件的等事做完再接着做下一件,要是异步的就会开启一条新的线程串行的执行我们的任务。
四 任务:任务按照自己通俗一点的理解,就是提到的“事”这个概念,这个“事”就可以理解为任务,那这个“事”也肯定是在线程上面执行的(不管是在当前线程还是你另开启的线程)。这个“事”你可以选择同步或者而是异步执行,这就衍生出了东西也就契合线程上面的同步线程和异步线程。
- 同步任务:不需要开启新的线程,在当前线程执行就可以。
- 异步任务:你需要开辟一条新的线程去异步的执行这个任务。
iOS当中还有一个特殊的串行队列-- 主队列, 这个主队列中运行着一条特殊的线程 -- 主线程
主线程又叫UI线程,UI线程顾名思义主要的任务及时处理UI,也只有主线程有处理UI的能力,其他的耗时间的操作我们就放在子线程(也就是开辟线程)去执行,开线程也会占据一定的内存的,所以不要同时开启很多的线程。
通过上面的内容解释了多线程里面几个关键的概念的东西,要是有不理解的地方欢迎多交流,下面再给出队列执行时候的一个运行的表格,我们一个一个慢慢的解释。
三:NSThread
其实在我们日常的开发中NSThread使用也是挺多的,具体关于它的一些我们需要注意的地方我们一步步的开始说,先看看它的初始化的几个方法
/* 初始化NSThread的类方法,具体的任务在Block中执行 + (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); 利用selector方法初始化NSThread,target指selector方法从属于的对象 selector方法也是指定的target对象的方法 + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument; 初始化NSThread的方法,这两个方法和上面两个方法的区别就是这两个你能获取到NSThread的对象 具体的参数和前面解释的参数意义都是一样的 切记一点: 下面两个方法初始化的NSThread你需要手动start开启线程 - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0); - (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); */
除了上面四个我们提出的方法,我们在初始化这个问题上还需要注意的还有一点,就是 NSObject (NSThreadPerformAdditions) ,为我们的NSObject添加的这个类别,它里面的具体的一些方法我们也是很常用的:
/* 这个方法你执行的aSelector就是在MainThread执行的,也就是在主线程 注意这里的waitUntilDone这个后面的BOOL类型的参数,这个参数表示是否等待一直到aSelector这个方法执行结束 modes是RunLoop的运行的类型这个RunLoop我也会好好在总结后面 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; // equivalent to the first method with kCFRunLoopCommonModes 上面的两个方法是直接在主线程里面运行,下面的这两个方法是要在你初始化的thr中去运行,其他的参数和上面解释的一样 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0); - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0); // equivalent to the first method with kCFRunLoopCommonModes - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0); */
我们在说说前面说的waitUntilDone后面的这个BOOL类型的参数,这个参数的意义有点像我们是否同步执行aSelector这个任务!具体的看下面两张图的内容就一目了然了:
在看看等于YES的时候结果的输出情况:
关于NSThread我们再说下面几个方法的具体的含义就不在描述了,关于NSThread有什么其他的问题,可以加我QQ交流:
/* 设置线程沉睡到指定日期 + (void)sleepUntilDate:(NSDate *)date; 线程沉睡时间间隔,这个方法在设置启动页间隔的时候比较常见 + (void)sleepForTimeInterval:(NSTimeInterval)ti; 线程退出,当执行到某一个特殊情况下的时候你可以退出当前的线程,注意不要在主线程随便调用 + (void)exit; 线程的优先级 + (double)threadPriority; 设置线程的优先级 + (BOOL)setThreadPriority:(double)p; */
四:NSOperation
多线程我们还得提一下NSOperation,它可能比我们认识中的要强大一点,NSOperation也是有很多东西可以说的,前面的NSThread其实也是一样,这些要是仔细说的话都能写一篇文章出来,可能以后随着自己接触的越来越多,关于多线程这一块的东西我们会独立的创建一个分类总结出去。
首先得知道NSOperation是基于GCD封装的,NSOperation这个类本身我们使用的时候不多,更多的是集中在苹果帮我们封装好的NSInvocationOperation和NSBlockOperation
你command一下NSOperation进去看看,有几个点你还是的了解一下的,主要的就是下面的几个方法:
NSOperation * operation = [[NSOperation alloc]init]; [operation start]; //开始 [operation cancel]; //取消 [operation setCompletionBlock:^{ //operation完成之后的操作 }];
我们具体的说一下我们上面说的两个类:NSInvocationOperation和NSBlockOperation,先看看NSInvocationOperation的初始化:
/* 初始化方法 看过前面的文章之后它的target 、sel 、arg 等参数相信不难理解 -(nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg; -(instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER; */
补充: NS_DESIGNATED_INITIALIZER 指定初识化方法并不是对使用者。而是对内部的现实,可以点击进去具体了解一下它!NSInvocationOperation其实是同步执行的,因此单独使用的话就价值不大了,它和NSOperationQueue一起去使用才能实现多线程调用。这个我们后面再具体的说
在看看NSBlockOperation这个,它重要的方法就我们下面的两个
/* 初始化方法 + (instancetype)blockOperationWithBlock:(void (^)(void))block; 添加一个可以执行的block到前面初始化得到的NSBlockOperation中 - (void)addExecutionBlock:(void (^)(void))block; */
NSBlockOperation这个我们得提一点: 它的最大的并发具体的最大并发数和运行环境也是有关系的,具体的内容我们可以戳戳这里同行总结以及验证的,我们由于篇幅的原因就不在这里累赘。
其实只要是上面这些的话是不够我们日常使用的,但还有一个激活它们俩的类我们也得说说:NSOPerationQueue 下面是关于它的大概的一个说明,都挺简单,就不在特意写Demo。
关于NSOperation的我们就说这么多,下面重点说一下GCD。
五:主角GCD -- 主线程
一、我们先从主队列,主线程开始说起,通过下面的方法我们就可以获取得到主队列:
dispatch_queue_t mainqueue = dispatch_get_main_queue();
二、我们在主线程同步执行任务,下面是操作的结果以及打印的信息:
我们解释一下为什么在主线程中执行同步任务会出现这个结果,我们一步一步的梳理一下这个执行过程:
- 获取到在主队列主线程中执行了最前面的打印信息,这个没什么问题
- 开始执行dispatch_sync这个函数,主队列是串行队列,这个函数会把这个任务插入到主队列的最后面(理解队列添加任务)
- 主线程执行到这里的时候就会等待插入的这个同步任务执行完之后再执行后面的操作
- 但由于这个同步任务是插入到主队列的最后面,主队列前面的任务没有执行完之前是不会执行这个block的(主线程在执行initMainQueue任务)
- 这样就造成了一个相互等待的过程,主线程在等待block完返回,block却在等待主线程执行它,这样就造成了死锁,看打印的信息你也就知道block是没有被执行的。
这里我们你可能会思考,主队列是一个串行队列,那我们在主线程中添加一个串行队列,再给串行队列添加一个同步任务,这时候和前面主线程主队列添加同步任务不就场景一样了吗?那结果呢? 我们看看下面的打印:
我们按照前面的方式解释一下这个的执行步骤:
- 主线程在执行主队列中的方法initSerialQueue,到这个方法时候创建了一个串行队列(注意不是主队列)打印了前面的第一条信息
- 执行到dispatch_sync函数,这个函数给这个串行队列中添加了一个同步任务,同步任务是会立马执行的
- 主线程就直接操作执行了这个队列中的同步任务,打印的第二条信息
- 主线程接着执行下面的第三条打印信息
理解:看这个执行的过程对比前面造成死锁的,不同的地方就是前面是添加在了主队列当中,但这里没有添加到主队列。前面是插入到主队列的末尾,所以需要主队列的任务都执行完才能指定到它,但主线程执行到initMainQueue这个方法的时候在等待这个方法中添加的同步任务执行完接着往下执行,但它里面的同步任务又在等待主线程执行完在执行它,就相互等待了,但主线程执行不是主队列里面的同步任务的时候是不需要主线程执行完所有操作再执行这个任务的,这个任务是它添加到串行队列的开始也是结束的任务,就不需要等待,也就不会造成死锁!
上面这个问题经常会看到有人问,有许多解释,也希望自己能把这个问题给说清楚了!
三、主线程这里我们再提一点,就是线程间的信息简单传递
前面我们有说到主线程又叫做UI线程,所有关于UI的事我们都是在主线程里面更新的,像下载数据以及数据库的访问等这些耗时的操作我们是建议放在子线程里面去做,那就会产生子线程处理完这些之后要回到主线程更行UI的问题上,这一点值得我们好好的注意一下,但其实这一点也是我们用的最多的,相信大家也都理解!
六:主角GCD -- 串行队列
串行队列的概念性的东西我们就不在这里说了,不管是串行队列+同步任务还是串行队列+异步任务都简单,有兴趣可以自己了解一下,后面分析会提到它们的具体使用,我们再说一个稍微比前面的说的复杂一点点的类型,串行队列+异步+同步,可以先试着不要往下面看结果,自己先分析一下下面这段代码的执行结果。
static void * DISPATCH_QUEUE_SERIAL_IDENTIFY; -(void)initDiapatchQueue{ dispatch_queue_t serialQueue = dispatch_queue_create(DISPATCH_QUEUE_SERIAL_IDENTIFY, DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ NSLog(@"一个异步任务的内容%@",[NSThread currentThread]); dispatch_sync(serialQueue, ^{ NSLog(@"一个同步任务的内容%@",[NSThread currentThread]); }); }); }
不知道你分析的这段代码的执行结果是什么,我们这里来看看实际运行结果,然后和上面一步一步的分析一下它的整个的执行过程,就能找到答案:
答案就是crash了,其实也是死锁,下面一步一步的走一下这整个过程,分析一下哪里死锁了:
- 主线程主队列中执行任务initDispatchQueue,进入了这个方法,在这个方法里面创建了一个串行队列,这一步相信大家都明白,没什么问题。
- 给这个串行队列添加了一个异步任务,由于是异步任务,所以会开启一条新的线程,为了方便描述,我们把新开的这个线程记做线程A, 把这个任务记做任务A,也由于是异步任务,主线程就不会等待这个任务返回,就接着往下执行其他任务了。
- 接下来的分析就到了这个线程A上,这个任务A被添加到串行队列之后就开始在线程A上执行,打印出了我们的第一条信息,也证明了不是在主线程,这个也没问题。
- 线程A开始执行这个任务A,进入这个任务A之后在这个任务A里面又同步在串行队列里面添加任务,记做任务B,由于任务B是dispatch_sync函数同步添加的,需要立马被执行,就等待线程A执行它
- 但是这个任务B是添加到串行队列的末尾的,线程A在没有执行完当前任务A是不会去执行它的,这样就造成线程A在等待当前任务A执行完,任务B又在等待线程A执行它,就形成了死锁
经过上面的分析,你就能看到这个场景和你在主线程同步添加任务是一样的,我们再仔细的考虑一下这整个过程,在分析一下上面主线程+串行队列+同步任务为什么没有形成死锁!相互对比理解,就能把整个问题想明白。
七:主角GCD -- 并行队列
下面我们接着再说说这个并行队列,并行队列+同步执行或者并行队列+异步执行这个我们也就没什么好说的了,在这里说说并行+异步的需要注意的地方,不知道大家有没有想过,并行的话很多任务会一起执行,要是异步任务的话会开启新的线程,那是不是我们添加了十个异步任务就会开启十条线程呢?那一百个异步任务岂不是要开启一百条线程,答案肯定是否定的!那系统到底是怎么处理的,我们也说说,下面的是高级编程书里面的解释我们梳理一下给出结论。
- 当为DISPATCH_QUEUE_CONCURRENT的时候,不用等待前面任务的处理结束,后面的任务也是能够直接执行的
- 并行执行的处理数量取决于当前系统的状态,即iOS和OS X基于Dispatch Queue中的处理数、CPU核数以及CPU负荷等当前系统状态来决定DISPATCH_QUEUE_CONCURRENT中并行执行的处理数
- iOS 和 OS X的核心 -- XNU内核决定应当使用的线程数,并且生成所需的线程执行处理
- 当处理结束,应当执行的处理数减少时,XNU内核会结束不在需要的线程
- 处理并行异步任务时候线程是可以循环往复使用的,比如任务1的线程执行完了任务1,线程可以接着去执行后面没有执行的任务
这里的东西就这些,我们在前面串行队列的时候,串行队列+异步任务嵌套同步任务会造成死锁,那我们要是把它变成并行队列呢?结果又会是什么样子呢?我们看看下面这段代码的执行结果:
从上面的结果可以看得出来,是没有问题的,这里我们就不在一步一步的分析它的执行过程了,就说说为什么并行的队列就没有问题,但是串行的队列就会出问题:
并行队列添加了异步任务也是创建了一个新的线程,然后再在这个任务里面给并行队列添加一个同步任务,由于是并行队列 ,执行这个同步任务是不需要前面的异步任务执行完了,就直接开始执行,所以也就有了下面的打印信息,通过上面几个问题,相信理解了之后,对于串行队列或者并行队列添加同步任务或者异步任务都有了一个比较深的理解了,我们再接着往下总结。
八:GCD不仅仅这些
关于GCD的内容还有下面这些都是值得我们关注的,我们再说一说:
- dispatch_barrier_async
dispatch_barrier_async 函数是我们俗称的栅栏方法,“栅栏”的意思理解一下字面的,就是把外面和里面阻隔开,这个函数的作用就是这样,把插入的这个栅栏之前和之后的阻隔开,等前面的执行完了就执行“栅栏函数”插入的任务,等栅栏的任务执行结束了就开始执行栅栏后面的任务。看下面一个简单的Demo就理解了。
从上面就可以看到,我们把0插入到第三个任务的位置,它是等前面的两个任务执行完了,在去执行第三个,要是你觉得这里前两个任务简单,执行不需要太多的时间的话,你可以试着把前面两个任务的“任务量”设置大一点,这样有助于你更好的理解这个“栅栏”操作!
-
dispatch_after
dispatch_after 延时操作
如果某一条任务你想等多少时间之后再执行的话,你就完全可以使用这个函数处理,写法很简单,因为已经帮我们封装好了,看下面这两行代码:
// DISPATCH_TIME_NOW 当前时间开始 // NSEC_PER_SEC 表示时间的宏,这个可以自己上网搜索理解 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"延迟了10秒执行"); });
-
dispatch_apply
dispatch_apply 类似一个for循环,会在指定的dispatch queue中运行block任务n次,如果队列是并发队列,则会并发执行block任务,dispatch_apply是一个同步调用,block任务执行n次后才返回。 由于它是同步的,要是我们下面这样写就会有出问题:(其实和最开始说的主线程添加同步任务相同)
可以看到出问题了,但我们要是把它放在串行队列或者并行队列就会是下面这样的情况
- dispatch_group_t
dispatch_group_t的作用我们先说说,在追加到Dispatch Queue 中的多个任务全部结束之后想要执行结束的处理,这种情况也会经常的出现,在只使用一个Serial Dispatch Queue时,只要将想执行的操作全部追加该Serial Dispatch Queue中并且追加在结束处理就可以实现,但是在使用 Concurrent Dispatch Queue 时或者同时使用多个 Dispatch Queue时候,就比较的复杂了,在这样的情况下 Dispatch Group 就可以发挥它的作用了。看看下面的这段代码:
-(void)testDispatch_group_t{ dispatch_group_t group_t = dispatch_group_create(); dispatch_queue_t queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_async(group_t, queue_t, ^{ NSLog(@"1--当前的线程%@",[NSThread currentThread]); }); dispatch_group_async(group_t, queue_t, ^{ NSLog(@"2--当前的线程%@",[NSThread currentThread]); }); dispatch_group_async(group_t, queue_t, ^{ NSLog(@"3--当前的线程%@",[NSThread currentThread]); }); dispatch_group_async(group_t, queue_t, ^{ for (int i = 1; i<10; i++) { NSLog(@"4--当前的线程%@",[NSThread currentThread]); } }); // 当前的所有的任务都执行结束 dispatch_group_notify(group_t, queue_t, ^{ NSLog(@"前面的全都执行结束了%@",[NSThread currentThread]); }); }
这段代码的意图很明显,看了下面的打印信息这个你也就理解它了:
总结: 关于多线程的最基本的问题暂时先总结这么多,还有许多的问题,自己也在总结当中,比如以下线程锁等等的问题,等总结到差不多的时候再分享!