iOS多线程-GCD

649 阅读9分钟

Grand Central Dispatch

GCD基础

  • 特点
    • 纯C语言的多线程管理
  • 优势
    • 是苹果公司为多核的并行运算提出的解决方案
    • 自动利用更多的CPU内核(比如双核、四核)
    • 自动管理线程的生命周期(创建线程、调度任务、销毁线程)
    • 只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
  • 核心
    • 将任务添加到队列里 任务:需要执行的操作 队列:任务的集合

同步任务&异步任务

GCD中有2个用来执行任务的函数

  • 同步任务
    • 在当前线程执行(不代表主线程)
    • 立即执行

参数1:任务需要添加到的队列;参数2:任务block

dispatch_sync(dispatch_queue_t _Nonnull queue, ^(void)block);

  • 异步任务
    • 新开辟线程执行(主队列不开辟)
    • 等待CPU调度执行

dispatch_async(dispatch_queue_t _Nonnull queue, ^(void)block);

串行队列&并发队列

  • 串行队列(Serial Dispatch Queue)

    • 让任务一个接着一个有序的执行,不管队列里放的是什么任务,一个任务执行完毕后,再执行下一个任务
    • 同时只能调度一个任务执行
    • 特点
      • 以先进先出(FIFO)的方式,顺序调用队列中的任务执行
      • 无论队列中所指定的执行任务函数是同步还是一部,都会等待前一个任务执行完成后,再调度后面的任务
  • 并发队列(Concurrent Dispatch Queue)

    • 可以让多个任务同时执行,自动开启多个线程同时执行多个任务
    • 同时可以调度多个任务
    • 并发队列的并发功能只有内部任务是异步任务是,才有效

队列与任务的关系(是否开辟新线程,执行顺序)

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self demo4];
}

//MARK:并发队列+异步任务
//是否开辟线程:开辟(多个线程)
//是否有序:无序(因为多线程时CPU随机调度)
//end位置:不确定
-(void)demo4{
    dispatch_queue_t q = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    
    for (int i =0 ; i<5; i++) {
        dispatch_async(q, ^{
            NSLog(@"%d:%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"end");
}

//MARK:并行队列+同步任务
//是否开辟线程:不开辟
//是否有序:有序(如果是一条线程,无论什么队列都是有序的)
//end位置:最后
-(void)demo3{
    dispatch_queue_t q = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    
    for (int i =0 ; i<5; i++) {
        dispatch_sync(q, ^{
            NSLog(@"%d:%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"end");
}

//MARK:串行队列+异步任务
//是否开辟线程:开辟(一个线程;因为要保证有序)
//是否有序:有序
//end位置:不确定(异步任务end的位置都不确定,因为CPU在线程池里是随机调度线程的)
-(void)demo2{
    dispatch_queue_t q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    
    for (int i =0 ; i<5; i++) {
        dispatch_async(q, ^{
            NSLog(@"%d:%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"end");
}

//MARK:串行队列+同步任务
//是否开辟线程:不开辟
//是否有序:有序
//end位置:最后(同步任务立即执行,并且当前任务是在主线程上,所以end在最后)
-(void)demo1{
    dispatch_queue_t q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    
    for (int i =0 ; i<5; i++) {
        dispatch_sync(q, ^{
            NSLog(@"%d:%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"end");
}

线程重用

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //NSTread: 连续点击,会不断创建新线程
//    [self performSelectorInBackground:@selector(demo) withObject:nil];
    
    //GCD: 连续点击,不会马上创建新线程,而是在线程复用池里找是否有可以复用的线程,有就拿来来使用,没有才创建
    //如果间隔一段时间,线程复用池里的线程会自动销毁,那么也会新创建线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
}

-(void)demo{
    NSLog(@"%@",[NSThread currentThread]);
}

特殊的队列(主队列&全局队列)

  • 主队列:不需要创建,程序启动的同时就会创建一个主队列(是一个特殊的串行队列)
    • 主队列里的任务只能有主线程来执行
    • 主队列里的任务只有等主线程空闲再才执行
    • 异步任务里如果加入的是主队列,那么也不会开辟线程
    • 只能在异步任务中使用,在同步任务中会崩溃(死锁)

主队列不等于主线程!!!主队列是一堆任务的集合,主线程是做任务的人

  • 使用

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        [self demo2];
    }
    -(void)demo2{
    	//必须加入到异步任务,如果加入到同步任务,xcode7以前会一直卡住,xcode8之后直接崩溃
    	//原因:同步任务立即执行,而主队列又是主线程任务执行完后才能开始执行,造成线程死锁
    	dispatch_sync(dispatch_get_main_queue(), ^{
    		NSLog(@"%@",[NSThread currentThread]);
    	});
        NSLog(@"end");
    }
    -(void)demo1{
        //常用于线程通讯,异步任务完成后回到主线程刷新UI
        dispatch_async(dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT), ^{
            dispatch_async(dispatch_get_main_queue(), ^{
    			NSLog(@"%@",[NSThread currentThread]);
    		});
        });
        NSLog(@"end");
    }
    
  • 全局队列(就是并发队列)

    • 也不需要创建,程序启动的同时就会创建一个全局队列(项目中所有位置都能使用)
    • 系统为了方便程序员开发提供的,其工作表现与并发队列一致
  • 使用

    //参数1:服务质量(优先级)
    /*
    iOS8(含)以后
    *  - QOS_CLASS_USER_INTERACTIVE    用户交互(希望最快完成-不能用太耗时的操作)
    *  - QOS_CLASS_USER_INITIATED      用户发起(希望快,也不能太耗时)
    *  - QOS_CLASS_DEFAULT             默认(用来底层重置队列使用的,不是给程序员用的)
    *  - QOS_CLASS_UTILITY             实用工具(专门用来处理耗时操作!)
    *  - QOS_CLASS_BACKGROUND          后台
    *
    iOS8以前
    *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
    *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
    *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
    *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
    */
    //参数2:给未来保留使用的
    for (int i = 0; i<5; i++) {
      dispatch_async(dispatch_get_global_queue(0, 0), ^{
          NSLog(@"%@",[NSThread currentThread]);
      });
    }
    

队列和任务组合总结

PS: 在GCD中,造成死锁的主要情况就是在当前串行队列里面同步执行当前串行队列。解决的方法就是将同步的串行队列放到另外一个线程执行。

GCD进阶

Barrier(阻塞,栅栏函数)

  • 使用场景

    1. 解决多线程资源共享的问题
      • 主要用于在 多个异步操作 完成之后,统一对 非线性安全 对象进行操作
      • 没有使用阻塞的话,因为异步操作非线程安全对象,所以可能出现多个线程在访问imgList的同个元素的setter方法(非线程安全对象setter方法里没有自旋锁),导致重复赋值,最终元素个数不足
      • 适合用于大规模的I/0操作
    2. 分隔同个队列中的任务
      • 队列中后面几个任务需要在前几个任务全部完成的情况下才可进行
  • 使用

@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *imgList;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self demo1];
}

//用法1:解决资源共享问题
- (void)demo1{
    _imgList = [NSMutableArray array];

    //使用barrier必须用自己创建的列队,不能使用全局列队,
    //    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //并行列队
    dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i<5; i++) {
        //异步执行
        dispatch_async(queue, ^{
            NSString *img = [NSString stringWithFormat:@"img%d",i];
            NSLog(@"阻塞前 img:%@, imglist:%@, 线程:%@",img,self.imgList,[NSThread currentThread]);

            //栅栏函数
            //只有等列队里所有任务执行完后,才会执行阻塞里的代码,并且是异步执行(会新开一条线程,因为线程复用,这条线程在线程缓存池里取出,所以是最后一条执行完的线程)
            dispatch_barrier_async(queue, ^{
                [self.imgList addObject:img];
                NSLog(@"阻塞 imglist:%@, 线程:%@",self.imgList,[NSThread currentThread]);
            });
        });
    }
}


//用法2:分隔同个队列中的任务
- (void)demo2{
    dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"+++++%d",i);
        });
    }

    //栅栏函数
    //将同个队列中的任务分隔执行,只有在barrier前的任务执行完才会执行barrier后的任务
    dispatch_barrier_async(queue, ^{
        NSLog(@"barrier");
    });

    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"-----%d",i);
        });
    }
}

注意:不能用全局队列!会崩溃,要用自己创建的队列

  • dispatch_barrier_syncdispatch_barrier_async区别
    • 区别在于会不会阻塞当前的线程
  • 使用
//同步栅栏函数
- (void)demo3{
    dispatch_queue_t queue = dispatch_queue_create("sync", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    //栅栏函数
    dispatch_barrier_sync(queue, ^{
        NSLog(@"3");
    });

    dispatch_async(queue, ^{
        NSLog(@"4");
    });

    NSLog(@"5");
}
// 输出结果:12354
// 同步栅栏函数,阻塞当前的线程,所以 5一定是在3 后面打印
// 栅栏函数是在同一队列的任务,栅栏上方的任务先执行,当上方任务执行完毕再执行栅栏内部任务,最后执行栅栏下方任务
// 所以 1,2 先打印,1,2 的顺序不固定。接下来一定是打印 3
// 后面打印 5,是因为dispatch_async本身存在耗时操作,所以4一定在5后面
//异步栅栏函数
- (void)demo4{
    dispatch_queue_t queue = dispatch_queue_create("async", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    //栅栏函数
    dispatch_barrier_async(queue, ^{
        NSLog(@"3");
    });

    dispatch_async(queue, ^{
        NSLog(@"4");
    });

    NSLog(@"5");
}
// 输出结果:51234
// 异步栅栏函数,不会阻塞当前线程,而dispatch_async存在耗时,所以 5 先打印,剩下的顺序与 同步栅栏函数一致

Group(调度组)

统一管理多个异步任务,当调度组中所有任务完成时,发送通知,执行block

  • 使用
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self demo2];
}

-(void)demo2{
	dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue =  dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    
    //分部实现(常用于网络请求)
    dispatch_group_enter(group);//添加标识,没有添加只有移除的话会崩溃
    dispatch_async(queue, ^{
        NSLog(@"4 %@",[NSThread currentThread]);
        dispatch_group_leave(group);//移除标识,只有添加没有移除的话无法收到通知
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       NSLog(@"5 %@",[NSThread currentThread]);
    });
}

-(void)demo1{
	//1.创建组
    dispatch_group_t group = dispatch_group_create();
	//2.创建队列
    dispatch_queue_t queue =  dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    
    //3.将队列的任务加入到组里
    //调用这个方法,都会把这些任务添加到group里做一个标记
    //执行完以后,就移除这个标记
    //当group里标记为0,就通知你
    dispatch_group_async(group, queue, ^{
        NSLog(@"1 %@",[NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"2 %@",[NSThread currentThread]);
    });
    
    //4.当组里任务全部完成时,就会收到的通知,函数里的block才会执行
    //参数1:哪个组的通知
    //参数2:收到通知后把任务加入到哪个队列
    //参数3:收到通知需要执行的任务
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       NSLog(@"3 %@",[NSThread currentThread]);
    });
}

终端man加dispatch_group_async可看到函数内部实现(unix,linux,macOS系统级函数和部分C函数)

Semaphore(信号量)

用来控制访问资源的数量的标识,使用信号量告知系统按照我们指定的信号量数量来执行多个线程

  • 作用
    • 控制最大并发数
    • 线程锁(顺序请求)

在主线程中使用时需谨慎!容易造成主线程卡死

  • 使用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self demo7];
}

//用法1:控制最大并发数
-(void)demo5{
    uint value = 1;
    //传入一个>=0的value,创建信号量
    //最大并发数 = value + 1
    dispatch_semaphore_t sem = dispatch_semaphore_create(value);

    for (int i = 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(1);
            NSLog(@"任务%d:结束",i);
            //发送信号量,对信号量进行+1操作
            dispatch_semaphore_signal(sem);
        });
        //等待信号量
        NSLog(@"任务%d:开始",i);
        //等待信号量,对信号量进行-1操作
        //当信号量<0时,当前线程进入阻塞状态,直到信号量增加 或 等待时间结束为止
        //参数1:信号量;参数2:等待时间
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }
}

//用法2:线程锁(顺序请求)
//信号量+调度组
-(void)demo6{
    dispatch_group_t group = dispatch_group_create();
    //必须是串行队列
    //Q:串行队列不就顺序执行了吗,为什么还要用信号量?
    //A:串行队列只能保证请求被顺序发出,不能保证下一个请求在上个请求结果返回后再发出,所以这里可以用信号量来实现
    dispatch_queue_t queue =  dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t requestQ = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);

    for (int i = 0; i<5; i++) {
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            //模拟网络请求
            dispatch_async(requestQ, ^{
                sleep(1);
                NSLog(@"任务%d",i);
                dispatch_semaphore_signal(sem);
                dispatch_group_leave(group);
            });
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        });
    }

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"完成");
    });

    NSLog(@"主线");
    //输出结果:主线 任务0 任务1 任务2 任务3 任务4 完成
}
//信号量+栅栏函数
-(void)demo7{
    dispatch_queue_t queue =  dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t requestQ = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    for (int i = 0; i<6; i++) {
        dispatch_async(queue, ^{
            dispatch_async(requestQ, ^{
                sleep(1);
                NSLog(@"任务%d",i);
                dispatch_semaphore_signal(sem);
            });
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        });
    }

    dispatch_barrier_async(queue, ^{
        NSLog(@"完成");//注意:这里是子线程
    });

    NSLog(@"主线");
    //输出结果:主线 任务0 任务1 任务2 任务3 任务4 完成
}

After(延迟执行)

  • 延迟操作:dispatch_after这个函数默认是异步执行的
  • 使用和拆解
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

//    NSLog(@"开始");
//    
//    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//        NSLog(@"延迟操作中");
//    });
//    
//    NSLog(@"结束");
    
    //拆解
    NSLog(@"开始");
    
    //参数1:开始时间(DISPATCH_TIME_NOW 执行到延时操作函数时的时间,不是主线程结束后的时间,如果主线程任务执行时间超过该时间,那么在主线程任务结束立即执行)
    //参数2:间隔时间((int64_t)(2 * NSEC_PER_SEC)间隔 2 * 10亿纳秒 = 2秒,比NSTimer更加精确,因为它是用纳秒来算的)
    dispatch_time_t afterTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
    
//    dispatch_queue_t afterQueue = dispatch_get_main_queue();//主队列,所以延时操作会在主线程的任务执行完后才执行主队列中的任务
    
    dispatch_queue_t afterQueue = dispatch_get_global_queue(0, 0);//全局队列 
    
    dispatch_block_t afterBlock = ^{
        NSLog(@"延迟操作中,%@",[NSThread currentThread]);
    };
    
    //实际是一个异步任务,只是传了主队列,所以是主线程执行,如果传的是其他队列,就是创建新线程来执行
    dispatch_after(afterTime, afterQueue, afterBlock);
    
    [NSThread sleepForTimeInterval:4];
    
    NSLog(@"结束");
}

Once(一次执行)

  • dispatch_onec_t 内部有一把锁,能够保证线程安全,而且是苹果推荐使用的

典型应用场景:单例设计模式

  • 使用和仿写
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //GCD一次执行
    static dispatch_once_t onceToken;
    NSLog(@"%zd",onceToken);
    
    dispatch_once(&onceToken, ^{
        NSLog(@"一次执行");
    });
    NSLog(@"%zd",onceToken);
    //[self demo]
}

-(void)demo{
//仿写一次执行
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    static int num = 0;//static修饰编译时执行,运行后不再执行
    
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            //不加锁的话,在多线程的情况下,肯能还没等num变为-1,其他线程已经进入if中并打印了
            @synchronized (self) {
                if (num == 0) {
                    NSLog(@"一次执行");
                    num = -1;
                }
            }
        });
    }
}
  • 三种方式实现单例(线程安全)
    1. dispatch_once
    2. @synchronized
    3. initialize
@implementation NetworkManager
// 声明静态对象
static id instance;

//initialize 会在类第一次被使用时调用
//initialize 方法的调用是线程安全的
+ (void)initialize{
    // 只会开辟一次内存空间,只会被实例化一次
    instance = [[self alloc] init];
}

// 饿汉式单例
+ (instancetype)sharedManager{
    return instance;
}
@end