GCD

Mar 26, 2015


GCD是什么

GCD全称:Grand Central Dispatch,是苹果公司为多核、并行运算提出的解决方案。由C语言编写,提供了非常强大的函数。它可以自动利用设备多核,自动管理线程的生命周期(创建线程、调度任务、销毁线程)。

我们可以使用GCD并发(同时)执行多个任务,而不用手动管理线程,不需要编写管理线程的任何代码。

基本概念

任务:执行什么操作,如数据运算、下载等。

队列:用来存放任务,一个队列可以存放多个任务,并且任务取出遵循队列的FIFO(First in,First out)原则:先进先出,后进后出。

死锁:两个任务相互等待导致两个任务都不执行。比如正在一个运行一个队列,并调用dispatch_sync添加新的任务到当前队列,就会造成死锁。

GCD使用

同步异步

GCD中有两个用来执行任务的函数,分别为同步和异步。

同步执行
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block)

该函数执行后,不会立即返回,而是等待任务(block)执行完才返回,会阻塞当前线程

也就是说,该函数不会开启新的线程,所有的任务在当前线程中同步执行。

异步执行
dispatch_async(dispatch_queue_t queue, dispatch_block_t block)

该函数执行后,会立即返回,不会阻塞当前线程。

也就是说,该函数会开启新的线程,异步执行任务。

队列

队列分为两种:并发队列和串行队列。

并发队列

并发队列(Concurrent Dispatch Queue):队列中任务会并发(同时)执行(自动开启多个线程同时执行任务)。

GCD默认已经提供了全局并发队列,供整个程序使用,无需创建,获取全局队列的函数:

dispatch_get_global_queue(long identifier, unsigned long flags)

第一个参数long identifier指定队列的优先级,包括4种,分别为:

#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

第二个参数:unsigned long flags无用,传0即可。

也可以手动创建并发队列:

dispatch_queue_t concurrentQueue = dispatch_queue_create("name", DISPATCH_QUEUE_CONCURRENT);
串行队列

串行队列(Serial Dispatch Queue):队列中的任务一个接一个地执行(一个任务执行完毕后,再执行下一个任务,单线程)。

手动创建串行队列的函数:

dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)

第一个参数const char *label表示队列的名称。

第二个参数dispatch_queue_attr_t attr表示队列的属性,传入DISPATCH_QUEUE_CONCURRENT表示并发队列,传入NULL或者DISPATCH_QUEUE_SERIAL表示串行队列。

主队列

主队列是串行队列的一种,获得主队列的函数如下:

dispatch_get_main_queue()
组队列

我们通常会碰到这样的需求:分别异步执行两个耗时操作,两个操作都完成后,再执行下一个步骤(比如刷新UI等)。这时候,我们可以考虑组队列,使用方法示例如下:

//组队列
dispatch_group_t group_queue = dispatch_group_create();
dispatch_group_async(group_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //任务1
});
dispatch_group_async(group_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //任务2
});
dispatch_group_notify(group_queue, dispatch_get_main_queue(), ^{
    //任务1和任务二执行完毕后,回到主线程执行这里的代码。
});

或者,对于多个下载/上传http请求都完成后,做进一步刷新操作:

- (void)groupQueue{
    dispatch_group_t groupQueue = dispatch_group_create();
    for (int i = 0 ;  i < 5; i++) {
        dispatch_group_enter(groupQueue);
        [self httpRequestComplete:^{
            //http请求完成
            NSLog(@"1个http请求完成了");
            dispatch_group_leave(groupQueue);
        }];
    }
    dispatch_group_notify(groupQueue, dispatch_get_main_queue(), ^{
        NSLog(@"5组http请求都完成了");
    });
}
//模拟后台http请求操作
- (void)httpRequestComplete:(void(^)())block{
    if (block) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            //http请求耗时5s
            [NSThread sleepForTimeInterval:5];
            dispatch_async(dispatch_get_main_queue(), ^{
                 block();
            });
        });
    }
}

总结

执行方式 主队列 全局队列 自定义队列
同步(sync) 主线程/串行 当前线程/串行 当前线程/串行
异步(async) 主线程/串行 再开N条线程/并行 再开1条线程/串行

使用举例

//并发队列,同时执行过个任务 任务1任务2同时执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"任务1 --- %@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"任务2 --- %@", [NSThread currentThread]);
});

//串行队列,任务1任务2依次执行
dispatch_queue_t queue =dispatch_queue_create("name", NULL);
dispatch_async(queue, ^{
    NSLog(@"任务1 --- %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"任务2 --- %@", [NSThread currentThread]);
});

其他用法

延迟执行

GCD可以方便执行延迟代码:

dispatch_after(dispatch_time_t when,
	dispatch_queue_t queue,
	dispatch_block_t block);

第一个参数dispatch_time_t when指定在什么时候执行任务

第二个参数dispatch_queue_t queue指定在哪个线程执行任务

第三个参数dispatch_block_t block用于存放需要延迟执行的任务。

一次性代码

保证某段代码在程序运行过程中只会执行一次:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
   //code 这里的代码整个程序运行过程中只会执行一次
});

static关键字创建单例线程是不安全的,而使用用dispatch_once可以非常方便创建单例,并且线程是安全的。

读写数据安全

信号量

信号量类似于标志位,但是它可以让线程等待。在GCD中可以方便控制线程的个数、执行时间等。

比如:异步上传、下载多个任务然后所有任务执行完毕后再进行刷新UI操作。如果我们用AFNetworking,http请求本身就是异步的,求情完成只能在block中回调。这时候,如果我们使用信号量,在请求发出之前创建信号量,成功之后再使得信号量+1,从而根据信号量来判断是否异步请求是否完成。

创建信号量:

dispatch_semaphore_create(long value)

该函数用于创建一个信号量值为value的信号,传入参数为long类型。特别的,参数value必须大于或等于0,否则会返回NULL。

信号量+1:

long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);

该函数使传入的信号量dsema的值+1。

该函数返回值为long类型。当返回值为0表示当前并没有线程等待其处理的信号量,其处理的信号量值+1即可。当返回值不为0时,表示当前有(1个或多个)线程等待其处理的信号量,那么该函数唤醒一个等待的线程(根据线程优先级进行唤醒)。

信号量-1:

long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

该函数会使传入的信号量dsema的值-1。

如果dsema信号量大于0,那么该函数所处的线程就会继续执行下面的语句,并且将信号量值-1

如果dsema信号量小于等于0,那么该函数就会阻塞当前线程等待timeout,如果等待期间dsema的值被dispatch_semaphore_signal+1,那么该函数就会继续执行并且信号量-1;如果等待期间没有信号量一直小于等于0,那么等到timeout,其所处的线程就会自动执行后面语句。

该函数返回值为long类型。当其返回0时表示在timeout之前,该函数所处的线程被成功唤醒;当其返回不为0时,表示timeout发生。

dispatch_time_t timeout 的设置:

有两个宏可以使用:

#define DISPATCH_TIME_NOW (0ull) // 当前时间
#define DISPATCH_TIME_FOREVER (~0ull) //遥远的未来

也可以手动创建:

dispatch_time_t
dispatch_time(dispatch_time_t when, int64_t delta);

比如:

dispatch_time_t  t = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));

信号量的释放

当信号量release的时候,信号量的value值必须大于等于初始化时创建的value,否则会导致崩溃。 参考文章

atomic

使用atomic关键词可以保证数据的读写安全,同一时间数据对象属性只能进行一个读或者一个写操作,保证了数据安全。但是,使用atomic会大大降低代码效率。另外,atomic是使用self进行加锁的,如果有多个属性都使用了atomic关键词,那么这些使用了atomic关键字的属性,所有的读写操作都是按照顺序执行,同一时间只能进行一个读或写操作,这样做效率非常低,也没有必要。

串行队列

创建一个串行队列,并将set和get方法的具体代码放在该串行队列中,这样就可以保证所有对属性的访问都是同步的。但是这种方法只可以实现单读、单写,并非最优。

dispatch_barrier_async

对于数据读写安全,最优的解是:读取可以并发进行,写入只能串行进行,且写入的时候不能进行读取操作。这是时候,我们使用dispatch_barrier_async非常方便。

在队列中barrier任务必须单独执行,不能和其他block并行。也就是说,我们可以通过该方法,让并发队列的某个任务单独执行,而不和其他任务并行。

示例代码:

//声明一个属性,为并发队列 @property (nonatomic, strong) dispatch_queue_t concurrentQueue;并在对象初始化的时候进行初始化。
-(void)setName:(NSString *)name{
    //barrier的任务必须单独执行,不能并发
    //写入操作单独执行,不并发
    dispatch_barrier_async(_concurrentQueue, ^{
       _name = name;
    });
}

-(NSString *)name{
    //读取操作,可以并发
    __block NSString *tempName;
    dispatch_sync(_concurrentQueue, ^{
        tempName = _name;
    });
    return tempName;
}

锁总结

自旋锁:自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。线程一直是running(加锁——>解锁),死循环检测锁的标志位,。

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。

1. OSSpinLockLock 自旋锁(已废弃)

如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。

2. os_unfair_lock

Replacement for the deprecated OSSpinLock. Does not spin on contention but waits in the kernel to be woken up by an unlock.

As with OSSpinLock there is no attempt at fairness or lock ordering, e.g. an unlocker can potentially immediately reacquire the lock before a woken up waiter gets an opportunity to attempt to acquire the lock. This may be advantageous for performance reasons, but also makes starvation of waiters a possibility.

3. pthread_mutex互斥锁,mutex: Mutual exclusion

4. NSLock/NSRecursiveLock

5. NSCondition

条件变量常与互斥锁同时使用,达到线程同步的目的:条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。

6. NSConditionLock

以及条件加锁、解锁,可以控制线程启动顺序、依赖。

7. 关键字锁

objc_sync_enter(_ obj: Any!) 
objc_sync_exit(_ obj: Any!)

8. pthread_rwlock 读写锁

Mutex & DispatchSemaphore

Mutex:

  • lock & unLock 必须在同一线程
  • 对单一资源保护。

Semaphore:

  • lock & unLock 任意线程
  • 调度线程,一些线程生产,同时另一些线程消费,可以让生产和消费保持合乎逻辑的执行顺序。(生产消费模式)
  • 如:A、B两个任务都执行结束,才可进行C任务。