iOS多线程之超实用理论+demo演示(可下载)

移动开发 作者: 2024-08-24 18:35:01
背景简介 在初学iOS相关知识过程中,大多都对多线程有些恐惧的心里,同时感觉工作中用上的概率不大。但是如果平时不多积累并学透多线程,当工作中真的需要用到的时候,就很可能简单百度后把一些知识点稀里糊涂地

背景简介

GCD、OperationQueue 对比

  • GCD的核心概念:将 任务(block) 添加到队列,并且指定执行任务的函数。
  • NSOperation 的核心概念:把 操作(异步) 添加到 队列。
    • 将任务(block)添加到队列(串行/并发/主队列),并且指定任务执行的函数(同步/异步)
    • GCD是底层的C语言构成的API
    • iOS 4.0 推出的,针对多核处理器的并发技术
    • 在队列中执行的是由 block 构成的任务,这是一个轻量级的数据结构
    • 要停止已经加入 queue 的 block 需要写复杂的代码
    • 需要通过 Barrier(dispatch_barrier_async)或者同步任务设置任务之间的依赖关系
    • 只能设置队列的优先级
    • 高级功能:
      dispatch_once_t(一次性执行,多线程安全);
      dispatch_after(延迟);
      dispatch_group(调度组);
      dispatch_semaphore(信号量);
      dispatch_apply(优化顺序不敏感大体量for循环);

GCD

串行队列(Serial Queues)

    private func serialExcuteByGCD(){
        let lArr : [UIImageView] = [imageView1,imageView2,imageView3,imageView4]
        
        //串行队列,异步执行时,只开一个子线程
        let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage")
        
        for i in 0..<lArr.count{
            let lImgV = lArr[i]
            
            //清空旧图片
            lImgV.image = nil
            
         //注意,防坑:串行队列创建的位置,在这创建时,每个循环都是一个新的串行队列,里面只装一个任务,多个串行队列,整体上是并行的效果。
            //            let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage")
            
            serialQ.async {
                
                print("第\(i)个 开始,%@",Thread.current)
                Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in
                    let lImgV = lArr[i]
                    
                    print("第\(i)个 结束")
                    DispatchQueue.main.async {
                        print("第\(i)个 切到主线程更新图片")
                        lImgV.image = img
                    }
                    if nil == img{
                        print("第\(i+1)个img is nil")
                    }
                }
            }
        }
    }

图中下载时可顺利拖动滚动条,是为了说明下载在子线程,不影响UI交互
第0个 开始
第0个 结束
第1个 开始
第0个 更新图片
第1个 结束
第2个 开始
第1个 更新图片
第2个 结束
第3个 开始
第2个 更新图片
第3个 结束
第3个 更新图片

并发队列(Concurrent Queues)

  • 依赖对象不同,barrier 依赖的对象是自定义并发队列,锁操作依赖的对象是线程。
  • 作用不同,barrier 起到自定义并发队列中栅栏的作用;锁起到多线程操作时防止资源竞争的作用。
private func concurrentExcuteByGCD(){
        let lArr : [UIImageView] = [imageView1,imageView4]
        
        for i in 0..<lArr.count{
            let lImgV = lArr[i]
            
            //清空旧图片
            lImgV.image = nil
            
            //并行队列:图片下载任务按顺序开始,但是是并行执行,不会相互等待,任务结束和图片显示顺序是无序的,多个子线程同时执行,性能更佳。
            let lConQ = DispatchQueue.init(label: "cusQueue",qos: .background,attributes: .concurrent)
            lConQ.async {
                print("第\(i)个开始,%@",Thread.current)
                Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in
                    let lImgV = lArr[i]
                      print("第\(i)个结束")
                    DispatchQueue.main.async {
                        lImgV.image = img
                    }
                    if nil == img{
                        print("第\(i+1)个img is nil")
                    }
                }
            }
        }
    }
第0个开始,%@ <NSThread: 0x600002de2e00>{number = 4,name = (null)}
第1个开始,%@ <NSThread: 0x600002dc65c0>{number = 6,name = (null)}
第2个开始,%@ <NSThread: 0x600002ddc8c0>{number = 8,name = (null)}
第3个开始,%@ <NSThread: 0x600002d0c8c0>{number = 7,name = (null)}
第0个结束
第3个结束
第1个结束
第2个结束

串行、并发队列对比图

注意事项

  • 无论串行还是并发队列,都是 FIFO ;
    一般创建 任务(blocks)和加任务到队列是在主线程,但是任务执行一般是在其他线程(asyc)。需要刷新 UI 时,如果当前不再主线程,需要切回主线程执行。当不确定当前线程是否在主线程时,可以使用下面代码:
/**
 Submits a block for asynchronous execution on a main queue and returns immediately.
 */
static inline void dispatch_async_on_main_queue(void (^block)()) {
    if (NSThread.isMainThread) {
        block();
    } else {
        dispatch_async(dispatch_get_main_queue(),block);
    }
}
    • DISPATCH_QUEUE_PRIORITY_HIGH
      
    • DISPATCH_QUEUE_PRIORITY_DEFAULT
      
    • DISPATCH_QUEUE_PRIORITY_LOW
      
    • DISPATCH_QUEUE_PRIORITY_BACKGROUND
      
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2 * NSEC_PER_SEC)),dispatch_get_main_queue(),^{
        NSLog(@"2s后执行");
    });
for (i = 0; i < count; i++) {
   printf("%u\n",i);
}
printf("done");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
 
 //count 是迭代的总次数。
dispatch_apply(count,queue,^(size_t i) {
   printf("%u\n",i);
});

//同样在上面循环结束后才调用。
printf("done");

OperationQueue

    • NSInvocationOperation (调用操作)
    • NSBlockOperation (块操作)
           一般常用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题。任务被封装在 NSOperation 的子类实例类对象里,一个 NSOperation 子类对象可以添加多个任务 block 和 一个执行完成 block ,当其关联的所有 block 执行完时,就认为操作结束了。
  • UIGestureRecognizer
  • CAAnimation
  • CAPropertyAnimation
func testDepedence(){
        let op0 = BlockOperation.init {
            print("op0")
        }
        
        let op1 = BlockOperation.init {
            print("op1")
        }
        
        let op2 = BlockOperation.init {
            print("op2")
        }
        
        let op3 = BlockOperation.init {
            print("op3")
        }
        
        let op4 = BlockOperation.init {
            print("op4")
        }
        
        op0.addDependency(op1)
        op1.addDependency(op2)
        
        op0.queuePriority = .veryHigh
        op1.queuePriority = .normal
        op2.queuePriority = .veryLow
        
        op3.queuePriority = .low
        op4.queuePriority = .veryHigh
        
        gOpeQueue.addOperations([op0,op1,op2,op3,op4],waitUntilFinished: false)
    }

 op4
 op2
 op3
 op1
 op0
 op4
 op3
 op2
 op1
 op0
///暂停队列,只对未执行中的任务有效。本例中对串行队列的效果明显。并发队列因4个任务一开始就很容易一起开始执行,即使挂起也无法影响已处于执行状态的任务。
    @IBAction func pauseQueueItemDC(_ sender: Any) {
        gOpeQueue.isSuspended = true
    }
    
    ///恢复队列,之前未开始执行的任务会开始执行
    @IBAction func resumeQueueItemDC(_ sender: Any) {
       gOpeQueue.isSuspended = false
    }
  • 一旦添加到操作队列中,操作对象实际上归队列所有,不能删除。取消操作的唯一方法是取消它。可以通过调用单个操作对象的 cancel 方法来取消单个操作对象,也可以通过调用队列对象的 cancelAllOperations 方法来取消队列中的所有操作对象。
  • 更常见的做法是取消所有队列操作,以响应某些重要事件,如应用程序退出或用户专门请求取消,而不是有选择地取消操作。

取消单个操作对象

取消队列中的所有操作对象

    deinit {
        gOpeQueue.cancelAllOperations()
        print("die:%@",self)
    }
  • 操作的 QOS 和队列的 QOS 有何关系?
    A:队列的 QOS 设置,会自动把较低优先级的操作提升到与队列相同优先级。(原更高优先级操作的优先级保持不变)。后续添加进队列的操作,优先级低于队列优先级时,也会被自动提升到与队列相同的优先级。
    注意,苹果文档如下的解释是错误的 This property specifies the service level applied to operation objects added to the queue. If the operation object has an explicit service level set,that value is used instead.
    原因详见:Can NSOperation have a lower qualityOfService than NSOperationQueue?

常见问题

  • 对于有明显先后依赖关系的任务,最佳方案是 GCD串行队列,可以在不使用线程锁时保证资源互斥。
  • 其他情况,对存在资源竞争的代码加锁或使用信号量(初始参数填1,表示只允许一条线程访问资源)。
  • 串行队列同步执行时,如果有任务相互等待,会死锁。
    比如:在主线程上同步执行任务时,因任务和之前已加入主队列但未执行的任务会相互等待,导致死锁。
  func testDeadLock(){
        //主队列同步执行,会导致死锁。block需要等待testDeadLock执行,而主队列同步调用,又使其他任务必须等待此block执行。于是形成了相互等待,就死锁了。
        DispatchQueue.main.sync {
            print("main block")
        }
        print("2")
    }
- (void)testSynSerialQueue{
    dispatch_queue_t myCustomQueue;
    myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue",NULL);
     
    dispatch_async(myCustomQueue,^{
        printf("Do some work here.\n");
    });
     
    printf("The first block may or may not have run.\n");
     
    dispatch_sync(myCustomQueue,^{
        printf("Do some more work here.\n");
    });
    printf("Both blocks have completed.\n");
}

“西饼传说”

  • 尽可能依赖 系统 框架。实现并发性的最佳方法是利用系统框架提供的内置并发性。
  • 尽早识别系列任务,并尽可能使它们更加 并行。如果因为某个任务依赖于某个共享资源而必须连续执行该任务,请考虑更改体系结构以删除该共享资源。您可以考虑为每个需要资源的客户机制作资源的副本,或者完全消除该资源。
  • 不使用锁来保护某些共享资源,而是指定一个 串行队列 (或使用操作对象依赖项)以正确的顺序执行任务。
  • 避免使用 GCD 调度队列操作队列 提供的支持使得在大多数情况下不需要锁定。

确定操作对象的适当范围

  • 尽管可以向操作队列中添加任意大量的操作,但这样做通常是不切实际的。与任何对象一样,NSOperation 类的实例消耗内存,并且具有与其执行相关的实际成本。如果您的每个操作对象只执行少量的工作,并且您创建了数以万计的操作对象,那么您可能会发现,您花在调度操作上的时间比花在实际工作上的时间更多。如果您的应用程序已经受到内存限制,那么您可能会发现,仅仅在内存中拥有数万个操作对象就可能进一步降低性能。
  • 有效使用操作的关键是 在你需要做的工作量和保持计算机忙碌之间找到一个适当的平衡 。尽量确保你的业务做了合理的工作量。例如,如果您的应用程序创建了 100 个操作对象来对 100 个不同的值执行相同的任务,那么可以考虑创建 10 个操作对象来处理每个值。
  • 您还应该避免将大量操作一次性添加到队列中,或者避免连续地将操作对象添加到队列中的速度快于处理它们的速度。与其用操作对象淹没队列,不如批量创建这些对象。当一个批处理完成执行时,使用完成块告诉应用程序创建一个新的批处理。当您有很多工作要做时,您希望保持队列中充满足够的操作,以便计算机保持忙碌,但是您不希望一次创建太多操作,以至于应用程序耗尽内存。
  • 当然,您创建的操作对象的数量以及在每个操作对象中执行的工作量是可变的,并且完全取决于您的应用程序。你应该经常使用像 Instruments 这样的工具来帮助你在效率和速度之间找到一个适当的平衡。有关 Instruments 和其他可用于为代码收集度量标准的性能工具的概述,请参阅 性能概述

术语解释摘录

  • 异步任务(asynchronous tasks):由一个线程启动,但实际上在另一个线程上运行,利用额外的处理器资源更快地完成工作。
  • 互斥(mutex):提供对共享资源的互斥访问的锁。
    互斥锁一次只能由一个线程持有。试图获取由不同线程持有的互斥对象会使当前线程处于休眠状态,直到最终获得锁为止。
  • 进程(process):应用软件或程序的运行时实例。
    进程有自己的虚拟内存空间和系统资源(包括端口权限) ,这些资源独立于分配给其他程序的资源。一个进程总是包含至少一个线程(主线程) ,并且可能包含任意数量的其他线程。
  • 信号量(semaphore):限制对共享资源访问的受保护变量。
    互斥(Mutexes)和条件(conditions)都是不同类型的信号量。
  • 任务(task),表示需要执行的工作量。
  • 线程(thread):进程中的执行流程。
    每个线程都有自己的堆栈空间,但在其他方面与同一进程中的其他线程共享内存。
  • 运行循环(run loop): 一个事件处理循环,
    接收事件并派发到适当的处理程序。

本文 demo 地址

参考文章

下节预告

原创声明
本站部分文章基于互联网的整理,我们会把真正“有用/优质”的文章整理提供给各位开发者。本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
本文链接:http://www.jiecseo.com/news/show_67964.html