本文章属于原创,转载请注明出处
参考资料:
底层并发API
NSOperation和GCD
iOS高级编程
编写iOS代码的52个有效方法
这篇文章有点乱,当你读到这篇文章时,可以选择不同的小块来进行阅读,因为除了前面对并行串行,队列的介绍是基础以外,别的部分几乎没什么关联
pthread
- 一种使用极其复杂的线程实现方式
NSThread
- 对pthread的一种封装
以上,无论是pthread还是NSTread使用起来都相当复杂,在GCD出现后很少会用到
一些概念
并发 & 并行 & 串行
并发是一种现象,表示多个任务同时发生,需要被处理,比如很多人排队等待检票
并行是一种技术,表示同时处理多个任务的技术,比如很多人在不同的窗口等待检票
串行是并行的反义词,表示任务必须按顺序,一个一个执行
同步 & 异步
同步是指等一件事做完了再做另一件
异步是指,函数执行完之后立即返回,但内部执行的任务稍后完成
资源共享
两个线程同时访问一个资源会造成竞态条件,当一个线程正在写入一个资源,而另一个线程尝试访问该资源时,可能会造成访问的资源访问的半新不旧,保护该条件发生的方法是互斥
互斥锁
互斥是说同一时刻,只允许一个线程访问特定的资源,为保证这一点,每个想要访问共享资源的线程,首先要获得一个共享资源的互斥锁,一定一个线程完成了操作,就释放该互斥锁,这样另外一个线程就可以进行访问了
什么是GCD
- GCD以Block为基本单位,一个Block中的代码可以分为一个任务,任务可以说是执行一个Block,GCD有两个重要的概念,“队列"和"执行方式”
- 执行Block的过程其实是把Block放到合适的队列里,并选择合适的执行方式去执行Block的过程
- 队列可以分为三种
- 串行队列:先进入队列的任务先出队列,每次只执行一个任务
- 并发队列:先进入队列的任务先出,但是可以一起执行
- 主队列:特殊的串行队列,队列的任务一定在主线程上执行
- 执行方式有两种
- 同步执行
- 异步执行
// GCD简单的使用方式
dispatch_async (queue, ^{
// 长时间的处理
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程的执行
}
})
// '^' 字表示其中用到了Block
CPU一次只能执行一个命令,不能分开并行的执行两个命令,这种模式即为线程,"上下文切换"使线程看起来像是并行执行
多线程编程可以防止长时间的处理妨碍主线程的运行,会妨碍主线程中叫RunLoop的主循环的运行,导致用户界面,应用程序的画面长时间停滞
GCD里的死锁
以下代码会引起死锁
viewDidLoad() {
super.viewDidLoad()
let mainQueue = dispatch_get_main_queue()
let block = { print(NSTread.currentThread) }
dispatch.sync(mainQueue, block)
}
- 在这个例子里,主队列在执行dispatch_sync,随后队列里新增一个任务Block,因为主队列是同步队列,所以Block要等dispatch_sync执行完才能执行,但是dispatch_sync是同步派发,要等Block执行完才能结束,所以造成了死锁
- 只有在确定了任务的执行顺序时,必要的情况下,才使用sync函数。一般情况下都使用async
GCD的API
Dispatch Queue
dispatch_async (queue, ^{
// 执行的任务
});
使用Block语法,定义想执行的任务,通过dispatch_async函数,追加复制在变量queue的Dispatch Queue中,这样就可以使指定的Block在另一个线程中执行
Dispatch Queue是执行处理的等待队列,通过dispatch_async函数等,在Block语法中,记述想执行的处理并追加到Dispatch Queue中,Dispatch Queue按照追加顺序执行处理FIFO
有两种Dispatch Queue
- Serial Dispatch Queue等待当前执行的进程执行
- Concurrent Dispatch Queue不等待,并行执行,多个线程同时运行
dispatch_queue_create
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.exmpl.gcd.mySerialDispatchQueue", NULL);
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT");
// 第一个参数指定名称
// 第二个参数指定为一个Concurrent queue
- 多个线程使用一个资源时,会造成数据竞争,这样可以使用Serial Dispatch Queue,要避免生成大量的Serial Dispatch Queue
- 当想并行执行不发生数据竞争等问题的处理时,使用Concurrent Dispatch Queue,对于Concurrent Dispatch Queue来说,不管生成多少,XNU内核只使用有效管理的线程
通过dispatch_queue_crete生成的Dispatch Queue不能使用ARC
dispatch_release(mySerialDispatchQueue);
dispatch_retain(myConcurrentDispatchQueue);
Main Dispatch Queue/Global Dispatch Queue
- Main Dispatch Queue
系统提供Main Dispatch Queue和Global Dispatch Queue,不用自己提供
因为主线程只有一个,所以Main Dispatch Queue是Serial Dispatch Queue,追加到主线程的处理在主线程的RunLoop中执行,由于在主线程执行,因此要将用户界面更新等一些必须在主线程中执行的方法在Main Dispatch Queue中使用
- Global Dispatch Queue
Global Dispatch Queue是所有程序都能使用的Concurrent Dispatch Queue,有四个优先级
各Dispatch Queue的获取方法如下
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
对Main Dispatch Queue和Global Dispatch Queue执行retain,release操作不会有任何变化,强烈建议大多数情况下使用默认的优先级
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
dispatch_async(dispatch_get_main_queue(), ^ {
});
});
dispatch_set_target_queue
- 改变Dispatch Queue的优先级
dispatch_after
在指定时间后追加处理到Dispatch Queue
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
// ull是数字字面量,表示unsigned long long
真正的追加时间会有所延迟
第一个参数是指定时间用的dispatch_time_t类型的值,该函数从第一个参数开始,从第二个参数后结束
Dispatch Group
在追加到Dispatch Queue中的多个处理全部结束后执行结束处理
如下,追加三个Block到Global Dispatch Queue里,当这些Block全部执行完毕,就会执行Main Dispatch Queue中结束处理用的Block
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY, 0);
dispatch_group_t group = dispatch_group_create();
// 与dispatch_async相同,但group为第一个参数,指定的Block属于制定的Dispatch Group
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")l });
dispatch_release(group);
// 执行release操作
向Global Dispatch Queue即Concurrent Dispatch Queue追加处理,多个线程并行执行,所以追加处理的执行顺序不定,执行时会发生变化,但done一定是最后输出的
dispatch_barrier_async
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
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);
当在blk3和blk4之间写入时,会发生数据竞争,此时使用dispatch_barrier_async函数,会让blk3之后的写入操作执行完,再执行blk4
dispatch_sync
-
async意味着非同步,dispatch_async函数不做任何等待
-
sync会等待Block执行结束
dispatch_sync函数可以说是简易版的dispatch_group_wait函数,下面的源码都会引起死锁
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{ NSLog(@"Hello?"); });
该源码在Main Dispatch Queue即主线程中执行指定的Block,并等待结束,而其实主线程正在执行执行源代码,所以无法执行追加到Main Dispatch Queue上的Block
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
dispatch_sync(queue, ^{ NSLog(@"Hello?); });
});
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue"), NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{ NSLog(@"Hello?"); });
});
dispatch_apply
dispatch_apply是dispatch_sync函数和Dispatch Group的关联API,该函数按照指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束
dispatch_queue_t queue = dispatch_queue_get_global_queue(DISPATCH_QUEUE_PRIORITY, 0,);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
NSLog(@"done");
因为在Global Dispatch Queue中执行处理,所以各个处理的执行时间不定,但是输出结果中最后的done必定在最后的位置,因为dispatch_apply函数会等待全部处理执行结束
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^ {
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"done");
});
});
dispatch_suspend / dispatch_resume
dispatch_suspend会挂起Dispatch Queue,适当的时候再恢复
dispatch_suspend(queue);
dispatch_resume会恢复指定的Dispatch Queue
dispatch_resume(queue);
Dispatch Semaphore
- 同一资源在被不同线程访问的时候,可能被无意间改变,得到期望以外的值,而防止这种情况发生的方法之一就叫做信号量
- Serial Dispatch Queue 和 dispatch_barrier_async函数可避免这类问题,不过也可以更小粒度的控制
信号量的机制就相当于,在租房子的时候,没有租到房子的人是不能住进去的,所以需要上锁,此时信号量就相当于最多能租几间房子,当能出租的房间数量为0的时候,想要租房间的人就要在租房名单上排队等候,等到房间里租房的人不再续租了才可以入住,入住顺序就是名单上的顺序,而不再入住的人还要告知房东,并安排接下来的人入住,不能一走了事
// 创建信号量(买好房子,有几个房间)
var semaphore = dispatch_semaphore_create(2)
// wait表示一直等待直到信号量的值大于等于1,执行这个方法后会把信号量的值 -1
// 入住一个人
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
// 把此时信号量的值加1
// 有人不再续租了,可入住的房间空出来
dispatch_semaphore_signal(semaphore)
var semaphore = dispatch_semaphore_create(1)
let queue = dispatch_queue_create("com.gcd", DISPATCH_QUEUE_CONCURRENT)
var array = [Int]()
for i in 0...10000 {
dispatch_async(queue, { () -> in
// 线程执行到这里,如果信号值为1,那么wait方法返回1,开始执行接下来的操作
// 同时,因为信号量变成了0,其它执行到这里的线程都必须等待
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
// 执行了wait方法之后,信号量的值变成了0,可以进行接下来的操作
// 这时候其它线程都会等待wait方法返回
// 可以修改Array的线程在任意时刻都只有一个
array.append(i)
// 排他操作结束,要记得调用signal方法,把信号值加一
// 这样,如果有别的线程在等待wait函数返回,就由最先等待的线程先执行
dispatch_semaphore_signal(semaphore)
}
}
dispatch_once
保证在程序执行中只执行一次处理,单例模式,在生成单例对象时使用,完全线程安全
+ (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
要确保dispatch_once_t被声明为static或者别的全局作用域,也不要把dispatch_once_t作为成员变量
使用dispatch_once可以简化代码并彻底保证线程安全,开发者无需担心加锁或同步,所有问题都由GCD在底层处理,由于每次调用都必须产出完全相同的标记,所以标记要申明成static,该变量在static的作用域中,可以保证编译器每次执行sharedInstance方法都会调用这个变量,不会产生新的,dispatch_once也更高效