iOS - 多线程

前言

iOS的多线程有四种 : pthread , GCD , NSTread , NSOperation, 前两种C语言编写 , 后两种OC语言编写 , NSThread是对pthread的封装 , NSOperation是对GCD的封装.
日常工作中我们最常用的是GCD, GCD会自动分配CPU资源 , 会自动管理线程的生命周期 , 使用起来非常方便 , 本文只讲述GCD

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

1. 进程和线程

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

  • (2). 线程 : 执行任务基本单元 , 一个App可以有多个线程

    • 线程创建好后 , 会放入可调度线程池中 , 等待CPU调度

    • 线程的几种状态 : 新建 , 就绪 , 运行 , 死亡 , 阻塞

    • 注意 : 一个CPU核心同一时间内只能执行一条线程 , 即单核CPU同一时间只能处理一条线程 , 多核CPU同一时间可以处理多条线程

  • (3). 多线程的作用,我们为什么要学习多线程?

    • 提高程序执行效率 , 更好的利用多核CPU资源 , 多条线程可以同时利用多个CPU核心

    • 防止阻塞主线程

    • GCD中 , 线程执行完毕后会自动销毁这个线程

  • (4). 多线程的缺点 , 我们使用多线程时要注意的地方

    • 线程太多会占用大量内存 , iOS系统中, 子线程一个线程会占用512KB的空间 , 主线程会占用1M的空间 , 开辟一个线程大约消耗90毫秒

    • 线程太多会导致CPU调度线程开销过大 , 降低程序运行效率 (例如 : 单核CPU执行并发任务时 , CPU会对线程执行一个时间片规定的时间 , 然后快速切换到下一个线程, 由于时间片非常短 , 所以看起来是同时执行的 , 但是实际上是伪同时 , 线程太多会让CPU疲于调度 . CPU切换线程时 , 线程会在就绪状态和运行状态之间切换)

    • 可能会造成死锁 (可以理解为互相等待)

    • 数据竞争 (多个线程操作同一个数据,导致数据紊乱)

    • 上述缺点的解决办法 , 我们会在下文中讲到

2. 队列和执行方式

  • (1). 两个常用概念

    • 队列 : 存放任务的队形结构 , 系统以先进先出的方式(FIFO)调度队列中的任务

    • 任务 : 任务就是在线程中要执行的代码

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

  • (2). 两种通用队列

    • 串行队列 : 在一条线程中 , 按顺序执行任务的队列

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

    • 不管是串行队列还是并发队列 , 都是按照FIFO(先进先出)的原则, 依次将任务分配线程

    • **串行队列原理** : CPU会从串行队列中取出一个任务 , 然后分配一个线程去执行 , 然后等待任务执行完毕后 , CPU继续取出任务 , 分配线程执行,也就是CPU会等待任务完成

    • 并发队列原理: CPU会从并行队列中取出一个任务 , 然后分配一个线程去执行 , 不会等待任务执行完毕 , 就会CPU就会又去取出任务 , 然后分配新的线程去执行,也就是CPU会等待发生

(3). 两种执行方式

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

  • 异步执行(async) : 开辟新线程执行任务

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

(4). 两种特殊队列

  • 主队列-main : 系统为我们创建好的一个串行队列 , 它用来管理主线程的任务

  • 全局队列-global: 系统为我们创建好的并发队列 , 不受线程阻断barrier的影响

(5). 四种组合方式

  • 串行队列同步执行 : 会阻塞当前线程 , 让当前线程优先执行我这个串行队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .

  • 串行队列异步执行 : 开辟1个新线程 , 在这个新线程中顺序执行任务

  • 并发队列同步执行 : 会阻塞当前线程 , 让当前线程优先执行我这个并发队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .

  • 并发队列异步执行 : 开辟多个新线程 , 在不同线程中同时执行不同任务

(6). 在主线程中使用主队列同步执行任务, 会不会造成线程死锁呢, 如果会,那么原因是什么呢?

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

- (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 *)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---串行队列同步执行 : 会阻塞当前线程,让当前线程优先执行自己队列里的任务,当前线程 = {number = 1, name = main}
2018-08-27 17:37:28.164264+0800 Test[4601:829031] A---串行队列同步执行 : 然后输出这句话,当前线程 = {number = 1, name = main}
2018-08-27 17:37:28.164478+0800 Test[4601:829031] B---串行队列异步执行 : 和队列里的任务同时处理,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.165190+0800 Test[4601:829031] C---并发队列同步执行 : 会阻塞当前线程,让当前线程优先执行并发队列里的任务,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.165389+0800 Test[4601:829031] C---并发队列同步执行 : 并发队列里的任务执行完毕才会输出,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.165548+0800 Test[4601:829031] D---并发队列异步执行 : 不会阻塞当前线程,同时执行,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.166267+0800 Test[4601:829098] B---串行队列异步执行 : 不会阻塞当前线程,会开辟一条新线程去执行自己队列里的任务,当前线程 = {number = 3, name = (null)}
2018-08-27 17:37:30.165904+0800 Test[4601:829096] D2---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = {number = 5, name = (null)}
2018-08-27 17:37:30.165904+0800 Test[4601:829100] D1---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = {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] 主队列中异步执行任务-开始,当前线程 = {number = 1, name = main}
2018-08-27 17:28:03.478496+0800 Test[4517:813940] 主队列中异步执行任务-结束,当前线程 = {number = 1, name = main}
2018-08-27 17:28:04.478929+0800 Test[4517:813940] 主队列中异步执行任务-休眠1s结束,当前线程 = {number = 1, name = main}
2018-08-27 17:28:04.498234+0800 Test[4517:813940] 主队列中异步执行任务-正在执行,当前线程 = {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之后,当前线程 = {number = 5, name = (null)}
2018-08-27 17:57:18.323898+0800 Test[4830:862430] 主队列---5s之后,当前线程 = {number = 1, name = main}
2018-08-27 17:57:18.323898+0800 Test[4830:862575] 自定义串行队列---5s之后,当前线程 = {number = 3, name = (null)}
2018-08-27 17:57:18.323898+0800 Test[4830:862573] 全局队列---5s之后,当前线程 = {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] 在整个应用的声明周期内,只会执行一次,当前线程 = {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此-当前线程={number = 1, name = main}
2018-08-27 18:14:01.851617+0800 Test[5072:894227] 第1此-当前线程={number = 1, name = main}
2018-08-27 18:14:02.853056+0800 Test[5072:894227] 第2此-当前线程={number = 1, name = main}
2018-08-27 18:14:03.853922+0800 Test[5072:894227] 第3此-当前线程={number = 1, name = main}
2018-08-27 18:14:04.855369+0800 Test[5072:894227] 第4此-当前线程={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此-当前线程={number = 1, name = main}
2018-08-27 18:14:06.857685+0800 Test[5072:894347] 第1此-当前线程={number = 4, name = (null)}
2018-08-27 18:14:06.857689+0800 Test[5072:894510] 第2此-当前线程={number = 3, name = (null)}
2018-08-27 18:14:06.857730+0800 Test[5072:894509] 第3此-当前线程={number = 5, name = (null)}
2018-08-27 18:14:07.860394+0800 Test[5072:894347] 第4此-当前线程={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-{number = 3, name = (null)}
2018-08-27 18:30:01.262625+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-0-{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-{number = 3, name = (null)}
2018-08-27 18:30:01.262959+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-1-{number = 4, name = (null)}
2018-08-27 18:30:01.263139+0800 Test[5240:921467] 第1组任务-栅栏-并发异步-2-{number = 3, name = (null)}
2018-08-27 18:30:01.263166+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-2-{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-{number = 4, name = (null)}
2018-08-27 18:30:01.269175+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-0-{number = 3, name = (null)}
2018-08-27 18:30:01.269335+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-1-{number = 4, name = (null)}
2018-08-27 18:30:01.269366+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-1-{number = 3, name = (null)}
2018-08-27 18:30:01.269574+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-2-{number = 4, name = (null)}
2018-08-27 18:30:01.269909+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-2-{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] 任务已完成,{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 *)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 - 多线程)