美文网首页
IOS基础:多线程(上)

IOS基础:多线程(上)

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-25 16:32 被阅读0次

    原创:知识点总结性文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 简介
      • 1、CPU是什么
      • 2、操作系统中进程与线程的区别
      • 3、进程间通信
      • 4、线程间通信
      • 5、线程的状态与生命周期
      • 6、主线程
      • 7、线程池
      • 8、多线程同步
      • 9、选择 NSThread、 NSOperation 还是 GCD
    • 一、NSThread
      • 1、pThread的使用
      • 2、NSThread的属性和方法
      • 3、NSThread创建线程
      • 4、NSThread取消和结束线程
    • 二、GCD(Grand Central Dispatch)
      • 1、任务
      • 2、队列(Dispatch Queue)
      • 3、创建队列(串行队列/并发队列)
      • 4、任务执行方式(同步执行/异步执行)
      • 5、组合方式
      • 6、GCD 线程间通讯
      • 7、GCD 栅栏方法
      • 8、GCD 延时执行方法
      • 9、GCD 一次性代码(只执行一次)
      • 10、GCD 快速迭代方法
      • 11、GCD 的队列组
      • 12、GCD 信号量
      • 13、GCD中的定时器
      • 14、GCD底层实现原理
    • 三、NSOperation
      • 1、NSOperation简介
      • 2、NSOperation的创建
      • 3、创建队列 NSOperationQueue
      • 4、将操作添加到队列
      • 5、运用最大并发数实现串行
      • 6、取消或者暂停操作
      • 7、NSOperation 操作依赖
      • 8、NSOperation、NSOperationQueue 线程间的通信
    • 四、多线程与锁
      • 问题
      • 方案一:OSSpinLock自旋锁
      • 方案二:os_unfair_lock互斥锁
      • 方案三:pthread_mutex互斥锁
      • 方案四:pthread_mutex递归锁
      • 方案五:pthread_mutex条件锁
      • 方案六:NSLock锁
      • 方案七:NSRecursiveLock锁
      • 方案八:NSCondition条件锁
      • 方案九:NSConditionLock
      • 方案十:dispatch_semaphore信号量
      • 方案十一:@synchronized
      • 方案十二:atomic
      • 方案十三:pthread_rwlock读写锁
      • 方案十四:dispatch_barrier_async异步栅栏
      • 方案十五:dispatch_group_t调度组
    • Demo
    • 参考文献

    简介

    1、CPU是什么

    CPU主要由运算器、控制器、寄存器三部分组成,从字面意思看就是运算就是起着运算的作用,控制器就是负责发出CPU每条指令所需要的信息,寄存器就是保存运算或者指令的一些临时文件,这样可以保证更高的速度。

    CPU有着处理指令、执行操作、控制时间、处理数据四大作用,打个比喻来说,CPU就像我们的大脑,帮我们完成各种各样的生理活动。因此如果没有CPU,那么电脑就是一堆废物,无法工作。

    多核CPU与多个CPU多核。一个核心同时只能处理一个线程,单核CPU只能实现并发,而不是并行。

    打个比方,CPU好比是一个工厂,进程是一个车间,线程是车间里面的工人,车间的空间是工人们共享的。

    2、操作系统中进程与线程的区别

    • 进程:就是相互隔离的、独立运行的程序,一个进程就是一个执行中的程序,每个App启动就是一个进程,它可以包含多个线程工作。 进程是拥有资源的最小单位,每一个进程都有自己独立的一块内存空间,所以是系统进行资源分配和调度的基本单位。
    • 线程:是轻量级的进程,就像进程一样,线程在程序中是独立的、并发执行的,每个线程有它自己的局部变量。线程是执行的最小单位。
    • 多线程:在同一时刻,一个CPU只能处理1条线程,但CPU可以在多条线程之间快速的切换,只要切换的足够快,就造成了多线程一同执行的假象。同一个进程中的多个线程之间共享相同的内存地址空间,这意味着它们可以访问相同的变量和常量。
    • 主线程:每个程序都至少有一个线程,这个线程就是主线程 。 当一个程序启动时,主线程被创建,主线程控制程序的主要流程,负责显示和更新 Ul,所有 UI元素的更新必须在主线程中进行。不要把耗时操作放在主线程,会卡界面。

    线程就像火车的一节车厢,进程则是火车。车厢(线程)离开火车(进程)是无法跑动的,而火车(进程)至少有一节车厢(主线程)。多线程可以看做多个车厢,运用多线程的目的是将耗时的操作放在后台执行!。

    3、进程间通信

    iOS系统是相对封闭的系统,App各自在各自的沙盒(sandbox)中运行,每个App都只能读取iPhoneiOS系统为该应用程序程序创建的文件夹AppData下的内容,不能随意跨越自己的沙盒去访问别的App沙盒中的内容。所以iOS的系统中进行App间通信的方式也比较固定,常见的App间通信方式以及使用场景总结如下。

    a、Port (local socket)

    上层封装为NSMachPort:Foundation
    中层封装为CFMachPort:Core Foundation
    下层封装为Mach Ports:Mach内核层(线程、进程都可使用它进行通信)

    一个App1在本地的端口port1234进行TCPbindlisten,另外一个App2在同一个端口port1234发起TCP的connect连接,这样就可以建立正常的TCP连接,进行TCP通信了,那么就想传什么数据就可以传什么数据了。但是有一个限制,就是要求两个App进程都在活跃状态,而没有被后台杀死。尴尬的一点是iOS系统会给每个TCP在后台600秒的网络通信时间,600秒后APP会进入休眠状态。

    b、URL Scheme

    这个是iOS App通信最常用到的通信方式,App1通过openURL的方法跳转到App2,并且在URL中带上想要的参数,有点类似httpget请求那样进行参数传递。这种方式是使用最多的最常见的,使用方法也很简单只需要源App1info.plist中配置LSApplicationQueriesSchemes,指定目标App2scheme,然后在目标App2info.plist中配置好URL types,表示该App接受何种URL Scheme的唤起。

    典型的使用场景就是各开放平台SDK的分享功能,如分享到微信朋友圈微博等,或者是支付场景。比如从滴滴打车结束行程跳转到微信进行支付。

    c、Keychain

    iOS系统的Keychain是一个安全的存储容器,它本质上就是一个sqllite数据库,它的位置存储在/private/var/Keychains/keychain-2.db,不过它所保存的所有数据都是经过加密的,可以用来为不同的App保存敏感信息,比如用户名,密码等。iOS系统自己也用Keychain来保存VPN凭证和Wi-Fi密码。它是独立于每个App的沙盒之外的,所以即使App被删除之后,Keychain里面的信息依然存在。

    基于安全和独立于App沙盒的两个特性,Keychain主要用于给App保存登录和身份凭证等敏感信息,这样只要用户登录过,即使用户删除了App重新安装也不需要重新登录。

    Keychain用于App间通信的一个典型场景也和App的登录相关,就是统一账户登录平台。使用同一个账号平台的多个App,只要其中一个App用户进行了登录,其他app就可以实现自动登录不需要用户多次输入账号和密码。一般开放平台都会提供登录SDK,在这个SDK内部就可以把登录相关的信息都写到Keychain中,这样如果多个App都集成了这个SDK,那么就可以实现统一账户登录了。

    Keychain的使用比较简单,使用iOS系统提供的类KeychainItemWrapper,并通过Keychain access groups就可以在应用之间共享Keychain中的数据的数据了。

    d、UIPasteboard

    UIPasteboard是剪切板功能,因为iOS的原生控件UITextViewUITextFieldUIWebView,我们在使用时如果长按,就会出现复制、剪切、选中、全选、粘贴等功能,这个就是利用了系统剪切板功能来实现的。每一个App都可以去访问系统剪切板,所以就能够通过系统剪贴板进行App间的数据传输了。

    //创建系统剪贴板
    let pasteboard = UIPasteboard.general
    //往剪贴板写入淘口令
    pasteboard.string = "复制这条信息¥rkUy0Mz97CV¥后打开👉手淘👈"
    
    //淘宝从后台切到前台,读取淘口令进行展示
    let alert = UIAlertView.init(title: "淘口令", message: "发现一个宝贝,口令是rkUy0Mz97CV", delegate: self, cancelButtonTitle: "取消", otherButtonTitles: "查看")
    alert.show()
    

    UIPasteboard典型的使用场景就是淘宝跟微信/QQ的链接分享。由于腾讯和阿里的公司战略,腾讯在微信和QQ中都屏蔽了淘宝的链接。那如果淘宝用户想通过QQ或者微信跟好友分享某个淘宝商品,怎么办呢? 阿里的工程师就巧妙的利用剪贴板实现了这个功能。首先淘宝App中将链接自定义成淘口令,引导用户进行复制,并去QQ好友对话中粘贴。然后QQ好友收到消息后再打开自己的淘宝App,淘宝App每次从后台切到前台时,就会检查系统剪切板中是否有淘口令,如果有淘口令就进行解析并跳转到对于的商品页面。微信好友把淘口令复制到淘宝中,就可以打开好友分享的淘宝链接了。

    e、UIDocumentInteractionController

    UIDocumentInteractionController主要是用来实现同设备上App之间的共享文档,以及文档预览、打印、发邮件和复制等功能。

    首先通过调用它唯一的类方法interactionControllerWithURL:,并传入一个URL(NSURL),为你想要共享的文件来初始化一个实例对象,然后通过UIDocumentInteractionControllerDelegate显示菜单和预览窗口。

    let url = Bundle.main.url(forResource: "test", withExtension: "pdf")
    if url != nil {
        let documentInteractionController = UIDocumentInteractionController.init(url: url!)
        documentInteractionController.delegate = self
        documentInteractionController.presentOpenInMenu(from: self.view.bounds, in: self.view, animated: true)
    }
    
    e、AirDrop

    iOS并没有直接提供AirDrop的实现接口,但是可以使用UIActivityViewController的方法唤起AirDrop,进行数据交互。

    UIActivityViewController类是一个标准的ViewController,提供了几项标准的服务,比如复制项目至剪贴板,把内容分享至社交网站,以及通过Messages发送数据,也提供了内置的AirDrop功能。

    如果你有一些数据一批对象需要通过AirDrop进行分享,你所需要的是通过对象数组初始化UIActivityViewController,并展示在屏幕上:

    UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil]; 
    [self presentViewController:controller animated:YES completion:nil]; 
    
    f、App Groups

    App Group用于同一个开发团队开发的App之间,包括AppExtension之间共享同一份读写空间,进行数据共享。同一个团队开发的多个应用之间如果能直接数据共享,大大提高用户体验。

    4、线程间通信

    线程间通信的表现为:

    • 一个线程传递数据给另一个线程。
    • 在一个线程中执行完特定任务后,转到另一个线程继续执行任务。
    • 其他线程执行耗时任务,在主线程进行UI的刷新。
    a、NSThread线程间通信

    NSThread这套方案是经过苹果封装后,完全面向对象的,所以你可以直接操控线程对象,非常直观和方便。不过它的生命周期还是需要我们手动管理,所以实际上使用也比较少,使用频率较多的是GCD以及NSOperation

    当然,NSThread还可以用来做线程间通讯,比如下载图片并展示为例,将下载耗时操作放在子线程,下载完成后再切换回主线程在UI界面对图片进行展示。

    func onThread() {
        let urlStr = "http://haizeiwang.jpg"
        self.performSelector(inBackground: #selector(downloadImg(_:)), with: urlStr)
    }
    
    @objc func downloadImg(_ urlStr: String) {
        //打印当前线程
        print("下载图片线程", Thread.current)
        
        //获取图片链接
        guard let url = URL.init(string: urlStr) else {return}
        //下载图片二进制数据
        guard let data = try? Data.init(contentsOf: url) else {return}
        //设置图片
        guard let img = UIImage.init(data: data) else {return}
        
        //回到主线程刷新UI
        self.performSelector(onMainThread: #selector(downloadFinished(_:)), with: img, waitUntilDone: false)
    }
    
    @objc func downloadFinished(_ img: UIImage) {
        //打印当前线程
        print("刷新UI线程", Thread.current)
    
        // 赋值图片到imageview
        self.imageView.image = image
    }
    

    输出结果为:

    下载图片线程 <NSThread: 0x1c4464a00>{number = 5, name = (null)}
    刷新UI线程 <NSThread: 0x1c007a400>{number = 1, name = main}
    
    b、GCD线程间通信
    • GCD(Grand Central Dispatch)伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。
    • GCD会自动利用更多的CPU内核。
    • 会自动管理线程的生命周期(创建线程,调度任务,销毁线程等)。
    • 工程师只需要告诉GCD想要如何执行什么任务,不需要编写任何线程管理代码
    func onThread() {
        let urlStr = "http://haizeiwang.jpg"
    
        let dsp = DispatchQueue.init(label: "com.xjp.thread")
        dsp.async {
            self.downloadImg(urlStr)
        }
    }
    
    @objc func downloadImg(_ urlStr: String) {
        //打印当前线程
        print("下载图片线程", Thread.current)
        
        //获取图片链接
        guard let url = URL.init(string: urlStr) else {return}
        //下载图片二进制数据
        guard let data = try? Data.init(contentsOf: url) else {return}
        //设置图片
        guard let img = UIImage.init(data: data) else {return}
        
        //回到主线程刷新UI
        DispatchQueue.main.async {
            self.downloadFinished(img)
        }
    }
    
    @objc func downloadFinished(_ img: UIImage) {
        //打印当前线程
        print("刷新UI线程", Thread.current)
    }
    

    输出结果为:

    下载图片线程 <NSThread: 0x1c426b9c0>{number = 4, name = (null)}
    刷新UI线程 <NSThread: 0x1c0263140>{number = 1, name = main}
    
    c、NSOperation线程间通信
    • NSOperation是苹果推荐使用的并发技术,它提供了一些用GCD不是很好实现的功能。
    • NSOperation是一个抽象类,也就是说它并不能直接使用,而是应该使用它的子类。
    • Swift里面可以使用BlockOperation和自定义继承自Operation的子类。
    • NSOperation的使用常常是配合NSOperationQueue来进行的。只要是使用NSOperation的子类创建的实例就能添加到NSOperationQueue操作队列之中,一旦添加到队列,操作就会自动异步执行(注意是异步)。如果没有添加到队列,而是使用start方法,则会在当前线程执行。
    func onThread() {
        let urlStr = "http://haizeiwang.jpg"
    
        let que = OperationQueue.init()
        // OperationQueue的对象执行addOperation的方法,其实是生成了一个BlockOperation对象,异步执行当前任务
        que.addOperation {
            self.downloadImg(urlStr)
        }
    }
    
    @objc func downloadImg(_ urlStr: String) {
        //打印当前线程
        print("下载图片线程", Thread.current)
        
        //获取图片链接
        guard let url = URL.init(string: urlStr) else {return}
        //下载图片二进制数据
        guard let data = try? Data.init(contentsOf: url) else {return}
        //设置图片
        guard let img = UIImage.init(data: data) else {return}
        
        //回到主线程刷新UI
        OperationQueue.main.addOperation {
            self.downloadFinished(img)
        }
    }
    
    @objc func downloadFinished(_ img: UIImage) {
        //打印当前线程
        print("刷新UI线程", Thread.current)
    }
    

    输出结果为:

    下载图片线程 <NSThread: 0x1c4271d80>{number = 3, name = (null)}
    刷新UI线程 <NSThread: 0x1c006cac0>{number = 1, name = main}
    

    5、线程的状态与生命周期

    线程的状态与生命周期
    • 新建:实例化线程对象
    • 就绪:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。detach方法和performSelectorInBackground方法会直接实例化一个线程对象并加入“可调度线程池”。
    • 运行:CPU 负责调度可调度线程池中线程的执行。线程执行完成之前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,开发者不能干预。
    • 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self)(互斥锁)。
    • 死亡:正常死亡,线程执行完毕。非正常死亡,
      • 线程内死亡:[NSThread exit]强行中止后,后续代码都不会在执行
      • 线程外死亡:[thread cancel]通知线程对象取消,在线程执行方法中需要增加isCancelled判断,如果isCancelled == YES,直接返回。
      • 死亡后线程对象的isFinished属性为YES;如果是发送cancle消息,线程对象的isCancelled属性为YES;死亡后stackSize == 0,内存空间被释放。

    6、主线程

    a、为什么只在主线程刷新UI?
    • 在iOS中除了主线程,其他子线程是独立于Cocoa Touch的。
    • UIKit不是线程安全的,如果多线程操作UI会产生很多渲染顺序等不可控情况。
    b、为什么只有主线程的runloop是开启的?

    main函数中调用UIApplicationMain,主线程需要处理UI,为了让程序可以一直运行,所以开启一个常驻线程。

    c、主线程为什么会被阻塞?

    主线程所做的事情应该是响应用户输入、 事件处理、更新UI, 而耗时的任务不要在主线程中处理。 耗时任务使得主线程被阻塞了,不能响应用户的请求 , 这样应用的用户体验会很差。比如当我们点击Load Image按钮时,按钮会一直处于按下状态而不弹起, 直到图片显示 “完成",这是因为主线程要进行耗时的处理(如进行网络通信、数据传输等任务),导致主线程不能响应用户的输入和请求。

    7、线程池

    我们知道,创建线程的过程,需要用到物理内存,CPU 也会消耗时间。而且,新建一个线程,系统还需要为这个进程空间分配一定的内存作为线程堆栈。堆栈大小是 4KB 的倍数。在 iOS 开发中,主线程堆栈大小是 1MB,新创建的子线程堆栈大小是 512KB

    除了内存开销外,线程创建得多了,CPU 在切换线程上下文时,还会更新寄存器,更新寄存器的时候需要寻址,而寻址的过程还会有较大的 CPU 消耗。所以,线程过多时内存和 CPU 都会有大量的消耗,从而导致 App 整体性能降低,使得用户体验变成差。CPU 和内存的使用超出系统限制时,甚至会造成系统强杀。这种情况对用户和 App 的伤害就更大了。

    线程池是一种线程使用模式。 线程过多会带来调度开销,进而影响缓存局部性和整体性能。 线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。 这避免了在处理短时间任务时创建与销毁线程的代价。

    线程池的执行流程:

    1. 启动若干数量的线程,并让这些线程处于睡眠状态
    2. 当客户端有新的请求时,线程池会唤醒某一个睡眠线程,让它来处理客户端的请求
    3. 当请求处理完毕,线程又处于睡眠状态

    所以在并发的时候,同时能有多少线程在跑是由线程池的线程缓存数量决定的。

    a、GCD中的线程池

    GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用的话,这个线程就会被销毁。池是系统自动来维护,不需要我们手动来维护。那GCD底层线程池的缓存数到底有多少个?

    @IBAction func onThread() {
        let dsp = DispatchQueue.init(label: "com.xjp.thread", attributes: .concurrent)
        for i in 0..<10000 {
            dsp.async {
                //打印当前线程
                print("\(i)当前线程", Thread.current)
                //耗时任务
                Thread.sleep(forTimeInterval: 5)
            }
        }
    }
    

    这段代码是生成了一个并发队列,for循环10000次,执行异步任务,相当于会生成10000条线程,由于异步执行任务,会立即执行而且不会等待任务的结束,所以在生成线程的时候,线程打印就立即执行了。从打印的结果来看,一次打印总共有64行,从而可以得出GCD的线程池缓存数量是64条。

    b、NSOperation中的线程池

    NSOperationQueue提供了一套类似于线程池的机制,通过它可以更加方便的进行多线程的并发操作,构造一个线程池并添加任务对象到线程池中,线程池会分配线程,调用任务对象的main方法执行任务。

    // 分配maxConcurrentOperationCount为3个
    @IBAction func onThread() {
        let opq = OperationQueue.init()
        opq.maxConcurrentOperationCount = 3
        for i in 0..<10 {
            opq.addOperation({
                //打印当前线程
                print("\(i)当前线程", Thread.current)
                //耗时任务
                Thread.sleep(forTimeInterval: 5)
            })
        }
    }
    

    输出结果为:

    1当前线程 <NSThread: 0x1c0274a80>{number = 4, name = (null)}
    0当前线程 <NSThread: 0x1c4673900>{number = 6, name = (null)}
    2当前线程 <NSThread: 0x1c4674040>{number = 7, name = (null)}
    

    可以看到,规定线程池缓存为3个,一次就打印3个线程,当这3个线程回收到线程池里,又会再打印3个,当然如果其中一个线程先执行完,它就会先被回收。

    NSOperationQueue一次能够并发执行多少线程呢?

    @IBAction func onThread() {
        let opq = OperationQueue.init()
        opq.maxConcurrentOperationCount = 300
        for i in 0..<100 {
            opq.addOperation({
                //打印当前线程
                print("\(i)当前线程", Thread.current)
                //耗时任务
                Thread.sleep(forTimeInterval: 5)
            })
        }
    }
    

    结果看到也是64个,也就是NSOperationQueue多了可以操作线程数量的接口,但是最大的线程并发数量还是64个。

    8、多线程同步

    a、多线程同步是什么

    多线程同步可理解为线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

    线程同步其实是对于并发队列说的,串行队列的任务是依次执行的,本身就是同步的。

    多线程同步
    b、多线程同步用途

    结果传递的例子:小明、小李有三个西瓜,这个三个西瓜可以同时切开(并发),全部切完之后放到冰箱里冰好(同步),小明、小李吃冰西瓜(并发)。

    资源竞争:是指多个线程同时访问一个资源时可能存在竞争问题提供的解决方案,使多个线程可以对同一个资源进行操作。比如线程A为数组M添加了一个数据,线程B可以接收到添加数据后的数组M。线程同步就是线程之间相互的通信。例子,购买火车票,多个窗口卖票(并发),票卖出去之后要把库存减掉(同步),多个窗口出票成功(并发)。

    c、多线程同步实现

    多线程同步实现的方式有很多

    • 信号量(DispatchSemaphore)
    • 锁(NSLock)
    • @synchronized
    • dispatch_barrier_async
    • addDependency
    • pthread_mutex_t

    9、选择 NSThread、 NSOperation 还是 GCD

    • NSThread是传统的线程类, 需要自己管理线程的生命周期、线程同步、 加锁、、睡眠以及唤醒等。
    • NSOperationQueue 面向对象,支持KVO,可以监测operation是否正在执行isExecuted、是否结束isFinished、是否取消isCanceld。可以很方便的取消一个操作的执行。
    • GCD是面向底层的C语言的APINSOperationQueue是基于GCD面向OC的封装,所以 GCD更轻量级,执行效率更高。
    • GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
    a、pThread

    定义:一套通用的多线程API

    优势:适用于Unix / Linux / Windows等系统,可以跨平台。

    劣势:基于C语言,使用难度大。

    b、NSThread

    定义:这才是iOS真正线程级别的并发编程。每一个NSThread都是一个线程,其任务都是在子线程下执行的。NSThread主要通过target-selector模式,在启动的线程中,会执行targetselector,从而完成并发任务。也可以采用继承NSThread,重写main方法的模式来实现任务封装。

    劣势:编程起来比较复杂,开发者需要自己维护线程,增加开发难度。平时很少使用此API来实现多线程开发。

    c、GCD

    定义:全称是Grand Central Dispatch。这是在iOS编程中使用最多的并发编程方式,其核心思想就是将需要并发执行的任务封装到block里,通过把block分发到不同类型的队列里,对不同类型的队列,实现并发执行。GCD是一套C语言级别的并发编程方式,通过调用系统提供的C函数来实现。

    优势:GCD是一种非常简单的并发编程方式。把要执行的任务封装到block就行了,然后只需要调用一个C函数,就可以实现并发。大部分情况下,开发者不需要维护自己的队列,只需使用系统提供的队列即可,更加节省了开发时间,提高开发效率。同时,GCD不需要开发者直接跟线程操作接触,所有的并发线程由系统提供并维护,大大简化了多线程管理的工作。系统会根据资源的多少来决定并发线程的数量,不会出现线程数量膨胀过多的情况,使整个App运行效率更高。

    劣势:由于GCD是一种C级别的API,故无法运用面向对象的很多特性。GCD采用block封装执行任务,比较适合小型的任务并发执行,对于一些大型的任务,代码量过大,会导致难以维护的尴尬。GCD没有对任务执行状态监听的机制,一旦分发任务,只能等待任务执行完成。

    d、NSOperation

    定义NSOperation的核心是对GCD的一种面向对象封装,故其使用方法跟GCD有些类似。把需要并发执行的任务封装到NSOperation里,然后将NSOperation增加到NSOperationQueue里,来实现并发执行。

    优势:其是一种面向对象的并发编程方式,能够使用面向对象的特性。同时NSOperation有一系列的任务执行状态监听机制,能够很方便的管理任务执行。也不需要直接操作线程,简化开发工作。


    一、NSThread

    pthreadNSThread都是对内核mach kernelmach thread的封装,所以在开发时一般不会使用pthread。所以,每一个NSThread的对象其实就是一个线程,我们创建一个NSThread对象也就意味着我们创建了一个新的线程。

    1、pThread的使用

    a、Pthreads创建线程

    #include <pthread.h>
    
    - (void)onThread {
        // 1. 创建线程: 定义一个pthread_t类型变量
        pthread_t thread;
    
        // 2. 开启线程: 执行任务
        //第一个参数:线程对象,指向线程标识符的指针
        //第二个参数:线程属性,可赋值NULL
        //第三个参数:指向函数的执行,run对应函数里是需要在新线程中执行的任务
        //第四个参数:传递给该函数的参数,可赋值NULL
        NSString *name = @"xiejiapei";
        pthread_create(&thread, NULL, run, (__bridge void *)(name));
        
        // 3. 设置子线程的状态设置为detached,该线程运行结束后会自动释放所有资源
        pthread_detach(thread);
    }
    
    void * run(void *param) {
        NSLog(@"线程为:%@,参数为:%@", [NSThread currentThread], param);
    
        return NULL;
    }
    

    输出结果为:

    2020-08-21 11:09:47.325181+0800 Demo[38020:3942508] 线程为:<NSThread: 0x6000025d7b00>{number = 7, name = (null)},参数为:xiejiapei
    

    b、Pthreads其他相关方法

    pthread_create()// 创建一个线程
    pthread_exit()// 终止当前线程
    pthread_cancel()// 中断另外一个线程的运行
    pthread_join()// 阻塞当前的线程,直到另外一个线程运行结束
    pthread_attr_init()// 初始化线程的属性
    pthread_attr_setdetachstate()// 设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
    pthread_attr_getdetachstate()// 获取脱离状态的属性
    pthread_attr_destroy()// 删除线程的属性
    pthread_kill()// 向线程发送一个信号
    

    c、Pthreads常用函数与功能

    // 用于表示Thread ID,具体内容根据实现的不同而不同,有可能是一个Structure,因此不能将其看作为整数
    pthread_t
    
    // pthread_equal函数用于比较两个pthread_t是否相等
    int pthread_equal(pthread_t tid1, pthread_t tid2)
    
    // pthread_self函数用于获得本线程的thread id
    pthread _t pthread_self(void);
    

    2、NSThread的属性和方法

    属性

    //线程是否在执行
    thread.isExecuting;
    
    //线程是否被取消
    thread.isCancelled;
    
    //线程是否完成
    thread.isFinished;
    
    //是否是主线程
    thread.isMainThread;
    
    //线程的优先级,取值范围0.0到1.0,默认优先级0.5,1.0表示最高优先级,优先级高,CPU调度的频率高
    thread.threadPriority;
    

    方法

    //判断当前线程是否是多线程
    + (BOOL)isMultiThreaded;
    // 判断当前线程是否为主线程
    [NSThread isMainThread];
    // 获取当前线程,如果number=1,则表示在主线程,否则是子线程
     +(NSThread *)currentThread; 
    // 主线程的对象
    NSThread *thread = [NSThread mainThread];
    // 设置线程名
    [thread setName:@"xiejiapei"];
    // 设置优先级,优先级从0到1,1最高
    - (void)setThreadPriority:(double)p
    
    //线程休眠到指定时间
    + (void)sleepUntilDate:(NSDate *)date;
    // 线程休眠多久
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    
    //取消线程
    - (void)cancel;
    //启动线程
    - (void)start;
    //退出线程
    + (void)exit;
    
    //是否正在执行
    - (BOOL)isExecuting
    //是否执行完成
    - (BOOL)isFinished
    //是否取消线程
    - (BOOL)isCancelled
    
    //创建线程后自动启动线程
    + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
    //隐式创建并启动线程
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
    
    // 在主线程上执行操作
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
    
    // 在指定线程上执行操作
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
    
    // 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    
    //调用栈返回地址
    + (NSArray *)callStackReturnAddresses
    + (NSArray *)callStackSymbols
    

    3、NSThread创建线程

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        self.title = @"NSThread";
    
        /** 方法一,先创建线程对象,需要start */
        NSThread *threadOne = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:@"需要start"];
        // 线程加入线程池等待CPU调度,时间很快,几乎是立刻执行
        [threadOne start];
        
        /** 方法二,创建好之后自动启动 */
        [NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:@"创建好之后自动启动"];
        
        /** 方法三,隐式创建,直接启动 */
        [self performSelectorInBackground:@selector(doSomething:) withObject:@"隐式创建,直接启动"];
    }
    
    - (void)doSomething:(NSObject *)object
    {
        // 传递过来的参数
        NSLog(@"参数:%@, 线程:%@",object,[NSThread currentThread]);
    }
    

    输出结果为:

    2020-08-18 13:41:41.755963+0800 MultiThreadDemo[25569:2565353] 参数:需要start, 线程:<NSThread: 0x600003ede040>{number = 6, name = (null)}
    2020-08-18 13:41:41.755979+0800 MultiThreadDemo[25569:2565354] 参数:创建好之后自动启动, 线程:<NSThread: 0x600003ede080>{number = 7, name = (null)}
    2020-08-18 13:41:41.758075+0800 MultiThreadDemo[25569:2565355] 参数:隐式创建,直接启动, 线程:<NSThread: 0x600003ede0c0>{number = 8, name = (null)}
    

    4、NSThread取消和结束线程

    关于cancel的疑问,当使用cancel方法时,只是改变了线程的状态标识,并不能结束线程,所以我们要配合isCancelled方法进行使用。

    - (void)onThread
    {
        // 使用NSObject的方法隐式创建并自动启动
        [self performSelectorInBackground:@selector(testCancel) withObject:nil];
    }
    
    - (void)testCancel
    {
        NSLog(@"当前线程%@", [NSThread currentThread]);
        
        for (int i = 0 ; i < 100; i++)
        {
            if (i == 20)
            {
                //取消线程
                [[NSThread currentThread] cancel];
                NSLog(@"取消线程%@", [NSThread currentThread]);
            }
            
            if ([[NSThread currentThread] isCancelled])
            {
                NSLog(@"结束线程%@", [NSThread currentThread]);
                //结束线程
                // [NSThread exit];
                NSLog(@"这行代码不会打印的");
            }
            
        }
    }
    

    此时输出结果为:

    2020-08-21 13:22:53.215763+0800 Demo[38375:3996756] 当前线程<NSThread: 0x6000007269c0>{number = 5, name = (null)}
    2020-08-21 13:22:53.235906+0800 Demo[38375:3996756] 取消线程<NSThread: 0x6000007269c0>{number = 5, name = (null)}
    2020-08-21 13:22:53.240021+0800 Demo[38375:3996756] 结束线程<NSThread: 0x6000007269c0>{number = 5, name = (null)}
    2020-08-21 13:22:53.240506+0800 Demo[38375:3996756] 这行代码不会打印的
    2020-08-21 13:22:53.240846+0800 Demo[38375:3996756] 结束线程<NSThread: 0x6000007269c0>{number = 5, name = (null)}
    2020-08-21 13:22:53.241273+0800 Demo[38375:3996756] 这行代码不会打印的
    .......
    

    将注释掉的[NSThread exit];打开,则输出结果变成了:

    2020-08-21 13:22:53.215763+0800 Demo[38375:3996756] 当前线程<NSThread: 0x6000007269c0>{number = 5, name = (null)}
    2020-08-21 13:22:53.235906+0800 Demo[38375:3996756] 取消线程<NSThread: 0x6000007269c0>{number = 5, name = (null)}
    2020-08-21 13:22:53.240021+0800 Demo[38375:3996756] 结束线程<NSThread: 0x6000007269c0>{number = 5, name = (null)}
    

    二、GCD(Grand Central Dispatch)

    • 用于多核的并行运算。
    • 会自动利用更多的 CPU 内核(比如双核、四核)。
    • 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。

    GCD的使用:将任务(要在线程中执行的操作block)添加到队列(自己创建或使用全局并发队列),并且指定执行任务的方式(异步dispatch_async,同步dispatch_sync)。

    1、任务

    任务(block)

    • 任务就是将要在线程中执行的代码,GCD 将这段代码用block封装好,然后将这个任务添加到指定的执行方式(同步执行 sync 和异步执行 async),等待CPU从队列中取出任务放到对应的线程中执行。
    • 同步执行和异步执行的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

    同步执行(sync)

    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力。

    异步执行(async)

    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力,所以异步是多线程的代名词。
    • 虽然具有开启新线程的能力,但是并不一定开启新线程,这跟任务所指定的队列类型有关

    2、队列(Dispatch Queue)

    队列(Queue)

    • 这里的队列指执行任务的等待队列,即用来存放任务的队列。
    • 队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。
    • 每读取一个任务,则从队列中释放一个任务。
    • GCD中有两种队列:串行队列和并发队列。两者都符合FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

    串行队列(Serial Dispatch Queue)

    • 只开启一个线程,一个任务执行完毕后,再执行下一个任务。

    并发队列(Concurrent Dispatch Queue)

    • 可以开启多个线程,并且同时执行任务,实际上是CPU在多条线程之间快速的切换。
    • 需要注意的是并发队列的并发功能只有在异步(dispatch_async)函数下才有效。

    3、创建队列(串行队列/并发队列)

    使用dispatch_queue_create创建队列

    • 传入的第一个参数表示队列的唯一标识符,可为空,Dispatch Queue 的名称推荐使用应用程序ID这种逆序全程域名。
    • 传入的第二个参数用来识别是串行队列还是并发队列,DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。
    // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    // 并发队列
    dispatch_queue_t queue1 = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    

    主队列(Main Dispatch Queue)

    • GCD 提供了的一种特殊的串行队列——主队列。主队列负责在主线程上调度任务,如果在主线程上已经有任务正在执行,主队列会等到主线程空闲后再调度任务。
    • 可使用dispatch_get_main_queue()获得主队列。
    • 通常是返回主线程更新UI的时候使用。
    • 主队列:是系统默认为我们创建的DispatchQueue.main,它是一个串行队列
    • 主线程:在程序启动的时候,系统会自动启动,并会加载在RunLoop
    // 主队列的获取方法
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
          // 耗时操作放在这里
    
          dispatch_async(dispatch_get_main_queue(), ^{
              // 回到主线程进行UI操作
    
          });
      });
    

    全局并发队列(Global Dispatch Queue)

    • 就是一个并发队列,是为了让我们更方便的使用多线程。
    • 可以使用dispatch_get_global_queue来获取。需要两个参数。
    • 传入的第一个参数表示队列优先级,一般使用DISPATCH_QUEUE_PRIORITY_DEFAULT
    • 传入的第二个参数暂时没用,用0即可。
    //全局并发队列的优先级
    #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高优先级
    #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)优先级
    #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低优先级
    #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台优先级
    
    // 全局并发队列的获取方法
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //现在获取全局并发队列时,可以直接传0
    dispatch_get_global_queue(0, 0);
    

    4、任务执行方式(同步执行/异步执行)

    • 同步(sync): 使用dispatch_sync来表示,串行执行任务没有开启新线程。
    • 异步(async):使用dispatch_async来表示,并发执行任务有开启新线程(1条) 。
    // 同步执行任务
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        // 同步执行的放在这个block里
        NSLog(@"大家好,我是同步执行的任务");
    
    });
    
    // 异步执行任务
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 异步执行的任务放在这个block里
        NSLog(@"大家好,我是异步执行的任务");
    
    });
    

    5、组合方式

    四种不同的组合方式:GCD 有两种队列(串行队列/并发队列),两种任务执行方式(同步执行/异步执行)

    • 串行队列 + 同步执行 = 串行同步
    • 串行队列 + 异步执行 = 串行异步
    • 并发队列 + 同步执行 = 并发同步
    • 并发队列 + 异步执行 = 并发异步

    额外的两种组合方式:虽然全局并发队列可以作为普通并发队列来使用,但是主队列却需要单独列出

    • 主队列 + 同步执行 = 主队列同步
    • 主队列 + 异步执行 = 主队列异步
    \ 串行队列 并发队列 主队列
    同步执行 当前线程,一个接着一个地执行,顺序执行,一个任务执行完毕后,再执行下一个任务 当前线程,一个接着一个地执行,顺序执行,一个任务执行完毕后,再执行下一个任务 在主线程调用会死锁卡住不执行,在其他线程调用则没有开启新线程,串行执行任务
    异步执行 其他线程,一个接着一个地执行 多个任务,多个线程,多个任务并发执行 没有开启新线程,串行执行任务
    a、串行同步
    • 执行完一个任务,再执行下一个任务。
    • 不开启新线程。
    // 串行同步
    - (void)syncSerial
    {
        
        NSLog(@"\n\n**************串行同步***************\n\n");
        
        // 串行队列
        dispatch_queue_t queue = dispatch_queue_create("syncSerial", DISPATCH_QUEUE_SERIAL);
        
        // 同步执行
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"串行同步1   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"串行同步2   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"串行同步3   %@",[NSThread currentThread]);
            }
        });
    }
    

    输出结果为顺序执行,都在主线程:

    2020-08-18 14:41:19.200243+0800 MultiThreadDemo[25743:2596142] 串行同步1   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200325+0800 MultiThreadDemo[25743:2596142] 串行同步1   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200400+0800 MultiThreadDemo[25743:2596142] 串行同步1   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200476+0800 MultiThreadDemo[25743:2596142] 串行同步2   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200538+0800 MultiThreadDemo[25743:2596142] 串行同步2   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200603+0800 MultiThreadDemo[25743:2596142] 串行同步2   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200671+0800 MultiThreadDemo[25743:2596142] 串行同步3   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200742+0800 MultiThreadDemo[25743:2596142] 串行同步3   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    2020-08-18 14:41:19.200942+0800 MultiThreadDemo[25743:2596142] 串行同步3   <NSThread: 0x600003ca4d80>{number = 1, name = main}
    
    b、串行异步
    • 开启新线程
    • 因为任务是串行的,所以还是按顺序执行任务
    // 串行异步
    - (void)asyncSerial
    {
        
        NSLog(@"\n\n**************串行异步***************\n\n");
        
        // 串行队列
        dispatch_queue_t queue = dispatch_queue_create("asyncSerial", DISPATCH_QUEUE_SERIAL);
        
        // 同步执行
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"串行异步1   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"串行异步2   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"串行异步3   %@",[NSThread currentThread]);
            }
        });
    }
    

    输出结果为顺序执行,有不同线程:

    2020-08-18 14:44:15.473556+0800 MultiThreadDemo[25767:2598454] 串行异步1   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.473644+0800 MultiThreadDemo[25767:2598454] 串行异步1   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.473702+0800 MultiThreadDemo[25767:2598454] 串行异步1   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.473756+0800 MultiThreadDemo[25767:2598454] 串行异步2   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.473806+0800 MultiThreadDemo[25767:2598454] 串行异步2   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.473855+0800 MultiThreadDemo[25767:2598454] 串行异步2   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.473919+0800 MultiThreadDemo[25767:2598454] 串行异步3   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.474002+0800 MultiThreadDemo[25767:2598454] 串行异步3   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    2020-08-18 14:44:15.474077+0800 MultiThreadDemo[25767:2598454] 串行异步3   <NSThread: 0x6000019dbdc0>{number = 3, name = (null)}
    
    c、并发同步
    • 因为是同步的,所以执行完一个任务,再执行下一个任务。
    • 不会开启新线程。
    // 并发同步
    - (void)syncConcurrent
    {
        NSLog(@"\n\n**************并发同步***************\n\n");
        
        // 并发队列
        dispatch_queue_t queue = dispatch_queue_create("syncConcurrent", DISPATCH_QUEUE_CONCURRENT);
        
        // 同步执行
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"并发同步1   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"并发同步2   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"并发同步3   %@",[NSThread currentThread]);
            }
        });
    }
    

    输出结果为顺序执行,都在主线程:

    2020-08-18 14:50:19.425759+0800 MultiThreadDemo[25789:2601885] 并发同步1   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.425841+0800 MultiThreadDemo[25789:2601885] 并发同步1   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.425913+0800 MultiThreadDemo[25789:2601885] 并发同步1   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.425979+0800 MultiThreadDemo[25789:2601885] 并发同步2   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.426043+0800 MultiThreadDemo[25789:2601885] 并发同步2   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.426106+0800 MultiThreadDemo[25789:2601885] 并发同步2   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.426180+0800 MultiThreadDemo[25789:2601885] 并发同步3   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.426242+0800 MultiThreadDemo[25789:2601885] 并发同步3   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    2020-08-18 14:50:19.426314+0800 MultiThreadDemo[25789:2601885] 并发同步3   <NSThread: 0x600001d8cf40>{number = 1, name = main}
    
    d、并发异步
    • 任务交替执行。
    • 开启多线程。
    // 并发异步
    - (void)asyncConcurrent
    {
        NSLog(@"\n\n**************并发异步***************\n\n");
        
        // 并发队列
        dispatch_queue_t queue = dispatch_queue_create("asyncConcurrent", DISPATCH_QUEUE_CONCURRENT);
        
        // 同步执行
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"并发异步1   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"并发异步2   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"并发异步3   %@",[NSThread currentThread]);
            }
        });
    }
    

    输出结果为无序执行,有多条线程:

    2020-08-18 14:53:36.090342+0800 MultiThreadDemo[25808:2604377] 并发异步1   <NSThread: 0x6000004638c0>{number = 4, name = (null)}
    2020-08-18 14:53:36.090357+0800 MultiThreadDemo[25808:2604376] 并发异步2   <NSThread: 0x60000041e200>{number = 6, name = (null)}
    2020-08-18 14:53:36.090357+0800 MultiThreadDemo[25808:2604378] 并发异步3   <NSThread: 0x600000468900>{number = 5, name = (null)}
    2020-08-18 14:53:36.090429+0800 MultiThreadDemo[25808:2604377] 并发异步1   <NSThread: 0x6000004638c0>{number = 4, name = (null)}
    2020-08-18 14:53:36.090431+0800 MultiThreadDemo[25808:2604376] 并发异步2   <NSThread: 0x60000041e200>{number = 6, name = (null)}
    2020-08-18 14:53:36.090451+0800 MultiThreadDemo[25808:2604378] 并发异步3   <NSThread: 0x600000468900>{number = 5, name = (null)}
    2020-08-18 14:53:36.090500+0800 MultiThreadDemo[25808:2604376] 并发异步2   <NSThread: 0x60000041e200>{number = 6, name = (null)}
    2020-08-18 14:53:36.090503+0800 MultiThreadDemo[25808:2604377] 并发异步1   <NSThread: 0x6000004638c0>{number = 4, name = (null)}
    2020-08-18 14:53:36.090508+0800 MultiThreadDemo[25808:2604378] 并发异步3   <NSThread: 0x600000468900>{number = 5, name = (null)}
    
    e、主队列同步
    • 如果在主线程中运用这种方式,则会发生死锁,程序崩溃,而在其他线程中则不会。
    • 主队列同步造成死锁的原因:
    1. 如果在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。
    2. 同步对于任务是立刻执行的,那么当把第一个任务放进主队列时,它就会立马执行。
    3. 可是主线程现在正在处理syncMain方法,任务需要等syncMain执行完才能执行。
    4. syncMain执行到第一个任务的时候,又要等第一个任务执行完才能往下执行第二个和第三个任务。
    5. 这样syncMain方法和第一个任务就开始了互相等待,形成了死锁。
    // 主队列同步
    - (void)syncMain
    {
        NSLog(@"\n\n**************主队列同步,放到主线程会死锁,现在是在新开的一条线程上执行***************\n\n");
        
        // 主队列
        dispatch_queue_t queue = dispatch_get_main_queue();
        
        // 同步执行
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"主队列同步1   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"主队列同步2   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_sync(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"主队列同步3   %@",[NSThread currentThread]);
            }
        });
    }
    
    f、主队列异步
    • 在主线程中
    • 任务按顺序执行
    // 主队列异步
    - (void)asyncMain {
        
        NSLog(@"\n\n**************主队列异步***************\n\n");
        
        // 主队列
        dispatch_queue_t queue = dispatch_get_main_queue();
        
        // 异步执行
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"主队列异步1   %@",[NSThread currentThread]);
            }
        });
    
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"主队列异步2   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"主队列异步3   %@",[NSThread currentThread]);
            }
        });
    }
    

    输出结果为在主线程中按顺序执行:

    2020-08-18 15:03:39.371831+0800 MultiThreadDemo[25863:2610785] 主队列异步1   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.371925+0800 MultiThreadDemo[25863:2610785] 主队列异步1   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.371979+0800 MultiThreadDemo[25863:2610785] 主队列异步1   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.372056+0800 MultiThreadDemo[25863:2610785] 主队列异步2   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.372119+0800 MultiThreadDemo[25863:2610785] 主队列异步2   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.372188+0800 MultiThreadDemo[25863:2610785] 主队列异步2   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.372261+0800 MultiThreadDemo[25863:2610785] 主队列异步3   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.372332+0800 MultiThreadDemo[25863:2610785] 主队列异步3   <NSThread: 0x6000022e4240>{number = 1, name = main}
    2020-08-18 15:03:39.372397+0800 MultiThreadDemo[25863:2610785] 主队列异步3   <NSThread: 0x6000022e4240>{number = 1, name = main}
    

    6、GCD 线程间通讯

    在移动开发过程中,我们一般在主线程里边进行UI刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传、文件解压等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。下面的代码是在新开的线程中进行图片的下载,下载完成之后回到主线程显示图片。

    // 线程间通讯
    - (void)communicationOfThread {
        // 获取全局并发队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
        // 获取主队列
        dispatch_queue_t mainQueue = dispatch_get_main_queue(); 
    
        dispatch_async(queue, ^{
            // 异步追加任务
            for (int i = 0; i < 2; ++i) {
                [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    
                // 打印当前线程
                NSLog(@"queue-currentThread---%@",[NSThread currentThread]);
            }
    
            NSString *picURLStr = @"http://www.bangmangxuan.net/uploads/allimg/160320/74-160320130500.jpg";
            NSURL *picURL = [NSURL URLWithString:picURLStr];
            NSData *picData = [NSData dataWithContentsOfURL:picURL];
            UIImage *image = [UIImage imageWithData:picData];
    
            // 回到主线程
            dispatch_async(mainQueue, ^{
                // 追加在主线程中执行的任务
                [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    
                // 打印当前线程
                NSLog(@"mainQueue-currentThread---%@",[NSThread currentThread]);
    
                // 在主线程上添加图片
                self.imageView.image = image;
            });
        });
    }
    

    7、GCD 栅栏方法

    需求:要异步执行两组操作,且第一组操作执行完之后,才能开始执行第二组操作。

    这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。 dispatch_barrier_async函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。

    利用dispatch_barrier_async 可以解决多读单写问题:

    • 读者与读者并法
    • 读者与写者互斥
    • 写者与写者互斥
    // GCD栅栏
    - (void)GCDBarrier
    {
        // 并发队列
        dispatch_queue_t queue = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT);
        
        // 异步执行
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"栅栏:并发异步1   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"栅栏:并发异步2   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_barrier_async(queue, ^{
            NSLog(@"------------barrier------------%@", [NSThread currentThread]);
            NSLog(@"******* 并发异步执行,但是34一定在12后面 *********");
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"栅栏:并发异步3   %@",[NSThread currentThread]);
            }
        });
        
        dispatch_async(queue, ^{
            for (int i = 0; i < 3; I++)
            {
                NSLog(@"栅栏:并发异步4   %@",[NSThread currentThread]);
            }
        });
    }
    

    输出结果显示开启了多条线程,所有任务都是并发异步进行。但是第一组完成之后,才会进行第二组的操作。

    2020-08-18 15:33:40.095907+0800 MultiThreadDemo[25955:2627166] 栅栏:并发异步1   <NSThread: 0x60000261a9c0>{number = 5, name = (null)}
    2020-08-18 15:33:40.095912+0800 MultiThreadDemo[25955:2627169] 栅栏:并发异步2   <NSThread: 0x600002602200>{number = 6, name = (null)}
    2020-08-18 15:33:40.095998+0800 MultiThreadDemo[25955:2627166] 栅栏:并发异步1   <NSThread: 0x60000261a9c0>{number = 5, name = (null)}
    2020-08-18 15:33:40.096006+0800 MultiThreadDemo[25955:2627169] 栅栏:并发异步2   <NSThread: 0x600002602200>{number = 6, name = (null)}
    2020-08-18 15:33:40.096076+0800 MultiThreadDemo[25955:2627169] 栅栏:并发异步2   <NSThread: 0x600002602200>{number = 6, name = (null)}
    2020-08-18 15:33:40.096080+0800 MultiThreadDemo[25955:2627166] 栅栏:并发异步1   <NSThread: 0x60000261a9c0>{number = 5, name = (null)}
    
    2020-08-18 15:33:40.096162+0800 MultiThreadDemo[25955:2627166] ------------barrier------------<NSThread: 0x60000261a9c0>{number = 5, name = (null)}
    2020-08-18 15:33:40.096237+0800 MultiThreadDemo[25955:2627166] ******* 并发异步执行,但是34一定在12后面 *********
    
    2020-08-18 15:33:40.096607+0800 MultiThreadDemo[25955:2627166] 栅栏:并发异步3   <NSThread: 0x60000261a9c0>{number = 5, name = (null)}
    2020-08-18 15:33:40.096615+0800 MultiThreadDemo[25955:2627169] 栅栏:并发异步4   <NSThread: 0x600002602200>{number = 6, name = (null)}
    2020-08-18 15:33:40.096820+0800 MultiThreadDemo[25955:2627166] 栅栏:并发异步3   <NSThread: 0x60000261a9c0>{number = 5, name = (null)}
    2020-08-18 15:33:40.097033+0800 MultiThreadDemo[25955:2627169] 栅栏:并发异步4   <NSThread: 0x600002602200>{number = 6, name = (null)}
    2020-08-18 15:33:40.097135+0800 MultiThreadDemo[25955:2627166] 栅栏:并发异步3   <NSThread: 0x60000261a9c0>{number = 5, name = (null)}
    2020-08-18 15:33:40.097244+0800 MultiThreadDemo[25955:2627169] 栅栏:并发异步4   <NSThread: 0x600002602200>{number = 6, name = (null)}
    

    8、GCD 延时执行方法

    需求:在指定时间(例如3秒)之后执行某个任务。

    可以用 GCDdispatch_after函数来实现。 需要注意的是:dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的。

    // 延时执行方法 dispatch_after
    - (void)after {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 3.0秒后异步追加任务代码到主队列,并开始执行
            // 打印当前线程
            NSLog(@"after---%@",[NSThread currentThread]);
        });
    }
    

    常见延迟执行的方法有:

    // 延迟执行的方法
    - (void)delayMethod
    {
        // 法一:performSelector方法, 要求必须在主线程中执行
        [self performSelector:@selector(compare:) withObject:nil afterDelay:1.0f];
        
        // 法二:定时器:NSTimer, 必须在主线程中执行
        [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(compare:) userInfo:nil repeats:NO];
        
        // 法三:sleep方式
        [NSThread sleepForTimeInterval:1.0f];
        
        // 法四:GCD方式
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        });
    }
    

    9、GCD 一次性代码(只执行一次)

    a、使用方法

    我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了GCDdispatch_once函数。使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

    // 只执行一次 dispatch_once
    - (void)once {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            // 只执行1次的代码(这里面默认是线程安全的)
        });
    }
    
    b、实现原理
    实现原理
    void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){
        // val == 0
        volatile long *vval = val;
        // 第一个线程进来
        // dispatch_atomic_cmpxchg(p,l,n) 方法的功能是原子操作
        // 方法含义为如果p==l 则 将n赋值给p, 并且返回true
        // 如果p != l 则返回 false
        // 所以第一次进来 val == 0 , val被赋值为1 , 并且返回ture
        if (dispatch_atomic_cmpxchg(val, 0, 1)) {
            func(ctxt); // block真正执行
            // dispatch_atomic_barrier 是一个编译器操作,意思为前后指令的顺序不能颠倒.这里是防止编译器优化将原本的指令语义破坏
            dispatch_atomic_barrier();
            // 将vval赋值为非零
            *val = ~0;
        } 
        else 
        {
            // 如果在第一个线程进来后执行上边代码块的同时,有其他的线程进来执行
            // 则进入空循环,等待vval被赋值为非零.
            do
            {
                 // 这有助于提高性能和节省CPU耗电,延迟空等
                _dispatch_hardware_pause();
            } while (*vval != ~0);
            dispatch_atomic_barrier();
        }
    }
    

    首次调用dispatch_once时,因为外部传入的dispatch_once_t变量值为nil,故vval会为NULL,故if判断成立。然后调用_dispatch_client_callout执行block,然后在block执行完成之后将vval的值更新成DISPATCH_ONCE_DONE表示任务已完成。最后遍历链表的节点并调用_dispatch_thread_semaphore_signal来唤醒等待中的信号量;

    当其他线程同时也调用dispatch_once时,因为if判断是原子性操作,故只有一个线程进入到if分支中,其他线程会进入else分支。在else分支中会判断block是否已完成,如果已完成则跳出循环;否则就是更新链表并调用_dispatch_thread_semaphore_wait阻塞线程,等待if分支中的block完成后再唤醒当前等待的线程。

    10、GCD 快速迭代方法

    • 通常我们会用for循环遍历,但是GCD给我们提供了快速迭代的函数dispatch_apply
    • dispatch_apply函数是dispatch_sync函数和dispatch_group_async函数的组合API
    • dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
    • 我们可以利用异步队列同时遍历,比如说遍历 0~5 这6个数字,for循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以同时遍历多个数字。
    // 快速迭代方法
    - (void)applyGCD
    {
        NSLog(@"\n\n************** GCD快速迭代 ***************\n\n");
        
        // 并发队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        // 同时遍历多个数字,并等到全部的处理执行结束
        // 指定重复次数,追加对象的Dispatch Queue,Block中index参数的作用是为了按执行的顺序区分各个Block
        dispatch_apply(6, queue, ^(size_t index) {
            // 迭代任务
            NSLog(@"GCD快速迭代 index %zd,currentThread:%@",index, [NSThread currentThread]);
        });
    
        NSLog(@"等到全部的处理执行结束");
    }
    

    输出结果为:

    2020-08-18 15:55:43.390620+0800 MultiThreadDemo[26031:2638500] GCD快速迭代 index 0,currentThread:<NSThread: 0x60000043cf00>{number = 1, name = main}
    2020-08-18 15:55:43.390700+0800 MultiThreadDemo[26031:2638500] GCD快速迭代 index 1,currentThread:<NSThread: 0x60000043cf00>{number = 1, name = main}
    2020-08-18 15:55:43.390774+0800 MultiThreadDemo[26031:2638500] GCD快速迭代 index 2,currentThread:<NSThread: 0x60000043cf00>{number = 1, name = main}
    2020-08-18 15:55:43.390788+0800 MultiThreadDemo[26031:2638692] GCD快速迭代 index 3,currentThread:<NSThread: 0x6000004637c0>{number = 5, name = (null)}
    2020-08-18 15:55:43.390807+0800 MultiThreadDemo[26031:2638691] GCD快速迭代 index 4,currentThread:<NSThread: 0x600000436140>{number = 4, name = (null)}
    2020-08-18 15:55:43.390848+0800 MultiThreadDemo[26031:2638500] GCD快速迭代 index 5,currentThread:<NSThread: 0x60000043cf00>{number = 1, name = main}
    2020-08-18 15:55:43.390900+0800 MultiThreadDemo[26031:2638500] 等到全部的处理执行结束
    

    11、GCD 的队列组

    需求:分别异步执行几个耗时任务,然后当几个耗时任务都执行完毕后再回到主线程执行任务,这时候我们可以用到 GCD 的队列组。比如同时上传 10 张图片,全部上传完成后通知用户。

    使用方式:

    • GCDdispatch_group_t 功能可以将多个任务分组,等待分组里面的所有任务执行完成之后,GCDdispatch_group_notify 方法可以通知。
    • 调用队列组的dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enterdispatch_group_leave 组合来实现 dispatch_group_async
    • 调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。
    // 队列组
    - (void)groupNotify
    {
        NSLog(@"group---begin");
        
        dispatch_group_t group =  dispatch_group_create();
        
        // 纳入队列组的监听范围
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            NSLog(@"队列组:有一个耗时操作任务1完成!%@ ",[NSThread currentThread]);
        });
        
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            NSLog(@"队列组:有一个耗时操作任务2完成!%@ ",[NSThread currentThread]);
        });
        
        dispatch_group_enter(group);
    
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_async(queue, ^{
            NSLog(@"队列组:有一个耗时操作任务3完成!%@ ",[NSThread currentThread]);
            
            dispatch_group_leave(group);
        });
        
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"队列组:有一个耗时操作任务4完成!%@ ",[NSThread currentThread]);
            
            dispatch_group_leave(group);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"队列组:有一个耗时操作任务5完成!%@ ",[NSThread currentThread]);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"队列组:有一个耗时操作任务6完成!%@ ",[NSThread currentThread]);
        });
        
        // 所有的任务会并发的执行(不按序)
        for (int i = 0; i < 10 ; i++) {
            dispatch_group_async(group, queue, ^{
                NSLog(@"执行一次任务 %@ ",[NSThread currentThread]);
            });
        }
        
        // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"group---end");
        
        // 监听上面的任务是否完成,如果完成, 就会调用这个方法
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"队列组:前面的耗时操作都完成了,回到主线程更新UI");
        });
        
    }
    

    输出结果显示将所有的任务都加入 group ,等待所有的任务执行完成后,dispatch_group_notify 会被调用。

    2020-08-18 16:18:36.141054+0800 MultiThreadDemo[26266:2656298] group---begin
    2020-08-18 16:18:36.141211+0800 MultiThreadDemo[26266:2656460] 队列组:有一个耗时操作任务1完成!<NSThread: 0x6000004d8040>{number = 4, name = (null)} 
    2020-08-18 16:18:36.141213+0800 MultiThreadDemo[26266:2656459] 队列组:有一个耗时操作任务2完成!<NSThread: 0x600000484940>{number = 5, name = (null)} 
    2020-08-18 16:18:36.141241+0800 MultiThreadDemo[26266:2656465] 队列组:有一个耗时操作任务3完成!<NSThread: 0x600000497dc0>{number = 6, name = (null)} 
    2020-08-18 16:18:36.141251+0800 MultiThreadDemo[26266:2656461] 队列组:有一个耗时操作任务4完成!<NSThread: 0x6000004c00c0>{number = 7, name = (null)} 
    2020-08-18 16:18:36.141262+0800 MultiThreadDemo[26266:2656458] 队列组:有一个耗时操作任务5完成!<NSThread: 0x6000004c40c0>{number = 8, name = (null)} 
    2020-08-18 16:18:36.141274+0800 MultiThreadDemo[26266:2656463] 队列组:有一个耗时操作任务6完成!<NSThread: 0x6000004d8080>{number = 3, name = (null)} 
    
    2020-08-18 16:18:36.142020+0800 MultiThreadDemo[26266:2656458] 执行一次任务 <NSThread: 0x6000004c40c0>{number = 8, name = (null)} 
    2020-08-18 16:18:36.142020+0800 MultiThreadDemo[26266:2656460] 执行一次任务 <NSThread: 0x6000004d8040>{number = 4, name = (null)} 
    2020-08-18 16:18:36.141354+0800 MultiThreadDemo[26266:2656461] 执行一次任务 <NSThread: 0x6000004c00c0>{number = 7, name = (null)} 
    2020-08-18 16:18:36.141318+0800 MultiThreadDemo[26266:2656459] 执行一次任务 <NSThread: 0x600000484940>{number = 5, name = (null)} 
    2020-08-18 16:18:36.141331+0800 MultiThreadDemo[26266:2656465] 执行一次任务 <NSThread: 0x600000497dc0>{number = 6, name = (null)} 
    2020-08-18 16:18:36.141358+0800 MultiThreadDemo[26266:2656463] 执行一次任务 <NSThread: 0x6000004d8080>{number = 3, name = (null)} 
    2020-08-18 16:18:36.141516+0800 MultiThreadDemo[26266:2656469] 执行一次任务 <NSThread: 0x6000004aa980>{number = 9, name = (null)} 
    2020-08-18 16:18:36.141539+0800 MultiThreadDemo[26266:2656458] 执行一次任务 <NSThread: 0x6000004c40c0>{number = 8, name = (null)} 
    2020-08-18 16:18:36.141541+0800 MultiThreadDemo[26266:2656470] 执行一次任务 <NSThread: 0x6000004cedc0>{number = 10, name = (null)} 
    2020-08-18 16:18:36.141556+0800 MultiThreadDemo[26266:2656471] 执行一次任务 <NSThread: 0x6000004aaa00>{number = 11, name = (null)} 
    2020-08-18 16:18:36.152545+0800 MultiThreadDemo[26266:2656298] group---end
    2020-08-18 16:18:36.154602+0800 MultiThreadDemo[26266:2656298] 队列组:前面的耗时操作都完成了,回到主线程更新UI
    

    12、GCD 信号量

    GCD 中的信号量是指Dispatch Semaphore,是持有计数的信号。 GCD 不像 NSOperation 那样有直接提供线程数量控制方法,但是通过 GCD 的semaphore功能一样可以达到控制线程数量的效果。

    Dispatch Semaphore提供了三个函数:

    • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
    • dispatch_semaphore_signal:发送一个信号,让信号总量加1
    • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。
    // 控制线程数量
    - (void)runMaxThreadCountWithGCD
    {
        dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentRunMaxThreadCountWithGCD", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t serialQueue = dispatch_queue_create("serialRunMaxThreadCountWithGCD", DISPATCH_QUEUE_SERIAL);
        
        // 创建一个semaphore,并设置最大信号量,最大信号量表示最大线程数量
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
        
        // 使用循环 往串行队列 serialQueue 增加 10 个任务
        for (int i = 0; i < 10 ; I++)
        {
            dispatch_async(serialQueue, ^{
                // 只有当信号量大于 0 的时候,线程将信号量减 1,程序继续执行
                // 否则线程会阻塞并且一直等待,直到信号量大于 0
                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
                
                dispatch_async(concurrentQueue, ^{
                    NSLog(@"%@ 并发队列执行任务一次  i = %d",[NSThread currentThread],i);
                    // 当线程任务执行完成之后,发送一个信号,增加信号量
                    dispatch_semaphore_signal(semaphore);
                });
            });
        }
        NSLog(@"%@ 执行任务结束",[NSThread currentThread]);
    }
    

    输出结果显示只有 number 3 和 number 4 这 2 个线程在执行:

    2020-08-18 16:44:41.615615+0800 MultiThreadDemo[26388:2670096] <NSThread: 0x6000032090c0>{number = 1, name = main} 执行任务结束
    
    2020-08-18 16:44:41.615620+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 0
    2020-08-18 16:44:41.615725+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 1
    2020-08-18 16:44:41.615798+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 2
    2020-08-18 16:44:41.615861+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 3
    2020-08-18 16:44:41.615919+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 4
    2020-08-18 16:44:41.615993+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 5
    2020-08-18 16:44:41.616065+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 6
    2020-08-18 16:44:41.616150+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 7
    2020-08-18 16:44:41.616248+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 8
    2020-08-18 16:44:41.616479+0800 MultiThreadDemo[26388:2670278] <NSThread: 0x6000032400c0>{number = 5, name = (null)} 执行任务一次  i = 9
    

    利用 GCDdispatch_group_tsemaphore 功能,我们可以做到控制线程数量,并且在所有任务执行完成之后得到通知。

    // 任务分组 + 线程数量控制
    - (void)runMaxCountInGroupWithGCD
    {
        dispatch_queue_t concurrentQueue = dispatch_queue_create("runGroupWithGCD", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t group =  dispatch_group_create();
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
        
        for (int i = 0; i < 10 ; I++)
        {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
            dispatch_group_async(group, concurrentQueue, ^{
                NSLog(@"%@ 执行任务一次",[NSThread currentThread]);
                dispatch_semaphore_signal(semaphore);
            });
        }
         
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"%@ 执行任务结束",[NSThread currentThread]);
        });
    }
    

    输出结果显示“执行任务结束”语句现在最后打印,说明在所有任务执行完成之后得到通知。

    2020-08-18 16:49:44.168396+0800 MultiThreadDemo[26424:2673958] <NSThread: 0x600001d14fc0>{number = 6, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168396+0800 MultiThreadDemo[26424:2673957] <NSThread: 0x600001d115c0>{number = 4, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168522+0800 MultiThreadDemo[26424:2673958] <NSThread: 0x600001d14fc0>{number = 6, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168522+0800 MultiThreadDemo[26424:2673957] <NSThread: 0x600001d115c0>{number = 4, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168636+0800 MultiThreadDemo[26424:2673957] <NSThread: 0x600001d115c0>{number = 4, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168642+0800 MultiThreadDemo[26424:2673958] <NSThread: 0x600001d14fc0>{number = 6, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168730+0800 MultiThreadDemo[26424:2673957] <NSThread: 0x600001d115c0>{number = 4, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168740+0800 MultiThreadDemo[26424:2673958] <NSThread: 0x600001d14fc0>{number = 6, name = (null)} 执行任务一次
    2020-08-18 16:49:44.168808+0800 MultiThreadDemo[26424:2673957] <NSThread: 0x600001d115c0>{number = 4, name = (null)} 执行任务一次
    2020-08-18 16:49:44.169035+0800 MultiThreadDemo[26424:2673958] <NSThread: 0x600001d14fc0>{number = 6, name = (null)} 执行任务一次
    
    2020-08-18 16:49:44.170975+0800 MultiThreadDemo[26424:2673759] <NSThread: 0x600001d550c0>{number = 1, name = main} 执行任务结束
    

    13、GCD中的定时器

    @property(nonatomic ,strong) dispatch_source_t timer;
    
    - (void)testGCDTimer
    {
        //0.创建一个队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        //1.创建一个GCD的定时器
        /*
         第一个参数:说明这是一个定时器
         第四个参数:GCD的回调任务添加到那个队列中执行,如果是主队列则在主线程执行
         */
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
        //2.设置定时器的开始时间,间隔时间以及精准度
    
        //设置开始时间,三秒钟之后调用。DISPATCH_TIME_NOW表示从当前开始
        dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
        //设置定时器工作的间隔时间
        uint64_t intevel = 1.0 * NSEC_PER_SEC;
    
        /*
         第一个参数:要给哪个定时器设置
         第二个参数:定时器的开始时间
         第三个参数:定时器调用方法的间隔时间
         第四个参数:定时器的精准度,如果传0则表示采用最精准的方式计算,如果传大于0的数值,则表示该定时切换i可以接收该值范围内的误差,通常传0
         该参数的意义:可以适当的提高程序的性能
         注意点:GCD定时器中的时间以纳秒为单位(面试)
         */
        dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);
    
        //3.设置定时器开启后回调的方法
        /*
         第一个参数:要给哪个定时器设置
         第二个参数:回调block
         */
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"定时器开启后每次回调的方法:打印当前线程------%@",[NSThread currentThread]);
        });
    
        //4.执行定时器
        dispatch_resume(timer);
    
        //注意:dispatch_source_t本质上是OC类,在这里是个局部变量,需要强引用
        self.timer = timer;
    }
    

    输出结果为:

    2020-08-19 14:00:08.072402+0800 MultiThreadDemo[29692:3059594] 定时器开启后每次回调的方法:打印当前线程------<NSThread: 0x6000019f5b80>{number = 4, name = (null)}
    2020-08-19 14:00:09.072310+0800 MultiThreadDemo[29692:3059592] 定时器开启后每次回调的方法:打印当前线程------<NSThread: 0x6000019f01c0>{number = 5, name = (null)}
    2020-08-19 14:00:10.071636+0800 MultiThreadDemo[29692:3059592] 定时器开启后每次回调的方法:打印当前线程------<NSThread: 0x6000019f01c0>{number = 5, name = (null)}
    2020-08-19 14:00:11.071227+0800 MultiThreadDemo[29692:3059592] 定时器开启后每次回调的方法:打印当前线程------<NSThread: 0x6000019f01c0>{number = 5, name = (null)}
    2020-08-19 14:00:18.071494+0800 MultiThreadDemo[29692:3059599] 定时器开启后每次回调的方法:打印当前线程------<NSThread: 0x6000019f22c0>{number = 3, name = (null)}
    

    14、GCD底层实现原理

    我们使用的GCDAPI是C语言函数,全部包含在LIBdispatch库中,DispatchQueue通过结构体和链表被实现为FIFO的队列;而FIFO的队列是由dispatch_async等函数追加的Block来管理的;Block不是直接加入FIFO队列,而是先加入Dispatch Continuation结构体,然后在加入FIFO队列,Dispatch Continuation用于记忆Block所属的Dispatch Group和其他一些信息(相当于上下文)。

    Dispatch Queue可通过dispatch_set_target_queue()设定,可以设定执行该Dispatch Queue处理的Dispatch Queue为目标。该目标可像串珠子一样,设定多个连接在一起的Dispatch Queue,但是在连接串的最后必须设定Main Dispatch Queue,或各种优先级的Global Dispatch Queue,或是准备用于Serial Dispatch QueueGlobal Dispatch Queue

    Global Dispatch Queue的8种优先级:

    .High priority
    .Default Priority
    .Low Priority
    .Background Priority
    .High Overcommit Priority
    .Default Overcommit Priority
    .Low Overcommit Priority
    .Background Overcommit Priority
    

    附有OvercommitGlobal Dispatch Queue使用在Serial Dispatch Queue中,不管系统状态如何,都会强制生成线程的 Dispatch Queue。 这8种Global Dispatch Queue各使用1个pthread_workqueue

    a、GCD初始化

    GCD初始化时,使用pthread_workqueue_create_np函数生成pthread_workqueuepthread_workqueue包含在Libc提供的pthreads的API中,他使用bsthread_registerworkq_open系统调用,在初始化XNU内核的workqueue之后获取workqueue信息。其中XNU有四种workqueue

    WORKQUEUE_HIGH_PRIOQUEUE
    WORKQUEUE_DEFAULT_PRIOQUEUE
    WORKQUEUE_LOW_PRIOQUEUE
    WORKQUEUE_BG_PRIOQUEUE
    

    这四种workqueueGlobal Dispatch Queue的执行优先级相同。

    b、Dispatch Queue执行block的过程
    1. 当在Global Dispatch Queue中执行Block时,libdispatchGlobal Dispatch Queue自身的FIFO中取出Dispatch Continuation,调用pthread_workqueue_additem_np函数,将该Global Dispatch Queue符合其优先级的workqueue信息以及执行Dispatch Continuation的回调函数等传递给pthread_workqueue_additem_np函数的参数。
    2. thread_workqueue_additem_np()使用workq_kernreturn系统调用,通知workqueue增加应当执行的项目。
    3. 根据该通知,XUN内核基于系统状态判断是否要生成线程,如果是Overcommit优先级的Global Dispatch Queueworkqueue则始终生成线程。
    4. workqueue的线程执行pthread_workqueue(),该函数用libdispatch的回调函数,在回调函数中执行执行加入到Dispatch ContinuatinBlock
    5. Block执行结束后,进行通知Dispatch Group结束,释放Dispatch Continuation等处理,开始准备执行加入到Dispatch Continuation中的下一个Block

    三、NSOperation

    续文见下篇 IOS基础:多线程(下)


    Demo在我的Github上,欢迎下载。
    MultiThreadDemo

    参考文章

    iOS 多线程:『GCD』详尽总结
    iOS 多线程:『NSOperation、NSOperationQueue』详尽总结
    GCD源码分析
    iOS中使用到的加锁方案
    关于 @synchronized,这儿比你想知道的还要多
    iOS多线程详解:概念篇

    相关文章

      网友评论

          本文标题:IOS基础:多线程(上)

          本文链接:https://www.haomeiwen.com/subject/dydnmktx.html