2017/02/20
今天的目标只有一个:弄懂多线程(真心的)
一、认识多线程
进程:
在系统中正在运行的一个应用程序;
线程:
一个进程想要执行任务,必须得有线程(每一个进程至少有一条线程),线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
多线程:
一个进程中可以开启多条线程,每条线程可以并行执行不同的任务(比如:进程->车间,线程->车间工人)
(1)每一个程序都有一个主线程,程序启动时创建(调用main来启动)
(2)主线程的生命周期是和应用程序绑定的,程序退出(结束)时,主线程也就停止了
(3)多线程技术表示,一个应用程序有多个线程,使用多线程能提供CPU的使用率,防止主线程的堵塞
(4)任何有可能堵塞主线程的任务不要在主线程执行(访问网络)
二、多线程的原理
同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)
多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
思考:如果线程非常非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源
每条线程被调度执行的频次会降低(线程的执行效率降低)
三、多线程的优缺点
优点:
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU、内存的利用率)
缺点:
- 开启线程需要占用一定的内存空间(默认情况下,主线程占用了1M,子线程占用512K),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU在调度线程上的开销就越大
- 程序设计更加复杂:比如线程之间的通信、多线程的数据共享
四、多线程在iOS开发中的应用
主线程:一个iOS程序运行后,默认会开启一条线程,称为“主线程”或“UI线程”
主线程的主要作用:
- 显示/刷新UI界面
- 处理UI事件(比如:点击事件、滚动事件、拖拽事件等)
主线程的使用注意:别将比较耗时操作放到主线程中。
耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
使用场合:
- 耗时操作,例如:网络图片、视频、歌曲、书籍等资源下载
- 游戏中的声音播放
五、iOS的三种多线程技术
1、NSThread:
优点:NSThread比其他两个轻量级,使用简单
缺点:不能控制线程的执行顺序(需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁对会有一定的系统开销)【需要使用start方法,才能启动实例化出来的线程、控制并发线程数、先后顺序困难,例如:下载图片(后台线程)->滤镜美化(后台线程)->更新UI(主线程)】
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
for(int i=1; i<100; i++) {
NSLog(@"=====%@======%d",[[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil].name,i);
if (i == 7) {
//创建线程对象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
//启动新线程
[thread start];
/**
* 创建并启动新线程
*/
//[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
/**
* 上面两种方法本质上都是将target对象的selector方法转换为线程执行体,其中selector方法最多可以接收一个参数,而argument就是代表传给selector方法的参数。这两种创建新线程的方式并没有明显的区别,只是上面的是一个实例化方法,该方法返回一个thread对象,调用start方法开启线程。下面的不会返回NSThread对象,因此这种方法会直接创建并启动新线程。
*
*/
}
}
/**
* 每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的则反之。每个子线程默认的优先级是0.5. NSThread 通过如下代码演示优先级。
*/
// NSLog(@"UI线程的优先级为:%g",[NSThread threadPriority]);
// //创建第一个线程对象
// NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
// //设置第一个线程对象的名字
// thread1.name = @"线程A";
// NSLog(@"线程A的优先级为:%g",thread1.threadPriority);
// //设置使用最低优先级
// thread1.threadPriority = 0.0;
// //创建第二个线程对象
// NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
// //设置第二个线程对象的名字
// thread2.name = @"线程B";
// NSLog(@"线程B的优先级为:%g",thread2.threadPriority);
// //设置使用最高优先级
// thread2.threadPriority = 1.0;
// //启动两个线程
// [thread1 start];
// [thread2 start];
}
- (void)run {
for (int i=0; i<100; i++) {
// if ([NSThread currentThread].isCancelled) {
// //终止当前正在执行的线程
// [NSThread exit];
// }
NSLog(@"------%@-----%d",[NSThread currentThread].name,i);
//没执行一次,线程暂停0.5秒
// [NSThread sleepForTimeInterval:0.5];
//使当前线程暂停一段时间,或者暂停到某个时刻
// + (void)sleepForTimeInterval:(NSTimeInterval)time;
// + (void)sleepUntilDate:(NSDate *)date;
}
}
//- (IBAction)cancelThread:(id)sender {
// //取消thread线程,调用该方法,调用该方法后,thread的isCancelled方法将会返回NO
// [thread cancel];
//}
@end
相关代码下载:OC-NSThread
2.NSOperation和NSOperationQueue
NSOperation是一个抽象基类,基本没有什么实际使用价值。我们使用最多的是系统封装好的NSInvocationOperation和NSBlockOperation。
(1)NSOperation
NSOperation 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。 大家可以看到 NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。操作步骤也很好理解:
- 将要执行的任务封装到一个 NSOperation 对象中。
- 将此任务添加到一个 NSOperationQueue 对象中。
NSOperation * operation = [[NSOperation alloc]init];
//开始执行
[operation start];
//取消执行
[operation cancel];
//执行结束后调用的Block
[operation setCompletionBlock:^{
NSLog(@"执行结束");
}];
NSInvocationOperation的使用方式和给Button添加事件比较相似,需要一个对象和一个Selector。使用方法非常简单。
添加任务
值得说明的是,NSOperation 只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务。分别是:NSInvocationOperation 和 NSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel 方法即可。
//创建
NSInvocationOperation * invo = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation) object:nil];
//执行
[invo start];
- (void)testNSOperation {
NSLog(@"我在第%@个线程",[NSThread currentThread]);
}
我们可以看到NSInvocationOperation其实是同步执行的,因此单独使用的话,这个东西也没有什么卵用,它需要配合我们后面介绍的NSOperationQueue去使用才能实现多线程调用,所以这里我们只需要记住有这么一个东西就行了。参考文章:NSOperation
(2)NSBlockOperation
//1.创建NSBlockOperation对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//2.开始任务
[operation start];
3.GCD
Grand Central Dispatch,听名字就霸气。它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。同时它使用的也是 c语言,不过由于使用了 Block(Swift里叫做闭包),使得使用起来更加方便,而且灵活。
首先来了解两个基本概念:任务和队列
-
任务:及操作,你想要干什么,说白了就是一段代码,在 GCD 中就是一个 Block,所以添加任务十分方便。任务有两种执行方式: 同步执行 和 异步执行,他们之间的区别是 是否会创建新的线程。
同步(sync) 和 异步(async) 的主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕! 如果是 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。 如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。
-
队列:用于存放任务。一共有两种队列,串行队列和并行队列(先入先出)。
口诀:
- 同步不开异步开,串行开一条,并行开多条
(1)创建队列
主队列:
这是一个特殊的 串行队列。什么是主队列,大家都知道吧,它用于刷新 UI,任何需要刷新 UI 的工作都要在主队列执行,所以一般耗时的任务都要放到别的线程执行。
dispatch_queue_t queue = ispatch_get_main_queue();
自己创建的队列:
第一个参数是标识符,第二个参数用来表示创建的对垒是串行还是并行。
//创建串行队列 - DISPATCH_QUEUE_SERIAL或NULL
serialQueue = dispatch_queue_create("fkjava.queue", DISPATCH_QUEUE_SERIAL);
//创建并发队列 - DISPATCH_QUEUE_CONCURRENT
concurrentQueue = dispatch_queue_create("fkjava.queue", DISPATCH_QUEUE_CONCURRENT);
全局并发队列:
只要是并行任务一般都加入到这个队列。这是系统提供的一个并发队列。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
(2)创建任务
-
同步任务:会阻塞当前线程(sync)
//将代码块以同步方式提交给指定队列,该队列底层的线程池将负责执行该代码块
dispatch_sync(<#dispatch_queue_t queue#>, <#^(void)block#>)
//将函数以同步方式提交给指定队列,该队列底层的线程池将负责执行该函数
dispatch_sync_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>) -
异步任务:不会阻塞当前线程 (async)
//将代码块以异步方式提交给指定队列,该队列底层的线程池将负责执行该代码块 dispatch_async(<#dispatch_queue_t queue#>, <#^(void)block#>) //将函数以异步方式提交给指定队列,该队列底层的线程池将负责执行该函数 dispatch_async_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
相关代码下载:OC-GCD
网友评论