美文网首页
iOS - 多线程

iOS - 多线程

作者: 冰风v落叶 | 来源:发表于2018-08-21 17:30 被阅读139次

    前言

    iOS的多线程有四种 : pthread , GCD , NSTread , NSOperation , 前两种C语言编写 , 后两种OC语言编写 , NSThread是对pthread的封装 , NSOperation是对GCD的封装.

    日常工作中我们最常用的是GCD , GCD会自动分配CPU资源 , 会自动管理线程的生命周期 , 使用起来非常方便 , 本文只讲述GCD

    一 . 首先我们要明确几个概念:

    1.进程和线程

    • 进程 : 在系统中独立运行并进行资源分配的基本单位 , 一个App就是一个进程

    • 线程 : 执行任务的基本单元 , 一个App可以有多个线程 (注意 : 一个CPU核心同一时间内只能执行一条线程 , 即单核CPU同一时间只能处理一条线程 , 多核CPU同一时间可以处理多条线程)
      线程创建好后 , 会放入可调度线程池中 , 等待CPU调度
      线程的几种状态 : 新建 , 就绪 , 运行 , 死亡 , 阻塞

    • 多线程的作用 , 我们为什么要学习多线程 :
      (1). 提高程序执行效率 , 更好的利用多核CPU资源 , 多条线程可以同时利用多个CPU核心
      (2). 防止阻塞主线程
      (3). 在GCD中 , 线程执行完毕后会自动销毁这个线程

    • 多线程的缺点 , 我们使用多线程时要注意的地方 :
      (1). 线程太多会占用大量内存 , iOS系统中, 子线程一个线程会占用512KB的空间 , 主线程会占用1M的空间 , 开辟一个线程大约消耗90毫秒
      (2). 线程太多会导致CPU调度线程开销过大 , 降低程序运行效率 (例如 : 单核CPU执行并发任务时 , CPU会对线程执行一个时间片规定的时间 , 然后快速切换到下一个线程 , 由于时间片非常短 , 所以看起来是同时执行的 , 但是实际上是伪同时 , 线程太多会让CPU疲于调度 . CPU切换线程时 , 线程会在就绪状态和运行状态之间切换)
      (3). 可能会造成死锁 (可以理解为互相等待)
      (4). 数据竞争 (多个线程操作同一个数据,导致数据紊乱)
      这些缺点的解决办法 , 我们会在下文中讲到

    2.队列和执行方式

    (1). 两个常用概念

    • 队列 : 存放任务的队形结构 , 系统以先进先出的方式(FIFO)调度队列中的任务
    • 任务 : 任务就是在线程中要执行的代码

    GCD的核心 : 将任务放到队列中 , 并指定执行方式 , CPU会自动从队列中取出任务放到对应的线程中执行

    (2). 两种通用队列

    • 串行队列 : 在一条线程中 , 按顺序执行任务的队列
    • 并发队列 : 可以在多个线程中 , 同时执行任务的队列 (其实会根据flag进行分组 , 高flag组的任务先同时执行 , 低flag组等待高flag组的任务执行完毕后 , 低flag组的任务才会同时执行)

    不管是串行队列还是并发队列 , 都是按照FIFO的原则, 依次将任务分配线程
    串行队列原理 : CPU会从串行队列中取出一个任务 , 然后分配一个线程去执行 , 然后等待任务执行完毕后 , CPU继续取出任务 , 分配线程执行 (等待完成)
    并发队列原理 : CPU会从并行队列中取出一个任务 , 然后分配一个线程去执行 , 不会等待任务执行完毕 , 就会CPU就会又去取出任务 , 然后分配新的线程去执行 (等待发生)

    (3). 两种执行方式

    • 同步执行(sync) : 在当前线程执行任务 , 不会开辟新的线程 , 会阻塞当前线程 , 直到此任务执行完毕
    • 异步执行(async) : 开辟新线程执行任务

    同步会阻塞当前线程,直到此任务执行完毕 ; 异步不会阻塞线程

    (4). 两种特殊队列

    • 主队列 : 系统为我们创建好的一个串行队列 , 它用来管理主线程的任务
    • 全局队列 : 系统为我们创建好的并发队列 , 不受线程阻断barrier的影响

    (5). 四种组合方式

    • 串行队列同步执行 : 会阻塞当前线程 , 让当前线程优先执行我这个串行队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .
    • 串行队列异步执行 : 开辟1个新线程 , 在这个新线程中顺序执行任务
    • 并发队列同步执行 : 会阻塞当前线程 , 让当前线程优先执行我这个并发队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .
    • 并发队列异步执行 : 开辟多个新线程 , 在不同线程中同时执行不同任务

    在主线程中使用主队列同步执行任务, 会造成线程死锁, 为什么呢?
    因为这么操作, 会把任务加主队列中, 同时同步执行会阻塞主线程, 让主线程优先执行自己的任务, 而主队列是串行队列, 只能顺序执行任务, 所以主线程执行完串行队列里的任务之前, 是不会执行此任务的, 但是你不完成此任务 , 就一直阻塞主线程 , 一直阻塞主线程就会一直完成不了任务, 所以就会造成死锁

    - (void)testDeadLock{
        NSLog(@"开始");
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"线程死锁了!");
        });
        NSLog(@"结束");
    }
    2018-08-28 15:52:39.675761+0800 Test[4277:586175] 开始
    只会打印出这句话,然后到此就崩溃了
    

    3.线程锁 , 保证线程安全 (防止多线程抢夺同一块资源,主要防止同时写入)

    • 互斥锁 : 让线程之间互相排斥 , 保证同一时间只能有一个线程去访问代码 , 使用@synchronized(object){代码块}可以加上互斥锁
      互斥锁原理 : 每一个对象内部都有一个锁 , 上面的object参数就是任意一个对象 , 它的内部是有一把锁的 , 当有线程要进入synchronized到代码块中就会先检查object的锁是打开还是关闭状态(默认锁是开打状态:1) . 如果锁是打开状态 , 线程就会执行代码块并上锁(0) , 执行完毕后就会解锁(1) ; 如果锁是关闭状态 , 线程想要执行代码块时 , 就要先等待 , 直到锁打开之后才能进入

    • 自旋锁 : 以死循环的方式等待锁定的代码执行完毕 , 使用atomic属性 , 就可以加上自旋锁
      自旋锁原理 : 如果发现有其它线程正在锁定代码 , 线程就会以死循环的方式 , 一直等待锁定的代码执行完毕 , 自选所更适合执行不耗时的代码 , automic属性内部就实现了自旋锁

    了解了两种锁之后,我们思考一个问题,我们开启两个线程同时去买票,怎么保证线程安全?

    @property (nonatomic,assign) NSInteger totalCount;  //默认为10张票
    
    //多线程买票
     - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [weakSelf buy];
        });
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [weakSelf buy];
        });
    }
      - (void)buyTicket{
        while (YES) {
            if (self.totalCount > 0) {
                self.totalCount = self.totalCount - 1;
                NSLog(@"剩余%ld张票",(long)self.totalCount);
            }else{
                NSLog(@"卖光了...");
                break;
            }
        }
    }
    
    

    想要解决这个问题 , 只需要保证对totalCount属性的读取和写入操作是原子性的 (读取和写入操作必须在一起执行) , 就可以保证线程安全 , 即对读取操作和写入操作同时加上互斥锁就可以.

    - (void)buyTicket{
        while (YES) {
            //互斥锁
            @synchronized (self) {     
                if (self.totalCount > 0) {
                    self.totalCount = self.totalCount - 1;
                    NSLog(@"剩余%ld",(long)self.totalCount);
                }else{
                    NSLog(@"卖光了...");
                    break;
                }
            }
        }
    }
    

    这里要注意的是 : 单纯的把nonatomic改成atomic是不行的 , 因为atomic保证了写入操作的线程安全 , 并没有同时保证读取和写入操作在一起执行.
    我们来复习一下属性修饰符
    nonatomic : 非原子属性,多个线程可以访问
    atomic : 原子属性,保证线程安全,单写多读,针对多线程设计,默认值,保证同一时间只有一个线程能够写入,但是同一时间多个线程都可以取值,注意虽然写入操作是线程安全的,但是并不能保证读取操作和写入操作在一起执行

    使用属性修饰符要注意的地方
    1. strong和retain的区别 : strong在ARC下使用,retain在MRC下使用 ; 均是强引用,都会使引用计数增加
    2. strong和copy的区别 : 本质上是setter方法的不同,在setter方法内部,strong仅仅存储了地址,而copy创建了一块内存去复制内容,并把新内存的地址存储起来
    3. weak和assign的区别 : weak只能在ARC下使用,修饰对象 ; assign只能用来修饰基本数据类型,在ARC和MRC下都可以使用 ; weak和assign都不会使引用计数增加 ; weak修饰的对象,一旦被释放就会被置为nil,给nil发送任何消息都没问题 ; 但是assigin修饰的对象被销毁后会维持原样,内存被销毁了,但是指针仍然指向那块内存,所以就会造成野指针导致崩溃
    4. __block和__weak的区别 : __block对象在block中可以被重新赋值,__weak不可以 ; __weak修饰可以防止循环引用,__block不行
    5. block为什么要用copy修饰? Block按存放位置不同可以分为三类:__NSGlobalBlock__(全局Block),__NSStackBlock__(栈Block) , __NSMallocBlock__(堆Block),对于保存在栈上的Block,它的作用域有限,超出作用域,block就会销毁掉,使用block时就会导致野指针 ; 所以我们需要对栈区的Block copy到 堆区,防止Block过早的销毁

    关于strong和weak原理的解释就需要引入内存管理的知识,并且引入autoreleasepool和RunLoop,有时间的话我会详细介绍
    关于__strong和__weak的实现原理,请研究clang和RunTime
    关于Block中循环引用问题,很好解决,但是原理是怎样的呢?待深入研究

    二 . GCD的具体使用方法

    1.GCD的基本使用
    - (void)commonGCD{
        dispatch_queue_t serialQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
        #pragma - 串行队列同步执行
        dispatch_sync(serialQueue, ^{
            sleep(1);
            NSLog(@"A---串行队列同步执行 : 会阻塞当前线程,让当前线程优先执行自己队列里的任务,当前线程 = %@",[NSThread currentThread]);
        });
        NSLog(@"A---串行队列同步执行 : 然后输出这句话,当前线程 = %@",[NSThread currentThread]);
        #pragma - 串行队列异步执行
        dispatch_async(serialQueue, ^{
            sleep(1);
            NSLog(@"B---串行队列异步执行 : 不会阻塞当前线程,会开辟一条新线程去执行自己队列里的任务,当前线程 = %@",[NSThread currentThread]);
        });
        NSLog(@"B---串行队列异步执行 : 和队列里的任务同时处理,当前线程 = %@",[NSThread currentThread]);
    
        
        dispatch_queue_t concurrentQueue = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
        #pragma - 并发行队列同步执行
        dispatch_sync(concurrentQueue, ^{
            sleep(1);
            NSLog(@"C---并发队列同步执行 : 会阻塞当前线程,让当前线程优先执行并发队列里的任务,当前线程 = %@",[NSThread currentThread]);
        });
        NSLog(@"C---并发队列同步执行 : 并发队列里的任务执行完毕才会输出,当前线程 = %@",[NSThread currentThread]);
        #pragma - 并发行队列异步执行
        dispatch_async(concurrentQueue, ^{
            sleep(1);
            NSLog(@"D1---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = %@",[NSThread currentThread]);
        });
        dispatch_async(concurrentQueue, ^{
            sleep(1);
            NSLog(@"D2---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = %@",[NSThread currentThread]);
        });
        NSLog(@"D---并发队列异步执行 : 不会阻塞当前线程,同时执行,当前线程 = %@",[NSThread currentThread]);
    }
    2018-08-27 17:37:28.164046+0800 Test[4601:829031] A---串行队列同步执行 : 会阻塞当前线程,让当前线程优先执行自己队列里的任务,当前线程 = <NSThread: 0x6000000780c0>{number = 1, name = main}
    2018-08-27 17:37:28.164264+0800 Test[4601:829031] A---串行队列同步执行 : 然后输出这句话,当前线程 = <NSThread: 0x6000000780c0>{number = 1, name = main}
    2018-08-27 17:37:28.164478+0800 Test[4601:829031] B---串行队列异步执行 : 和队列里的任务同时处理,当前线程 = <NSThread: 0x6000000780c0>{number = 1, name = main}
    2018-08-27 17:37:29.165190+0800 Test[4601:829031] C---并发队列同步执行 : 会阻塞当前线程,让当前线程优先执行并发队列里的任务,当前线程 = <NSThread: 0x6000000780c0>{number = 1, name = main}
    2018-08-27 17:37:29.165389+0800 Test[4601:829031] C---并发队列同步执行 : 并发队列里的任务执行完毕才会输出,当前线程 = <NSThread: 0x6000000780c0>{number = 1, name = main}
    2018-08-27 17:37:29.165548+0800 Test[4601:829031] D---并发队列异步执行 : 不会阻塞当前线程,同时执行,当前线程 = <NSThread: 0x6000000780c0>{number = 1, name = main}
    2018-08-27 17:37:29.166267+0800 Test[4601:829098] B---串行队列异步执行 : 不会阻塞当前线程,会开辟一条新线程去执行自己队列里的任务,当前线程 = <NSThread: 0x60400027b640>{number = 3, name = (null)}
    2018-08-27 17:37:30.165904+0800 Test[4601:829096] D2---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = <NSThread: 0x60400027c540>{number = 5, name = (null)}
    2018-08-27 17:37:30.165904+0800 Test[4601:829100] D1---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = <NSThread: 0x60000027ce40>{number = 4, name = (null)}
    

    总结 :

    • 串行队列同步执行 : 会阻塞当前线程 , 让当前线程优先执行我这个串行队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .
    • 串行队列异步执行 : 开辟1个新线程 , 在这个新线程中顺序执行任务
    • 并发队列同步执行 : 会阻塞当前线程 , 让当前线程优先执行我这个并发队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .
    • 并发队列异步执行 : 开辟多个新线程 , 在不同线程中同时执行不同任务

    上面说到 : 异步执行会开辟新线程,这里有一个特殊情况 :
    主队列是一个串行队列 , 在主队列中异步执行任务 , 不会开辟新的线程 , 因为主队列中的任务只能由主线程来执行 , 所以当GCD要开辟线程去执行主队列中的任务时 , 发现是主队列 , 所以不予开辟新线程 , 主线程按顺序执行主队列的任务.

    - (void)mainThreadGCD{
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        NSLog(@"主队列中异步执行任务-开始,当前线程 = %@",[NSThread currentThread]);
        dispatch_async(mainQueue, ^{
            NSLog(@"主队列中异步执行任务-正在执行,当前线程 = %@",[NSThread currentThread]);
        });
        NSLog(@"主队列中异步执行任务-结束,当前线程 = %@",[NSThread currentThread]);
        sleep(1);
        NSLog(@"主队列中异步执行任务-休眠1s后,当前线程 = %@",[NSThread currentThread]);
    }
    2018-08-27 17:28:03.478270+0800 Test[4517:813940] 主队列中异步执行任务-开始,当前线程 = <NSThread: 0x604000070dc0>{number = 1, name = main}
    2018-08-27 17:28:03.478496+0800 Test[4517:813940] 主队列中异步执行任务-结束,当前线程 = <NSThread: 0x604000070dc0>{number = 1, name = main}
    2018-08-27 17:28:04.478929+0800 Test[4517:813940] 主队列中异步执行任务-休眠1s结束,当前线程 = <NSThread: 0x604000070dc0>{number = 1, name = main}
    2018-08-27 17:28:04.498234+0800 Test[4517:813940] 主队列中异步执行任务-正在执行,当前线程 = <NSThread: 0x604000070dc0>{number = 1, name = main}
    
    
    2.GCD常见操作

    (1). 延迟执行 , 如果队列是主队列 , 则在主线程中执行 ; 如果是全局队列或者自定义队列 , 则任务在子线程中执行

    - (void)afterGCD{
        dispatch_time_t afterTime = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
        NSLog(@"开始");
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
        dispatch_queue_t customSerialQueue = dispatch_queue_create("自定义串行队列", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t customConcurrentQueue = dispatch_queue_create("自定义并发队列", DISPATCH_QUEUE_CONCURRENT);
        dispatch_after(afterTime, mainQueue, ^{
            NSLog(@"主队列---5s之后,当前线程 = %@",[NSThread currentThread]);
        });
        dispatch_after(afterTime, globalQueue, ^{
            NSLog(@"全局队列---5s之后,当前线程 = %@",[NSThread currentThread]);
        });
        dispatch_after(afterTime, customSerialQueue, ^{
            NSLog(@"自定义串行队列---5s之后,当前线程 = %@",[NSThread currentThread]);
        });
        dispatch_after(afterTime, customConcurrentQueue, ^{
            NSLog(@"自定义并发队列---5s之后,当前线程 = %@",[NSThread currentThread]);
        });
        NSLog(@"结束");
    }
    2018-08-27 17:57:13.323480+0800 Test[4830:862430] 开始
    2018-08-27 17:57:13.323710+0800 Test[4830:862430] 结束
    2018-08-27 17:57:18.323898+0800 Test[4830:862576] 自定义并发队列---5s之后,当前线程 = <NSThread: 0x6040004689c0>{number = 5, name = (null)}
    2018-08-27 17:57:18.323898+0800 Test[4830:862430] 主队列---5s之后,当前线程 = <NSThread: 0x600000260900>{number = 1, name = main}
    2018-08-27 17:57:18.323898+0800 Test[4830:862575] 自定义串行队列---5s之后,当前线程 = <NSThread: 0x600000467140>{number = 3, name = (null)}
    2018-08-27 17:57:18.323898+0800 Test[4830:862573] 全局队列---5s之后,当前线程 = <NSThread: 0x6000004671c0>{number = 4, name = (null)}
    

    (2). 执行一次

    - (void)onceGCD{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
           NSLog(@"在整个应用的生命周期内,只会执行一次,当前线程 = %@",[NSThread currentThread]);
        });
    }
    2018-08-27 18:04:11.097953+0800 Test[4897:874867] 在整个应用的声明周期内,只会执行一次,当前线程 = <NSThread: 0x604000079080>{number = 1, name = main}
    

    (3). 重复执行 , "执行完毕"一定会输出在最后的位置 , 因为dispatch_apply方法会阻塞当前线程

    - (void)applyGCD{
        dispatch_queue_t serialQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t concurrentQueue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"串行队列-重复执行5次是按顺序执行的");
        dispatch_apply(5, serialQueue, ^(size_t count) {
            sleep(1);
            NSLog(@"第%zu此-当前线程=%@",count,[NSThread currentThread]);
        });
        NSLog(@"串行队列-重复执行完毕");
        sleep(1);
        NSLog(@"并发队列-重复执行5次都是同时执行的");
        dispatch_apply(5, concurrentQueue, ^(size_t count) {
            sleep(1);
            NSLog(@"第%zu此-当前线程=%@",count,[NSThread currentThread]);
        });
        NSLog(@"并发队列-重复执行完毕");
    }
    2018-08-27 18:13:59.850946+0800 Test[5072:894227] 串行队列-重复执行5次是按顺序执行的
    2018-08-27 18:14:00.851306+0800 Test[5072:894227] 第0此-当前线程=<NSThread: 0x604000067d40>{number = 1, name = main}
    2018-08-27 18:14:01.851617+0800 Test[5072:894227] 第1此-当前线程=<NSThread: 0x604000067d40>{number = 1, name = main}
    2018-08-27 18:14:02.853056+0800 Test[5072:894227] 第2此-当前线程=<NSThread: 0x604000067d40>{number = 1, name = main}
    2018-08-27 18:14:03.853922+0800 Test[5072:894227] 第3此-当前线程=<NSThread: 0x604000067d40>{number = 1, name = main}
    2018-08-27 18:14:04.855369+0800 Test[5072:894227] 第4此-当前线程=<NSThread: 0x604000067d40>{number = 1, name = main}
    2018-08-27 18:14:04.855578+0800 Test[5072:894227] 串行队列-重复执行完毕
    2018-08-27 18:14:05.856076+0800 Test[5072:894227] 并发队列-重复执行5次都是同时执行的
    2018-08-27 18:14:06.857647+0800 Test[5072:894227] 第0此-当前线程=<NSThread: 0x604000067d40>{number = 1, name = main}
    2018-08-27 18:14:06.857685+0800 Test[5072:894347] 第1此-当前线程=<NSThread: 0x604000260580>{number = 4, name = (null)}
    2018-08-27 18:14:06.857689+0800 Test[5072:894510] 第2此-当前线程=<NSThread: 0x600000262b00>{number = 3, name = (null)}
    2018-08-27 18:14:06.857730+0800 Test[5072:894509] 第3此-当前线程=<NSThread: 0x6040002638c0>{number = 5, name = (null)}
    2018-08-27 18:14:07.860394+0800 Test[5072:894347] 第4此-当前线程=<NSThread: 0x604000260580>{number = 4, name = (null)}
    2018-08-27 18:14:07.860623+0800 Test[5072:894227] 并发队列-重复执行完毕
    
    3.GCD的栅栏
    • barrier将任务分割开执行
    • dispatch_barrier_async异步分割 , 不会阻塞当前线程
    • dispatch_barrier_sync同步分割 , 会阻塞当前线程
      例如 : 你想并发异步执行任务 , 并且想先同时执行第1组和第2组任务,然后在同时执行第3组和第4组任务 , 就可以使用栅栏
    - (void)barrierGCD{
        dispatch_queue_t concurrentQueue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 3; i++) {
                NSLog(@"第1组任务-栅栏-并发异步-%d-%@",i,[NSThread currentThread]);
            }
        });
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 3; i++) {
                NSLog(@"第2组任务-栅栏-并发异步2-%d-%@",i,[NSThread currentThread]);
            }
        });
        dispatch_barrier_async(concurrentQueue, ^{
            NSLog(@"---分割---");
            NSLog(@"第1组和第2组任务是同时执行的");
            NSLog(@"第3组和第4组任务也是同时执行的");
            NSLog(@"但是第3组和第4组任务一定在第1组和第2组任务完成之后执行");
            NSLog(@"---分割---");
        });
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 3; i++) {
                NSLog(@"第3组任务-栅栏-并发异步-%d-%@",i,[NSThread currentThread]);
            }
        });
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 3; i++) {
                NSLog(@"第4组任务-栅栏-并发异步2-%d-%@",i,[NSThread currentThread]);
            }
        });
        NSLog(@"任务完成");
    }
    2018-08-27 18:30:01.262625+0800 Test[5240:921467] 第1组任务-栅栏-并发异步-0-<NSThread: 0x604000464f80>{number = 3, name = (null)}
    2018-08-27 18:30:01.262625+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-0-<NSThread: 0x600000471680>{number = 4, name = (null)}
    2018-08-27 18:30:01.262648+0800 Test[5240:921366] 任务完成
    2018-08-27 18:30:01.262927+0800 Test[5240:921467] 第1组任务-栅栏-并发异步-1-<NSThread: 0x604000464f80>{number = 3, name = (null)}
    2018-08-27 18:30:01.262959+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-1-<NSThread: 0x600000471680>{number = 4, name = (null)}
    2018-08-27 18:30:01.263139+0800 Test[5240:921467] 第1组任务-栅栏-并发异步-2-<NSThread: 0x604000464f80>{number = 3, name = (null)}
    2018-08-27 18:30:01.263166+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-2-<NSThread: 0x600000471680>{number = 4, name = (null)}
    2018-08-27 18:30:01.263289+0800 Test[5240:921468] ---分割---
    2018-08-27 18:30:01.263393+0800 Test[5240:921468] 第1组和第2组任务是同时执行的
    2018-08-27 18:30:01.263492+0800 Test[5240:921468] 第3组和第4组任务也是同时执行的
    2018-08-27 18:30:01.263603+0800 Test[5240:921468] 但是第3组和第4组任务一定在第1组和第2组任务完成之后执行
    2018-08-27 18:30:01.263709+0800 Test[5240:921468] ---分割---
    2018-08-27 18:30:01.269175+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-0-<NSThread: 0x600000471680>{number = 4, name = (null)}
    2018-08-27 18:30:01.269175+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-0-<NSThread: 0x604000464f80>{number = 3, name = (null)}
    2018-08-27 18:30:01.269335+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-1-<NSThread: 0x600000471680>{number = 4, name = (null)}
    2018-08-27 18:30:01.269366+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-1-<NSThread: 0x604000464f80>{number = 3, name = (null)}
    2018-08-27 18:30:01.269574+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-2-<NSThread: 0x600000471680>{number = 4, name = (null)}
    2018-08-27 18:30:01.269909+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-2-<NSThread: 0x604000464f80>{number = 3, name = (null)}
    
    4.GCD的队列组
    • 当任务都完成时 , dispatch_group_notify的任务才会执行 , dispatch_group_notify不会阻塞当前线程
    • dispatch_group_wait会阻塞当前线程 , 直到group的任务完成之后 , 才会继续往下执行
    • dispatch_group_enter每执行一次 , group中的未完成任务就会+1 , group中的任务未完成之前 , dispatch_group_notify会不阻塞线程的等待 , dispatch_group_wait会阻塞当前线程式的等待.
    • dispatch_group_leave每执行一次 , group中的未完成任务就会-1 , group中的完成之后 , dispatch_group_notify会执行 , dispatch_group_wait也会开始执行.
    - (void)groupNotifyGCD{
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
        
        NSLog(@"任务开始");
        dispatch_group_async(group, globalQueue, ^{
            sleep(1);
            NSLog(@"任务1完成");
        });
        dispatch_group_async(group, globalQueue, ^{
            sleep(2);
            NSLog(@"任务2完成");
        });
        #pragma 当任务完成时,可以选择在哪个队列执行任务
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            //如果是主队列,则此处为主线程
            //如果是其它队列,则此处为子线程
            NSLog(@"任务已完成");
        });
        NSLog(@"任务结束");
    }
    2018-08-28 09:49:26.141459+0800 Test[1184:54263] 任务开始
    2018-08-28 09:49:26.141670+0800 Test[1184:54263] 任务结束
    2018-08-28 09:49:27.142816+0800 Test[1184:54733] 任务1完成
    2018-08-28 09:49:28.144000+0800 Test[1184:54731] 任务2完成
    2018-08-28 09:49:28.144230+0800 Test[1184:54263] 任务已完成
    
    - (void)groupWaitGCD{
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
        
        NSLog(@"任务开始");
        dispatch_group_async(group, globalQueue, ^{
            sleep(1);
            NSLog(@"任务1完成");
        });
        dispatch_group_async(group, globalQueue, ^{
            sleep(2);
            NSLog(@"任务2完成");
        });
        dispatch_group_notify(group, globalQueue, ^{
            NSLog(@"任务已完成,%@",[NSThread currentThread]);;
        });
        
        #pragma 会阻塞当前线程,直到group中的任务都结束后,才会执行"任务结束"
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"任务结束");
    }
    2018-08-28 09:59:21.225301+0800 Test[1314:80098] 任务开始
    2018-08-28 09:59:22.226823+0800 Test[1314:80208] 任务1完成
    2018-08-28 09:59:23.226653+0800 Test[1314:80209] 任务2完成
    2018-08-28 09:59:23.226884+0800 Test[1314:80098] 任务结束
    2018-08-28 09:59:23.226995+0800 Test[1314:80209] 任务已完成,<NSThread: 0x600000460f40>{number = 3, name = (null)}
    
    -(void)groupEnterAndLeave{
        NSLog(@"开始");
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
        dispatch_group_enter(group);
        dispatch_async(globalQueue, ^{
            sleep(1);
            NSLog(@"任务1完成");
            dispatch_group_leave(group);
        });
        dispatch_group_enter(group);
        dispatch_async(globalQueue, ^{
            sleep(2);
            NSLog(@"任务2完成");
            dispatch_group_leave(group);
        });
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"任务完成");
        });
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"结束");
    }
    2018-08-28 10:37:44.867311+0800 Test[1589:111949] 开始
    2018-08-28 10:37:45.870664+0800 Test[1589:112036] 任务1完成
    2018-08-28 10:37:46.870663+0800 Test[1589:112039] 任务2完成
    2018-08-28 10:37:46.870949+0800 Test[1589:111949] 结束
    2018-08-28 10:37:46.890723+0800 Test[1589:111949] 任务完成
    
    5.信号量
    作用
    • 将异步变成同步 , 其实除了信号量semaphore之外 , dispatch_groupdispatch_barrier都可以做到将异步变成同步 , 思考一下怎么做吧.
    • 为线程加锁
    用法
    • dispatch_semaphore_create创建信号量 , 如果信号量小于0 , 会返回nil
    • dispatch_semaphore_wait会做一个判断 :
      • 如果当前信号量为0 , 则会阻塞当前线程 , 直到信号量大于0
      • 如果当前信号量大于0 , 则会让当前信号量减一 , 并继续执行 (不会阻塞当前线程)
    • dispatch_semaphore_signal会让信号量加一 , 不会阻塞线程 , 会唤醒被dispatch_semaphore_wait阻塞的线程

    信号量的基本用法 :

    - (void)semaphoreGCD{
        NSLog(@"开始");
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(2);
            NSLog(@"任务完成");
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"结束");
    }
    2018-08-28 11:19:04.376679+0800 Test[1999:176567] 开始
    2018-08-28 11:19:04.376917+0800 Test[1999:176567] 结束
    2018-08-28 11:19:06.380869+0800 Test[1999:176609] 任务完成
    

    异步变同步的三种方式 , 要注意防止主线程被阻塞导致的"卡死"

    //异步变同步
    - (void)asyncToSync{
        #pragma mark - group
        NSLog(@"任务1开始");
        dispatch_group_t group = dispatch_group_create();
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            sleep(1);
            NSLog(@"任务1执行完毕");
        });
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"任务1终于完成了,可以做其它的事情了");
        
        #pragma mark - barrier
        #pragma 自定义的并发队列,线程阻断才会生效
        dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);  
        #pragma 系统的并发队列,线程阻断无效
    //    dispatch_queue_t queue = dispatch_get_global_queue(0, 0); 
        NSLog(@"任务2开始");
        dispatch_async(queue, ^{
            sleep(1);
            NSLog(@"任务2执行完毕");
        });
        dispatch_barrier_sync(queue, ^{
            NSLog(@"任务2完成了");
        });
        NSLog(@"任务2终于完成了,可以做其他事情了");
        
        #pragma mark - semaphore
        NSLog(@"任务3开始");
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        dispatch_async(queue, ^{
            sleep(1);
            NSLog(@"任务3执行完毕");
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任务3终于完成了,可以做其他事情了");
    }
    2018-08-28 15:21:11.689522+0800 Test[3984:489019] 任务1开始
    2018-08-28 15:21:12.690900+0800 Test[3984:489091] 任务1执行完毕
    2018-08-28 15:21:12.691178+0800 Test[3984:489019] 任务1终于完成了,可以做其它的事情了
    2018-08-28 15:21:12.691379+0800 Test[3984:489019] 任务2开始
    2018-08-28 15:21:13.694621+0800 Test[3984:489091] 任务2执行完毕
    2018-08-28 15:21:13.694876+0800 Test[3984:489019] 任务2完成了
    2018-08-28 15:21:13.695004+0800 Test[3984:489019] 任务2终于完成了,可以做其他事情了
    2018-08-28 15:21:13.695143+0800 Test[3984:489019] 任务3开始
    2018-08-28 15:21:14.695499+0800 Test[3984:489091] 任务3执行完毕
    2018-08-28 15:21:14.695726+0800 Test[3984:489019] 任务3终于完成了,可以做其他事情了
    

    这里我们有疑问了 , 异步变同步有啥用? 那么我们看看AFNetworking里的源码就知道了 : (摘录自AFURLSessionManager.m)

    #pragma mark -
    
    - (NSArray *)tasksForKeyPath:(NSString *)keyPath {
        __block NSArray *tasks = nil;
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
            if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
                tasks = dataTasks;
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
                tasks = uploadTasks;
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
                tasks = downloadTasks;
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
                tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
            }
    
            dispatch_semaphore_signal(semaphore);
        }];
    
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
        return tasks;
    }
    

    继续回到上面的多线程买票的问题 , 利用semaphore可以保证线程安全 , 思考下下面的代码里为什么要调用两次dispatch_semaphore_signal(_semaphore)

    //注意 : 这里的_semaphore必须设置为全局变量 , 并且信号量初始值为1 , 才能保证线程安全 , 如果不设置为全局变量的话 , 多线程每次进来执行的时候都会新建一个信号量 , 没法保证线程安全了
    - (void)semaphoreBuyTicket{
        while (YES) {
            dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
            if (_totalCount > 0){
                _totalCount = _totalCount - 1;
                NSLog(@"还剩%ld",_totalCount);
            }else{
                NSLog(@"卖光了");
                dispatch_semaphore_signal(_semaphore);
                break;
            }
            dispatch_semaphore_signal(_semaphore);
        }
    }
    

    练习题 : 以下代码会如何输出?

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [weakSelf testSemaphoreGCD];
        });
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [weakSelf testSemaphoreGCD];
        });
    }
    #pragma mark - 这里的_semaphore = dispatch_semaphore_create(1)
    - (void)testSemaphoreGCD{
        NSLog(@"开始111");
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"开始222");
        sleep(5);
        NSLog(@"结束");
        dispatch_semaphore_signal(_semaphore);
    }
    

    答案 :

    2018-08-28 12:03:51.868977+0800 Test[2527:242055] 开始111
    2018-08-28 12:03:51.868974+0800 Test[2527:242059] 开始111
    2018-08-28 12:03:51.869302+0800 Test[2527:242055] 开始222
    2018-08-28 12:03:56.872708+0800 Test[2527:242055] 结束
    2018-08-28 12:03:56.873042+0800 Test[2527:242059] 开始222
    2018-08-28 12:04:01.875866+0800 Test[2527:242059] 结束
    

    相关文章

      网友评论

          本文标题:iOS - 多线程

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