基本概念
时间片轮转调度算法
系统给每个线程分配一个时间片(100ms),当线程分配的时间片消耗完,会被移动到队列尾部。
原子操作
操作不可再分割,不可终端
临界区
访问临界资源的代码块。每次只允许一个进程进入临界区。
忙等
试图进入临界区的线程,占着CPU而不释放的状态
互斥锁
如果一个线程无法获取互斥量,这个线程将会被挂起。当其他线程释放互斥锁的时候,系统将会唤醒被挂起的线程。互斥锁会使得线程阻塞。阻塞有两个阶段。第一个阶段将空转,不停去尝试获取锁。第二个阶段将会进入 waiting 阶段,不再消耗 CPU资源,直到被系统唤醒。
自旋锁
自旋锁不会被挂起,将会不停消耗资源去获取锁。
NSLock && pthread_mutex_lock 互斥锁
NSLock
是 Objective-C 以对象的形式暴露给开发者的一种锁。
pthread_mutex_lock
是POSIX互斥锁
NSLock
比pthread_mutex_lock
慢的原因是经过的方法调用(runtime)。但是 runtime 会对方法调用进行缓存,所以多次调用不会有性能影响。
NSRecursiveLock 递归锁
递归锁可以在同一个线程被多次获得,解锁的条件就是加锁的次数和解锁的次数一样多。
NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE
NSConditionLock && NSCondition
条件锁,看到用的地方不多。
1 |
|
和 NSLock 类似,不同的地方是加锁和解锁只有在符合条件的时候。
根据白夜追凶,揭开iOS锁的秘密一文。
NSCondition的底层是通过条件变量(Condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。其组成结构是
信号量可以一定程度上替代 Condition,但是互斥锁不行。在以上给出的生产者-消费者模式的代码中, pthread_cond_wait 方法的本质是锁的转移,消费者放弃锁,然后生产者获得锁,同理, pthread_cond_signal 则是一个锁从生产者到消费者转移的过程。
这样做存在的问题在于,在等待 another_lock 之前, 生产者有可能先执行代码, 从而释放了 another_lock。也就是说,我们无法保证释放锁和等待另一个锁这两个操作是原子性的,也就无法保证“先等待、后释放 another_lock” 这个顺序。
用信号量则不存在这个问题,因为信号量的等待和唤醒并不需要满足先后顺序,信号量只表示有多少个资源可用,因此不存在上述问题。然而与 pthread_cond_wait 保证的原子性锁转移相比,使用信号量似乎存在一定风险(暂时没有查到非原子性操作有何不妥)。
不过,使用 Condition 有一个好处,我们可以调用 pthread_cond_broadcast 方法通知所有等待中的消费者,这是使用信号量无法实现的。
NSCondition封装了互斥锁和信号量,将 lock/signal/wait 封装起来提供给开发者。
1 | [condition wait];让当前线程处于等待状态 |
需要注意的是: NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,经测试, NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。
NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)
总结步骤如下
- 锁定条件对象
- 判断是否可以进入临界区
- 加入不可以进入,调用
wait/waitUntilDate:
来阻塞线程。待方法返回时,继续检查是否可以执行下面的任务,直到可以进入或超时。 - 执行任务
- 释放条件对象
@synchronized
@synchronized
是最常用一种加锁方式。传入一个对象作为锁定标记可以起到锁的作用。
@synchronized 将传入的对象地址的 hash 值作为 key,将SyncData作为 value。
传递给 @synchronized指令的对象是用于区分受保护块的唯一标识符。如果在两个不同的线程中执行上述方法,则在每个线程上为anObj参数传递一个不同的对象,每个线程都会锁定并继续处理而不被另一个线程阻塞。但是,如果在两种情况下传递相同的对象,则其中一个线程将首先获取锁,另一个会阻塞,直到第一个线程完成临界区。
需要注意的问题有几个
- 你调用 sychronized 的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。
- 如果在 sychronized 内部对象被释放或被设为 nil 看起来都 OK。不过这没在文档中说明,所以我不会再生产代码中依赖这条。注意不要向你的 sychronized block 传入 nil!这将会从代码中移走线程安全。你可以通过在 objc_sync_nil 上加断点来查看是否发生了这样的事情。
dispatch_semaphore
dispatch_semaphore
是 GCD用来实现同步的一种方式,即信号量。
1 | dispatch_semaphore_create(long value); |
dispatch_semaphore 和 NSCondition 类似,都是一种基于信号的同步方式, dispatch_semaphore 能保存发送的信号。 dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。
dispatch_semaphore_create(1)
会创建一个dispatch_semaphore_t
的信号量,初始值为1。这里必须传如非负数的,否则会返回 NULL。只有创建为1的时候才能作为锁,否则可能会有多个线程访问临界区。
dispatch_semaphore_wait (signal, overTime); 方法会判断 signal 的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。
dispatch_semaphore_signal(signal); 发送信号,如果没有等待的线程接受信号,则使 signal 信号值加一(做到对信号的保存)
总结一下
- 如果应用在递归场景的时候,我们有2种选择,一个是 NSRecursiveLock,另一个是 pthread_mutex(recursive)。追求高性能的话,优先选择 pthread_mutex,苹果已对 pthread_mutex进行过多次优化。
- 普通场景可以用@synchronized,但是加锁解锁效率最慢。建议是用
dispatch_semaphore
或者pthread_mutex