GCD

dispatch_sync / dispatch_async 同步与异步

dispatch_sync 提交 block 以供同步执行。这个接口会等到 block 执行结束才返回,所以不需要复制 block。

可以利用串行队列实现锁的功能。比如多线程写同一数据库,需要保持写入的顺序和每次写入的完整性

sync 和 async 区别
同步函数不论是并发队列还是串行队列,都不会开线程。一个任务开始后等待任务完成后返回
异步函数并发队列能开启多个线程,串行队列开启一个线程。异步方法会立即返回,不等待任务完成。

dispatch_queue_create 队列的创建或获取

GCD的所有队列都是先进先出的,它的队列主要分三种,主队列 main_queue、全局并发队列 global_queue、自定义队列
串行队列:队列中的任务必须在前一个任务结束后才能执行,包括 DISPATCH_QUEUE_SERIAL 和 主队列
并发队列:队列中的任务必须在前一个任务开始后才能执行,包括 DISPATCH_QUEUE_CONCURRENT 和 全局并发队列

// dispatch_queue_create
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);

// Serial dispatch queue,Executed on the main thread
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();

// How to get a global dispatch queue of high priority 
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

// How to get a global dispatch queue of default priority
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// How to get a global dispatch queue of low priority
dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

// How to get a global dispatch queue of background priority 
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

将上述的同步、异步、串行、并发四种方式组合成以下几种情况
同步串行:不开启新线程,串行执行队列中的任务
同步并发:不开启新线程,串行执行队列中的任务,并发失效
异步串行:开启新线程,串行执行队列中的任务
异步并发:开启新线程,并发执行任务

另外主队列中会有些特殊:
同步主队列:主线程调用会导致死锁;其他线程调用,不开启新线程,串行执行任务
异步主队列:不开启新线程,串行执行任务

例如,如果在调用该接口在当前 queue 上指派任务,就会导致 deadlock。以下是死锁的例子

- (void)deadLock1 {
    - (void)viewDidLoad {
        [super viewDidLoad];
        dispatch_sync(dispatch_get_main_queue(), ^{
            // 主线程中同步一个主队列,sync 是后加入到队列的,它阻止了主线程,同时 sync 等待主线程执行完毕后才能执行 block,主线程等待 sync 执行后才能执行,造成死锁
            NSLog(@"1");
        });
    }
}

- (void)deadLock2 {
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
    dispatch_sync(serialQueue,^{
        dispatch_sync(serialQueue,^{
            // 串行队列同步一个串行队列,死锁
            NSLog(@"1");
        });
    });
}

- (void)deadLock3 {
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
    dispatch_async(serialQueue, ^{
        // 串行队列里面同步一个串行队列就会死锁
        dispatch_sync(serialQueue, ^{
            NSLog(@"1");
        });
    });
}

全局并发队列可以设置优先级,对于 DISPATCH_QUEUE_PRIORITY_BACKGROUND 级别,文档解释说 Such a thread has the lowest priority and any disk I/O is throttled to minimize the impact on the system,也就是该级别最低,文件的读写等可以视情况使用该级别

更新一些关于锁的内容,如下

另外按照不再安全的 OSSpinLock文中所述,如果使用不同优先级别的并发队列,自旋锁是不安全的。

- (void)test {
    self.testString = @"1";

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"string is %@",self.testString);
        sleep(3);
        NSLog(@"string is %@",self.testString);
    });

    sleep(1);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        self.testString = @"2";
    });
}

输出

string is 1
string is 2

在异步线程中字符串被“意外”修改,如果不希望线程处理过程中 testString 被修改,可以使用加锁的方式处理

#import <libkern/OSAtomic.h>
- (void)test {
    OSSpinLock spinlock = OS_SPINLOCK_INIT;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&spinlock);
        NSLog(@"string is %@",self.testString);
        sleep(3);
        NSLog(@"string is %@",self.testString);
        OSSpinLockUnlock(&spinlock);
    });

    sleep(1);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        OSSpinLockLock(&spinlock);
        self.testString = @"2";
        OSSpinLockUnlock(&spinlock);
    });

OSSpinLockLock 起不到作用输出依然是

string is 1
string is 2

替换为 pthread_mutex

#import <pthread.h>
@interface ViewController () {
    pthread_mutex_t mutex;
}

- (void)test {
    self.testString = @"1"; 

    pthread_mutex_init(&mutex, NULL);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        pthread_mutex_lock(&mutex);
        NSLog(@"string is %@",self.testString);
        sleep(3);
        NSLog(@"string is %@",self.testString);
        pthread_mutex_unlock(&mutex);
    });

    sleep(1);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        pthread_mutex_lock(&mutex);
        self.testString = @"2";
        pthread_mutex_unlock(&mutex);
    });
}
- (void)dealloc {
    pthread_mutex_destroy(&mutex);
}

按照希望的输出

string is 1
string is 1

更多关于多线程和锁的内容点击这里

上述效果除了使用锁的方式实现,还可使用如下方法实现

- (void)test {
    self.testString = @"1";

    __weak __typeof(self.testString) weakString = self.testString;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        __strong __typeof(weakString) strongString = weakString;
        if(strongString) {
            NSLog(@"string is %@",strongString);
            sleep(3);
            NSLog(@"string is %@",strongString);
        }
    });

    sleep(1);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        self.testString = @"2";
    });
}

dispatch_set_target_queue 控制队列

由于自定义的队列在创建的时候不能指定优先级,它的优先级是默认的,通过这个方式可以设置其优先级。

// 第一个参数为要设置优先级的 queue,第二个参数是参照物,既将第一个 queue 的优先级和第二个queue的优先级设置一样。
dispatch_set_target_queue(serialQ, globalQ);

需要注意的是第一个参数 serialQ 是自定义的队列,不能是系统的队列(global / main),因为不能给系统的队列设置权限。通过上面的设置,serialQ 和 globalQ 一样的优先级。

如果异步执行几个串行队列,但是一个串行队列的目标队列被设置到其他串行队列的上,那么就失去了异步的意义。 如下

dispatch_queue_t targetQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", DISPATCH_QUEUE_SERIAL);  

dispatch_queue_t queue1 = dispatch_queue_create("com.example.gcd.queue1", DISPATCH_QUEUE_SERIAL);  
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.queue2", DISPATCH_QUEUE_SERIAL); 

dispatch_set_target_queue(queue1, targetQueue);  
dispatch_set_target_queue(queue2, targetQueue);  

dispatch_async(queue1, ^{  
    NSLog(@"blk1 in");  
    [NSThread sleepForTimeInterval:3.f];  
    NSLog(@"blk1 out");  
});  
dispatch_async(queue2, ^{  
    NSLog(@"blk2 in");  
    [NSThread sleepForTimeInterval:2.f];  
    NSLog(@"blk2 out");  
});  

结果输出

blk1 in  
blk1 out  
blk2 in  
blk2 out  

dispatch_after 延迟执行

有时候我们需要延迟一段时间后开始一个队列,例如等几秒钟然后做个动画或者给个提示

// “ull” is a C language literal specifying a type, is for “unsigned long long” type.
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC); 
dispatch_after(time, dispatch_get_main_queue(), ^{
     NSLog(@"waited at least three seconds.");
});

注意:这里不是在规定时间后就执行,因为主分发队列是在主线程的 RunLoop 中,例如如果 RunLoop 是 1/60 秒间隔,那么 Block 会在 3 + 1/60 秒后执行

dispatch_group_t 组任务

允许我们监听一组任务是否完成

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk2");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"done");
});
dispatch_release(group);

输出

blk1
blk2
blk0
done

上面的结果由于是异步执行所以顺序不一定,但是肯定会是最终才输出done或者说同步地等待一段时间看是否结束:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk2");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);

上面是等待所有的队列都执行完,下面这种方式可以等待一段时间

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
    // All the tasks that are associated with the dispatch group are finished
} else {
    //some tasks that are associated with the dispatch group are still running.
}

group 的异步线程如果嵌套另一个异步任务,要使用 dispatch_group_enter 和 dispatch_group_leave 保证所有任务执行完成

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
NSLog(@"start");
dispatch_group_async(group, queue, ^{
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:1.f];  
        NSLog(@"blk0");
    });
});
dispatch_group_notify(group, queue, ^{
    NSLog(@"done");
});

输出

start
done
blk0

为了得到预想的结果,将上述代码改为

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
NSLog(@"start");
dispatch_group_enter(group);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1.f];  
    NSLog(@"blk0");
    dispatch_group_leave(group);
});
dispatch_group_notify(group, queue, ^{
    NSLog(@"done");
});

输出

start
blk0
done

可以看到 dispatch_group_async 相当于 dispatch_group_enter 和 dispatch_group_leave 组合

dispatch_barrier_async 屏障

提交的任务会等它前面的任务执行结束才开始,然后它后面的任务必须等它执行完毕才能开始。例如读写数据库或者文件的操作,读写不能同时进行

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_async(queue, blk7_for_reading);

dispatch_barrier_async可以保证在同一时间,只有一个task在运行,blk_for_writing 时,其他读的 task 不会执行

使用 dispatch_barrier_async ,该函数只能搭配自定义队列 dispatch_queue_t 使用。不能使用: dispatch_get_global_queue ,否则 dispatch_barrier_async 的作用会和 dispatch_async 的作用一模一样。

dispatch_barrier_sync 和 dispatch_barrier_async 区别
dispatch_barrier_async 不用执行完内部方法即可返回,dispatch_barrier_sync 需要等到执行完内部方法才会返回, 类似 async 与 sync 的区别

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", DISPATCH_QUEUE_SERIAL);  
dispatch_async(queue, ^{
    NSLog(@"blk0");
});
dispatch_async(queue, ^{
    NSLog(@"blk1");
});
dispatch_barrier_async(queue,  ^{
    NSLog(@"block barrier");
});
NSLog("queue 1");
dispatch_async(queue, ^{
    NSLog(@"blk2");
});
NSLog("queue 2");

输出

blk1
blk0
block barrier
queue 1
queue 2
blk2

将上述代码的 dispatch_barrier_async 改为 dispatch_barrier_sync

输出

blk1
blk0
queue 1
queue 2
block barrier
blk2

dispatch_apply 执行某个代码片段若干次

dispatch_apply 相当于写了一个 for 循环,会等待 queue 中任务全部执行完毕后才会返回,否则一直等待。多线程将需要进行线程切换,线程同步等操作,会有一定的耗时,当单次任务执行的耗时较短,甚至小于线程切换的耗时时,使用 for 循环进行串行操作性能更优,否则应当使用 dispatch_apply,另外如果循环次数过多,可能会导致死锁或者线程爆炸,这是也应当使用 dispatch_apply。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(5, queue, ^(size_t index) {
  NSLog(@"%zu", index);
});
NSLog(@"done");

输出

4
1 
0
3
2
done

上面代码相当于

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

dispatch_suspend/dispatch_resume 挂起/重新开始一个队列

dispatch_suspend(queue);
dispatch_resume(queue);

dispatch_queue_set_specific / dispatch_queue_get_specific 设置标识符

用于在线程中查找指定的某一个线程

FMDB 中 给队列设置 specific 后,会在 inDatabase 中获取当前执行的队列是否是自己本身,避免死锁

- (void)inDatabase:(void (^)(FMDatabase *db))block {
    /* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
 * and then check it against self to make sure we're not about to deadlock. */
    FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
    assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
    // ..
}

dispatch_once

用来初始化全局的数据,保证整个应用程序生命周期中某段代码只被执行一次,线程安全的

static int initialized = NO;
if (initialized == NO) {
    // Initializing
    initialized = YES; 
}
// With the dispatch_once function, it will be modified as follows.
static dispatch_once_t pred;
dispatch_once(&pred, ^{
     // Initializing
});

dispatch_source_set_timer CGD 的 timer

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^() {
    // do something
});
dispatch_resume(timer);
// cancel timer
dispatch_source_cancel(timer);

只执行一次的话,不需要定时器可使用 dispatch_after

dispatch_semaphore

//创建信号量,信号量的初值如果小于0则会返回NULL
dispatch_semaphore_create(信号量值)

//等待降低信号量,如果信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1
dispatch_semaphore_wait(信号量,等待时间)

//提高信号量,这个函数会使传入的信号量 semaphore 的值加1;
dispatch_semaphore_signal(信号量)

NSThread / NSOperationQueue / GCD

NSThread 的使用

// 创建和启动
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(downLoadImage) object:nil];
[thread start];

// 创建并自动启动
[NSThread detachNewThreadSelector:@selector(downLoadImage) toTarget:self withObject:nil];

NSOperation 的使用

// 同步执行
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downLoadImage) object:nil];
[operation start];

// 异步执行
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downLoadImage) object:nil];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];

- (void)downLoadImage {
    // ... Download
    // After done, refresh UI
}

NSOperation 可以调用 start 方法来执行任务,但默认是在当前调用的线程(如果没有创建则是主线程)中同步执行的;如果将 NSOperation 添加到 NSOperationQueue 中,系统会自动异步执行 NSOperation 中的操作,异步方法只需要重写 start 方法,则当你添加 operationQueue 后,系统自动调用 start 方法

对比 GCD 用法

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // downLoadImage
    dispatch_async(dispatch_get_main_queue(), ^{
       //  refresh UI
    });
});

区别:

  • 【优先级】,NSOperationQueue 可以被设置优先级。我们可以让一个 Operation 依赖于另一个 Operation,这样的话尽管两个 Operation 处于同一个并行队列中,但前者会直到后者执行完毕后再执行;GCD 全局并发队列不能设置具体哪个队列依赖哪个队列,但是可以设置优先级别,
  • 【KVO】,NSOperationQueue 支持 KVO,意味着我们可以观察任务的执行状态,如 operations,operationCount,maxConcurrentOperationCount,suspended,name。
  • 【取消操作】,NSOperation 可以监听一个 Operation 是否完成或取消,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而 GCD 没法停止已经加入 queue 的 Block(其实是有的,但需要许多复杂的代码)
  • 【继承】,我们能够对 NSOperation 进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将 block 任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。

参考文章

GCD 源码
多线程要注意的安全问题

请我喝汽水儿