iOS之多线程的使用和优缺点比较

进程与线程的联系与区别

  • 进程(process)是一块包含了某些资源的内存区域,操作系统利用进程把它的工作划分为一些功能单元。进程是操作系统的基础,是一次程序的执行;它是操作系统动态执行的基本单元,在传统的操作系统中,进程是基本的分配单元,也是基本的执行单元。即正在进行中的程序被称为进程,负责程序运行的内存分配,每一个进程都有自己独立的虚拟内存空间。

  • 线程(thread):一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程)。线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行。主线程最大占1M的栈区空间,每条子线程最大占512K的栈区空间 。

  • 进程和线程都是由操作系统所体现的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性(并发是指两个或多个任务在同一时间间隔内发生,但是在任意一个时间点CPU只会处理一个任务),进程是线程的容器,真正完成代码执行的过程,而进程则作为线程的执行环境。

  • 两者的主要区别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式的影响下不会对其他进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等同于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

多线程的实现

  • 创建线程的目的:开启一条新的执行路径,运行指定的代码,与主线程中的代码实现同时运行。

  • 多线程的优势:
    1、充分发挥多核处理器优势,将不同线程任务分配给不同的处理器,真正进入"并行运算"状态;
    2、将耗时的任务分配到其他线程执行,由主线程负责统一更新界面会使应用程序更加流畅,用户体验更好;
    3、当硬件处理器的数量增加,程序会运行更快,而程序无需做任何调整。

  • 多线程的劣势:新建线程会消耗内存空间和CPU时间,线程太多会降低系统的运行性能。

  • 使用多线程的情况:
    1、大量运算,比如for循环计算量特别大的时候;
    2、数据读取(本地),数据库查询所有的东西;
    3、网络请求的时候(同步)。

  • 线程安全:当多个线程同时访问一个资源时,会出现线程安全问题。为避免出现线程安全问题,需要在代码中保证数据、变量的线程安全。UI Kit 中的所有对象默认都是非线程安全的,需要在主线程刷新UI界面元素。iOS中保证变量线程安全的方法为atomic 特性属性声明、@synchronized 关键字和NSLock 线程锁。

  • 多线程的实现技术

iOS有三种多线程编程的技术,分别是NSThread、NSOperation和GCD(Grand Central Dispatch)。

1、NSThread
即简单开辟一个线程处理需要放到后台的操作。NSThread 比其他两个轻量级,但缺点是需要自己管理线程的生命周期,线程同步,线程同步对数据的加锁会有一定的系统开销。
两种创建方法:

// 第一种创建
  NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(calculator) object:nil];
 [thread1 start];
 // 第二种创建
 [NSThread detachNewThreadSelector:@selector(calculator) toTarget:self withObject:nil];

常用的方法:

// 取消线程(void)cancel NS_AVAILABLE(10_5, 2_0);
// 执行线程(void)start NS_AVAILABLE(10_5, 2_0);

2、NSOperation
即将一个操作封装成对象,并开辟一个新线程执行。不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。与之相关的类是 NSOperation ,NSOperationQueue。NSOperation是个抽象类,使用它必须用它的子类(并重写main方法,加入自定义操作),可以实现它或者使用它定义好的两个子类:NSInvocationOperation和NSBlockOperation。创建NSOperation子类的对象,把对象添加到NSOperationQueue队列里执行。

NSBlockOperation的使用:

// 执行队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // 开辟线程做什么?
}];
[blockOperation setCompletionBlock:^{
    // 线程执行结束后做什么?
}];
[queue addOperation:blockOperation];

NSInvocationOperation使用:

// 执行队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 开辟线程
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] 
initWithTarget:self selector:@selector(loadWithURL:) object:IMGURL];
[queue addOperation:invocationOperation];

3、GCD
GCD是一种较为底层的多线程实现方式,其原理是将操作封装为block,并加入指定队列中开辟新线程执行。NSOperation以及NSOperationQueue都是对GCD机制的高层封装,使用GCD可以实现更加灵活的多线程处理。

优点:
(1)通过GCD,开发者不用再直接跟线程打交道,只需要向队列中添加代码块即可;
(2)GCD在后端管理着一个线程池,GCD不仅决定着代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理,从而让开发者从线程管理的工作中解放出来;通过集中的管理线程,缓解大量线程被创建的问题;

调度队列(dispath queue)
GCD的核心理念:将长期运行的任务拆分成多个工作单元,并将这些单元添加到dispath queue中,系统会为我们管理这些dispath queue,为我们在多个线程上执行工作单元,我们不需要直接启动和管理后台线程。GCD的dispath queue严格遵循FIFO(先进先出)原则,添加到dispath queue的工作单元将始终按照加入dispath queue的顺序启动。
dispatch queue按先进先出的顺序,串行或并发地执行任务:
(1) serial dispatch queue一次只能执行一个任务, 当前任务完成才开始出列并启动下一个任务;
(2)concurrent dispatch queue则尽可能多地启动任务并发执行。

创建和管理dispatch queue

  • 获得全局并发Dispatch Queue (concurrent dispatch queue)
    (1)并发dispatch queue可以同时并行地执行多个任务,不过并发queue仍然按先进先出的顺序来启动任务。并发queue会在之前的任务完成之前就出列下一个任务并开始执行。并发queue同时执行的任务数量会根据应用和系统动态变化,各种因素包括:可用核数量、其它进程正在执行的工作数量、其它串行dispatch queue中优先任务的数量等。
    (2)系统给每个应用提供三个并发dispatch queue,整个应用内全局共享,三个queue的区别是优先级。不需要显式地创建这些queue,使用dispatch_get_global_queue函数便可获取这三个queue。
// 获取默认优先级的全局并发dispatch queue  
dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 

注:第一个参数用于指定优先级,分别使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW两个常量来获取高和低优先级的两个queue;第二个参数目前未使用到,默认0即可。
(3)虽然dispatch queue是引用计数的对象,但不需要retain和release全局并发queue。因为这些queue对应用是全局的,retain和release调用会被忽略。也不需要存储这三个queue的引用,每次都直接调用dispatch_get_global_queue获得queue就行。

  • 创建串行Dispatch Queue (serial dispatch queue)
    (1) 应用的任务需要按特定顺序执行时,就需要使用串行Dispatch Queue,串行queue每次只能执行一个任务。可以使用串行queue来替代锁,保护共享资源或可变的数据结构。和锁不一样的是,串行queue确保任务按可预测的顺序执行。而且只要异步地提交任务到串行queue,就永远不会产生死锁。
    (2)必须显式地创建和管理所有使用的串行queue,应用可以创建任意数量的串行queue,但不要为了同时执行更多任务而创建更多的串行queue。如果需要并发地执行大量任务,应该把任务提交到全局并发queue。
    (3)利用dispatch_queue_create函数创建串行queue,两个参数分别是queue名和一组queue属性。
dispatch_queue_t queue;  
queue = dispatch_queue_create("cn.itcast.queue", NULL);
  • 运行时获得公共Queue
    (1)使用dispatch_get_current_queue函数作为调试用途,或者测试当前queue的标识。在block对象中调用这个函数会返回block提交到的queue(这个时候queue应该正在执行中)。在block对象之外调用这个函数会返回应用的默认并发queue。
    (2)使用dispatch_get_main_queue函数获得应用主线程关联的串行dispatch queue。
    (3)使用dispatch_get_global_queue来获得共享的并发queue。

  • Dispatch Queue的内存管理
    (1) Dispatch Queue和其它dispatch对象(还有dispatch source)都是引用计数的数据类型。当创建一个串行dispatch queue时,初始引用计数为 1,可以使用dispatch_retain和dispatch_release函数来增加和减少引用计数。当引用计数到达 0 时,系统会异步地销毁这个queue。
    (2)对dispatch对象(如dispatch queue)retain和release 是很重要的,确保它们被使用时能够保留在内存中。和OC对象一样,通用的规则是如果使用一个传递过来的queue,应该在使用前retain,使用完之后release。
    (3)不需要retain或release全局dispatch queue,包括全局并发dispatch queue和main dispatch queue。
    (4) 即使实现的是自动垃圾收集的应用,也需要retain和release创建的dispatch queue和其它dispatch对象。GCD 不支持垃圾收集模型来回收内存。

添加任务到queue

要执行一个任务,需要将它添加到一个适当的dispatch queue,可以单个或按组来添加,也可以同步或异步地执行一个任务。一旦进入到queue,queue会负责尽快地执行任务。一般可以用一个block来封装任务内容。

  • 添加单个任务到queue
    (1)异步添加任务:异步或同步地添加一个任务到Queue,尽可能地使用dispatch_async或dispatch_async_f函数异步地调度任务。因为添加任务到Queue中时,无法确定这些代码什么时候能够执行。因此异步地添加block或函数,可以让你立即调度这些代码的执行,然后调用线程可以继续去做其它事情。特别是应用主线程一定要异步地 dispatch 任务,这样才能及时地响应用户事件。
    (2)同步添加任务:少数时候可能希望同步地调度任务,以避免竞争条件或其它同步错误。使用dispatch_sync和dispatch_sync_f函数同步地添加任务到Queue,这两个函数会阻塞当前调用线程,直到相应任务完成执行。注意:绝对不要在任务中调用 dispatch_sync或dispatch_sync_f函数,并同步调度新任务到当前正在执行的 queue。对于串行queue这一点特别重要,因为这样做肯定会导致死锁;而并发queue也应该避免这样做。
    (3)代码演示
// 调用前,查看当前线程  
NSLog(@"当前调用线程:%@", [NSThread currentThread]);  
 // 创建一个串行queue  
dispatch_queue_t queue = dispatch_queue_create("cn.itcast.queue", NULL);  
 dispatch_async(queue, ^{  
    NSLog(@"开启了一个异步任务,当前线程:%@", [NSThread currentThread]);  
});  
 dispatch_sync(queue, ^{  
    NSLog(@"开启了一个同步任务,当前线程:%@", [NSThread currentThread]);  
});  
// 销毁队列  
dispatch_release(queue);  

打印信息:

2016-03-03 09:03:37.348 thread[6491:c07] 当前调用线程:<NSThread: 0x714fa80>{name = (null), 
num = 1}  
2016-03-03 09:03:37.349 thread[6491:1e03] 开启了一个异步任务,当前线程:<NSThread: 0x74520a0>
{name = (null), num = 3}  
2016-03-03 09:03:37.350 thread[6491:c07] 开启了一个同步任务,当前线程:<NSThread: 0x714fa80>
{name = (null), num = 1}
  • 并发地执行循环迭代
    使用循环执行固定次数的迭代,并发dispatch queue可以提高性能。
int i;  
int count = 10;  
for (i = 0; i < count; i++) {  
   printf("%d  ",i);  
} 

如上面的for循环,如果每次迭代执行的任务与其它迭代独立无关,而且循环迭代执行顺序也无关紧要,可以调用dispatch_apply或dispatch_apply_f函数来替换循环。这两个函数为每次循环迭代将指定的block或函数提交到queue。当dispatch到并发 queue时,就有可能同时执行多个循环迭代。用dispatch_apply或dispatch_apply_f时可以指定串行或并发 queue。并发queue允许同时执行多个循环迭代,而串行queue就没太大必要使用了。
下面的代码使用dispatch_apply替换了for循环,传递的block必须包含一个size_t类型的参数,用来标识当前循环迭代。第一次迭代这个参数值为0,最后一次值为count - 1。

// 获得全局并发queue  
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
size_t count = 10;  
dispatch_apply(count, queue, ^(size_t i) {  
    printf("%zd ", i);  
});  
// 销毁队列  
dispatch_release(queue);

打印信息为:

1 2 0 3 4 5 6 7 8 9

可以看出,这些迭代是并发执行的。和普通for循环一样,ispatch_apply和dispatch_apply_f函数也是在所有迭代完成之后才会返回,因此这两个函数会阻塞当前线程,主线程中调用这两个函数必须注意,可能会阻止事件处理循环并无法响应用户事件。所以如果循环代码需要一定的时间执行,可以考虑在另一个线程中调用这两个函数。如果传递的参数是串行queue,而且正是执行当前代码的queue,就会产生死锁。

  • 在主线程中执行任务
    (1) GCD提供一个特殊的dispatch queue,可以在应用的主线程中执行任务。只要应用主线程设置了run loop(由CFRunLoopRef类型或NSRunLoop对象管理),就会自动创建这个queue,并且最后会自动销毁。非Cocoa应用如果不显式地设置run loop, 就必须显式地调用dispatch_main函数来显式地激活这个dispatch queue,否则虽然可以添加任务到queue,但任务永远不会被执行。
    (2) 调用dispatch_get_main_queue函数获得应用主线程的dispatch queue,添加到这个queue的任务由主线程串行化执行
    (3)代码实现:比如异步下载图片后,回到主线程显示图片。
// 异步下载图片  
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
    NSURL *url = [NSURL URLWithString:@"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg"];  
    UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];  
    // 回到主线程显示图片  
    dispatch_async(dispatch_get_main_queue(), ^{  
        self.imageView.image = image;  
    });  
});  
  • 任务中使用Objective-C对象
    GCD支持Cocoa内存管理机制,因此可以在提交到queue的block中自由地使用Objective-C对象。每个dispatch queue维护自己的autorelease pool确保释放autorelease对象,但是queue不保证这些对象实际释放的时间。如果应用消耗大量内存,并且创建大量autorelease对象,需要创建自己的autorelease pool,用来及时地释放不再使用的对象。

暂停和继续queue

可以使用dispatch_suspend函数暂停一个queue以阻止它执行block对象;用dispatch_resume函数继续dispatch queue。调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止。

Dispatch Group的使用

假设需要从网络上下载两张不同的图片,然后显示到不同的UIImageView上去,可以这样:

// 根据url获取UIImage  
- (UIImage *)imageWithURLString:(NSString *)urlString {  
   NSURL *url = [NSURL URLWithString:urlString];  
   NSData *data = [NSData dataWithContentsOfURL:url];  
   return [UIImage imageWithData:data];  
}  
- (void)downloadImages {  
   // 异步下载图片  
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
       // 下载第一张图片  
       NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
       UIImage *image1 = [self imageWithURLString:url1];  
       // 下载第二张图片  
       NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
       UIImage *image2 = [self imageWithURLString:url2];  
       // 回到主线程显示图片  
       dispatch_async(dispatch_get_main_queue(), ^{  
           self.imageView1.image = image1;  
           self.imageView2.image = image2;  
       });  
   });  
}

虽然这种方案可以解决问题,但其实两张图片的下载过程并不需要按顺序执行,并发执行它们可以提高执行速度。有个注意的地方就是必须等两张图片都下载完毕后才能回到主线程显示图片。Dispatch Group能够在这种情况下帮我们提升性能。
下面先看看Dispatch Group的用处:可以使用dispatch_group_async函数将多个任务关联到一个Dispatch Group和相应的queue中,group会并发地同时执行这些任务。而且Dispatch Group可以用来阻塞一个线程, 直到group关联的所有的任务完成执行。有时候必须等待任务完成的结果,然后才能继续后面的处理。
用Dispatch Group优化上面的代码:

// 根据url获取UIImage  
- (UIImage *)imageWithURLString:(NSString *)urlString {  
    NSURL *url = [NSURL URLWithString:urlString];  
    NSData *data = [NSData dataWithContentsOfURL:url];  
    // 这里并没有自动释放UIImage对象  
    return [[UIImage alloc] initWithData:data];  
}  
  
- (void)downloadImages {  
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
      
    // 异步下载图片  
    dispatch_async(queue, ^{  
        // 创建一个组  
        dispatch_group_t group = dispatch_group_create();  
          
        __block UIImage *image1 = nil;  
        __block UIImage *image2 = nil;  
          
        // 关联一个任务到group  
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
            // 下载第一张图片  
            NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
            image1 = [self imageWithURLString:url1];  
        });  
          
        // 关联一个任务到group  
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
            // 下载第一张图片  
            NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
            image2 = [self imageWithURLString:url2];  
        });  
          
        // 等待组中的任务执行完毕,回到主线程执行block回调  
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{  
            self.imageView1.image = image1;  
            self.imageView2.image = image2;  
              
            // 千万不要在异步线程中自动释放UIImage,因为当异步线程结束,异步线程的自动释放池也会被销毁,那么UIImage也会被销毁  
              
            // 在这里释放图片资源  
            [image1 release];  
            [image2 release];  
        });  
          
        // 释放group  
        dispatch_release(group);  
    });  
}  

dispatch_group_notify函数用来指定一个额外的block,该block将在group中所有任务完成后执行。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

╰つ栺尖篴夢ゞ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值