iOS开发中一共有四种多线程方案:Pthreads、NSThread、GCD、NSOperation。
GCD和NSOperation对比:
1. GCD更接近底层,处理速度更快;NSOperation是GCD的高级抽象。
2. GCD仅支持FIFO队列,而NSOperationQueue中的队列可以设置优先级、依赖关系、最大并发数。
3. NSOperationQueue支持KVO,我们可以观察任务的执行状态。
任务就是 GCD 的 block 中需要执行的那段代码,有两种执行方式:同步执行(sync)和异步执行(async),两者的主要区别是:是否等待队列的当前任务执行结束,是否具备开启新线程的能力。
队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,有两种队列:串行队列(SDQ)和并发队列(CDQ)。两者的主要区别是:执行顺序不同,以及开启的线程数不同。
并发原理:放到并发队列的任务,GCD 也会按照 FIFO 的方式取出来,它取出来一个就会放到一个线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。
// 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("SerialQueue", DISPATCH_QUEUE_SERIAL);
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 获取主队列(所有放在主队列中的任务,都会放到主线程中执行)
dispatch_queue_t queue = dispatch_get_main_queue();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建同步执行任务
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 创建异步执行任务
dispatch_async(queue, ^{
// 这里放异步执行任务代码
);
任务和队列有四种组合方式,但是只有异步+并发才能实现并发执行任务
有时我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完成后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。
// 添加队列组
- (void)addGroupNotify {
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
// 添加任务1
[NSThread sleepForTimeInterval:2];
});
dispatch_group_async(group, queue, ^{
// 添加任务2
[NSThread sleepForTimeInterval:2];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等异步任务1、任务2都执行完成后,回到主线程执行下面任务
// 更新UI等
});
(不常用等待方法)
// 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
}
严格来说:dispatch_after 函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。这个时间并不是绝对准确的,但是可以大致延迟执行任务。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2.0秒后异步追加任务代码到主队列,并开始执行
});
我们在创建单例时经常使用 dispatch_once 函数,它能保证某段代码在程序运行过程中只被执行1次,并且在多线程的环境下,dispatch_once也可以保证线程安全。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
有时我们需要异步执行两组操作,而且在第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组分割开来,这就需要用到dispatch_barrier_(a)sync 方法在两个操作组间形成栅栏。原理如下图:
dispatch_barrier_sync 代码后边的任务直到 dispatch_barrier_sync 执行完才被追加到队列中;
dispatch_barrier_async 不用等代码执行完,后边的任务也会被追加到队列中,只是Block不会被执行。
Dispatch Semaphore,是持有计数的信号量。计数为0时等待,不可通过,计数大于或等于1时,不等待,可通过,且计数减1。
在实际开发中主要用于:保持线程同步,将异步执行任务转换为同步执行任务;保证线程安全,为线程加锁。
Dispatch Semaphore 提供了三个函数:
线程同步:有个场景:异步执行耗时任务,并使用异步执行的结果进行一些其他的操作。也就是说,将异步执行任务转换为同步执行任务。比如说:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。
- (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 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"semaphore---begin");
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 当信号量为0时就会一直等待(阻塞所在线程),否则正常执行,信号量减1。
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { //如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
// 模拟延时
[NSThread sleepForTimeInterval:0.2];
} else {
// 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 当信号量加1
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
NSTimer 定时器是在 RunLoop 中实现的,由于RunLoop在处理各种任务,所以会造成计时器不够准确,有时候会相对慢一些。使用 GCD 的计时器方法会让计时器变得更准确,而且 GCD 不受 RunLoop 的 Mode 影响。
// 倒计时
- (IBAction)countdown:(id)sender {
// 倒计时时间
__block NSInteger countTime = 59;
// 获取全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器(倒计时间、开始时间、间隔)
dispatch_source_set_timer(countTime,DISPATCH_TIME_NOW,1.0*NSEC_PER_SEC, 0);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
if(time <= 0) {
// 倒计时结束,关闭定时器
dispatch_source_cancel(timer);
// 回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
// 设置按钮的样式
[self.sendBtn setTitle:@"重新发送" forState:UIControlStateNormal];
self.sendBtn.userInteractionEnabled = YES;
});
} else {
// 回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
// 设置按钮的样式
[self.sendBtn setTitle:[NSString stringWithFormat:@"重新发送(%ld)",countTime]; forState:UIControlStateNormal];
// 在倒计时状态下 用户交互关闭,防止再次点击 button 再次倒计时
self.sendBtn.userInteractionEnabled = NO;
});
countTime--;
}
});
// 启动定时器
dispatch_resume(timer);
}
参考:
iOS详细介绍GCD
关于iOS多线程