必备知识架构-线程与网络-①线程 [toc]
目录
1 2 3 4 5 >1、多线程的原理 >2、多线程的优缺点 >3、多线程在iOS开发中的应用 >4、iOS中三种多线程技术(NSThread、NSOperation/NSOperationQueue、GCD) >
1 2 3 4 5 6 >线程间通信的体现: >1. 一个线程传递数据给另一个线程 >2. 在一个线程中执行完特定任务后,转到另一个线程继续执行任务 > >附:其他传值方法总结 >
1 2 3 4 5 6 7 8 >1、通常的做法/不好的做法 >2、合理的做法(使用多线程异步执行) >1)、先说说简单的多线程同步问题(异步线程里没有再开启异步线程) >2)、真正的线程同步问题(异步线程里再开启异步线程) >3)、其他补充 >附1:多个请求依次执行 >附2:并发数控制 >
1 2 3 >1、在使用SQLite过程中,如果多条线程同时操作同一数据库会造成什么问题,怎么解决? >2、串行队列与并行队列的区别 >
一、多线程介绍
< 返回目录
1、多线程的原理 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。 思考:如果线程非常非常多,会发生什么情况? 答:CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。
2、多线程的优缺点 1 2 3 4 5 6 7 8 多线程的优点 能适当提高程序的执行效率 能适当提高资源利用率(CPU、内存利用率) 多线程的缺点 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能 线程越多,CPU在调度线程上的开销就越大 程序设计更加复杂:比如线程之间的通信、多线程的数据共享
3、多线程在iOS开发中的应用 主线程:
一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”
主线程的主要作用:
显示\刷新UI界面,处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程的使用注意:
别将比较耗时的操作放到主线程中。耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验。
4、iOS中三种多线程技术(NSThread、NSOperation/NSOperationQueue、GCD) 1.NSThread
使用nsthread对象建立一个线程非常方便
但是!要使用nsthread管理多个线程非常困难,不推荐使用
技巧!使用[NSThread currentThread]
跟踪任务所在线程,适用于这三种技术
2.NSOperation/NSOperationQueue NSOperation和NSOperationQueue相关
是使用gcd实现的一套objective-c的api
是面向对象的线程技术
提供了一些在gcd中不容易实现的特性,如:限制最大并发数量、操作之间的依赖关系
1 2 3 4 5 6 7 8 9 10 > NSOperation`是系统提供的抽象的基类,我们使用的时候需要使用继承于它的子类。系统为我们提供了两种继承于`NSOperation`的子类,分别是`NSInvocationOperation`和`NSBlockOperation。 > > 当将操作添加到主操作队列时,所有操作会按照添加到队列中的先后顺序串行依次执行。(主队列是GCD自带的一种特殊串行队列。) > NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; > [mainQueue addOperation:op1]; > > 当将任务添加到自定义队列的时候,会开启子线程,操作会并发执行。 > NSOperationQueue *customQueue = [[NSOperationQueue alloc]init]; > [customQueue addOperation:op1]; >
3.GCD —— grand central dispatch
是基于c语言的底层api
用block定义任务,使用起来非常灵活便捷
提供了更多的控制能力以及操作队列中所不能使用的底层函数
NSOperationQueue与GCD的使用原则和场景 NSOperationQueue与GCD的使用原则和场景
三、线程等待(线程同步)、线程依赖、任务顺序问题
< 返回目录
多个任务中,某个线程的执行依赖其他线程的执行完毕,或者某个线程的执行需等待其他线程执行完毕。
常见场景: 某个页面加载时通过网络请求获得相应的数据,再做某些操作。有时候加载的内容需要通过好几个请求的数据组合而成,比如有两个请求A和B。
1、通常的做法/不好的做法:把并发队列变成串行 常见的有:为了省事,会将B请求放在A请求成功的回调中发起,将C请求放在B请求成功的回调中发起,在C的成功回调中将数据组合起来,这样做有明显的问题:
1 2 ①、请求如果多了,需要写许多嵌套的请求 ②、请求被变成了同步的方式了,这是最大的问题,在网络差的情况下,如果有n个请求,意味着用户要等待n倍于并发请求的时间才能看到内容
2、合理的做法(使用多线程异步执行) 1)、先说说简单的多线程同步问题(异步线程里没有再开启异步线程) 首先我们先来看简单的多线程同步问题(异步线程里没有再开启异步线程),这种时候,我们可以使用的方法有
①使用GCD线程组dispatch_group的dispatch_group_notify即可解决问题 。其中notify的作用就是在group中的其他操作全部完成后,再操作自己的内容。 ② 使用NSOperationQueue中operation的addDependency③使用dispatch_barrier_async 方法。
①、使用GCD线程组dispatch_group的dispatch_group_notify即可解决问题 使用dispatch_group方法的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //请求1 NSLog(@"Request_1"); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //请求2 NSLog(@"Request_2"); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //界面刷新 NSLog(@"任务均完成,刷新界面"); });
②使用使用NSOperationQueue中operation的addDependency,则是 1 2 3 4 5 6 7 //4.设置依赖 [operation3 addDependency:operation1]; //任务二依赖任务一 [operation3 addDependency:operation2]; //任务三依赖任务二 //5.创建队列并加入任务 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
③dispatch_barrier_async 2)、真正的线程同步问题(异步线程里再开启异步线程) 对于上面的例子,当将上面三个操作改成真实的网络操作后,这个简单的做法突然变得无效了,这是为什么呢?
其实这个道理很简单,因为我们开启的网络请求,是一个异步线程,所谓的异步线程,就是告诉系统你不要管我是否完成了,你尽管执行其他操作,开一个线程让我到外面操作去执行就行了,对我的处理你已经完成了,也就是说线程只负责将请求发出去,就认为自己的任务算完成了。所以,当三个请求都发送出去后,不会管网络操作是否完成,就会执行notify中的内容,但由于请求结果本身需要一定的时间,所以导致了界面都刷新了,而请求结果才返回。
即上面的问题,总结为如果dispatch_group_async里执行的是异步代码dispatch_group_notify会直接触发而不会等待异步任务完成。
所以,对于这种异步线程里开异步线程的线程同步问题,该怎么处理呢?
为了应对这种异步线程里开异步线程的线程同步问题,其实我们需要多处理的只是监控异步线程里的所有操作(包括异步线程里的异步线程)都结束后,才算这个异步线程结束 。
这里我们的解决方法常见的有:
方法A:直接使用dispatch_group_enter和dispatch_group_leave,即只需要在任务开始前enter和结束后leave即可达到线程同步的效果。
dispatch_group_enter(group):下面的任务由group组管理,group组的任务数+1 dispatch_group_leave(group):相应的任务执行完成,group组的任务数-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 - (void)testGroupSync_userEnterLeave { dispatch_group_t group = dispatch_group_create(); dispatch_group_enter(group); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //请求1 dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(5); NSLog(@"任务一完成"); dispatch_group_leave(group); }); }); dispatch_group_enter(group); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //请求2 dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(8); NSLog(@"任务二完成"); dispatch_group_leave(group); }); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //界面刷新 NSLog(@"任务均完成,刷新界面"); }); }
方法B:使用信号量,控制异步线程的结束,来达到多线程同步的效果。
在GCD中有三个函数是semaphore的操作,分别是:
1 2 3 dispatch_semaphore_create 创建一个semaphore dispatch_semaphore_signal 发送一个信号(信号量+1) dispatch_semaphore_wait 等待信号(wait执行完后,信号量-1)
实际使用,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 - (void)testGroupSync_semaphore_signal_wait { dispatch_group_t group = dispatch_group_create(); // 创建一个控制线程同步的信号量,初始值为0(红灯) dispatch_semaphore_t syncSemaphore = dispatch_semaphore_create(0); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //先再任务1的线程里中再开启一个异步线程执行请求1,同时堵塞住此时的任务1所在的线程,等到已开始的异步请求1结束之后,原本阻塞住的任务1线程才会恢复畅通,才代表任务1结束了。 //请求1 dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(5); NSLog(@"任务一完成"); // 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯) dispatch_semaphore_signal(syncSemaphore); }); // 开启信号等待,设置等待时间为永久,直到信号的信号量大于等于1(等绿灯) dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //请求2 dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(8); NSLog(@"任务二完成"); // 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯) dispatch_semaphore_signal(syncSemaphore); }); // 开启信号等待,设置等待时间为永久,直到信号的信号量大于等于1(等绿灯) dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //界面刷新 NSLog(@"任务均完成,刷新界面"); }); }
附:实际上,这段代码是一个常用的不控制并发数,只控制线程阻塞,实现线程同步的例子。在这里它是将等待信号dispatch_semaphore_wait
放在线程执行后。往下看,待会会介绍控制并发数的例子。
上面例子中,先再任务1的线程里中再开启一个异步线程执行请求1,同时堵塞住此时的任务1所在的线程,等到已开始的异步请求1结束之后,恢复畅通,才代表任务1结束了。
3)、其他控制任务顺序的例子 附1:多个请求依次执行 例子:三个任务分别为下载图片,打水印和上传图片,三个任务需异步执行但需要顺序性。
对于这个问题通常会通过线程依赖进行解决。
1、如果是GCD的话,设置线程依赖比较繁琐,所以这里就不讨论了。
2、如果是NSOperation的话,我们直接通过addDependency来设置一个操作队列NSOperationQueue中的线程依赖就可以了。
代码如下(此例子不讨论线程里开线程,如果需要的话,请将信号量的使用添加上去即可解决):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //1.任务一:下载图片 NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{ [self request_A]; }]; //2.任务二:打水印 NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{ [self request_B]; }]; //3.任务三:上传图片 NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{ [self request_C]; }]; //4.设置依赖 [operation2 addDependency:operation1]; //任务二依赖任务一 [operation3 addDependency:operation2]; //任务三依赖任务二 //5.创建队列并加入任务 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
四、多线程并发数控制 当我们在处理一系列线程的时候,当数量达到一定量,我们需要处理并发数控制。
1、如果是GCD的话,怎么快速的控制并发呢?答案就是dispatch_semaphore。这个比较复杂,但我们这边还是谈一下
2、如果是NSOperation的话,我们直接使用NSOperationQueue来控制并发,这个就不谈了。
GCD使用信号量dispatch_semaphore控制并发
说道信号量,这里顺便谈下信号量的概念:
信号量就是一个资源计数器,它是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
,线程会被阻塞(如果有必要的话),直至计数器大于零,然后线程会减少这个计数。
上面的例子中,信号量初始值为0,其先执行到dispatch_semaphore_wait,此时由于信号量为0,所以造成了阻塞,而使得线程1没能结束它的工作。而到线程1里的异步线程执行完后,给信号量发送了一个通知,使得信号量的值加上了1,此时刚才线程1里的dispatch_semaphore_wait发现信号量大于等于1了,它就不再阻塞,而是继续执行下去,从而使得了线程1这时候真正结束了它所应该处理的工作。wait执行完后,信号量又减1了。
也就是通过这种线程阻塞/等待 的方法,我们实现了线程的同步。
当然信号量的用途,除了可以用来实现线程同步外,还可以用来实现控制GCD的并发数 。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (void)testBingfa { // 创建队列组 dispatch_group_t group = dispatch_group_create(); // 创建信号量,并且设置值为10 dispatch_semaphore_t semaphore = dispatch_semaphore_create(10); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for (int i = 0; i < 100; i++) { // 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); dispatch_group_async(group, queue, ^{ NSLog(@"%i",i); sleep(2); // 每次发送信号则semaphore会+1, dispatch_semaphore_signal(semaphore); }); } }
比较这段代码与上面的代码,控制并发一般都是将等待信号dispatch_semaphore_wait
放在线程执行前。而我们常用的只控制线程阻塞,实现线程同步的,都是将等待信号dispatch_semaphore_wait
放在线程执行后。所以,如果一个多线程,它需要同时控制线程同步和线程并发数的话,那它就需要创建两个信号量来分别控制。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 - (void)testGroupSyncAndBingfa_semaphore_signal_wait { dispatch_group_t group = dispatch_group_create(); // 创建一个控制线程并发的信号量,初始值为最大并发数2(红灯) dispatch_semaphore_t bingfaSemaphore = dispatch_semaphore_create(2); // 创建一个控制线程同步的信号量,初始值为0(红灯) dispatch_semaphore_t syncSemaphore = dispatch_semaphore_create(0); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号 //请求1 dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(5); NSLog(@"任务一完成"); dispatch_semaphore_signal(syncSemaphore); //同步信号 }); dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号 }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号 //请求2 dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(8); NSLog(@"任务二完成"); dispatch_semaphore_signal(syncSemaphore); //同步信号 }); dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号 }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号 //请求3 dispatch_async(dispatch_get_global_queue(0, 0), ^{ sleep(6); NSLog(@"任务三完成"); dispatch_semaphore_signal(syncSemaphore); //同步信号 }); dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号 }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //界面刷新 NSLog(@"任务均完成,刷新界面"); }); }
通过所附加的两个例子,我们明显的看出NSOperation在处理线程依赖以及并发数的问题上,明显更高级。所以,如果有类似的问题,我们优先使用NSOperation来处理。
常见笔试/面试题 < 返回目录
1、在使用SQLite过程中,如果多条线程同时操作同一数据库会造成什么问题,怎么解决?
答:(1)容易造成系统崩溃 (2)解决方案:开启串行模式,使用一个类(单例方式)操作数据库。
END
< 返回目录