多线程相关的概念
- 时间片轮转调度算法:是目前操作系统中大量使用的线程管理方式,大致就是操作系统会给每个线程分配一段时间片(通常 100 ms 左右),这些线程都被放在一个队列中,CPU 只需要维护这个队列,当队首的线程时间片耗尽就会被强制放到队尾等待,然后提取下一个队首线程执行
- 原子操作:“原子”一般指最小粒度,不可分割;原子操作也就是不可分割,不可中断的操作
- 临界区 :每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问
- 忙等(busy-waiting): 试图进入临界区的线程,占着 CPU 而不释放的状态
- 睡眠(sleep-waiting):试图进入临界区的线程,会进入睡眠状态,主动让出时间片,不会再占着 CPU 而不释放
- 上下文切换(Context Switch):当线程进入睡眠(sleep-waiting)的时候,cpu的核心会进行上下文切换,将该线程置于等待队列中,而其他线程就会继续执行任务,上下文切换需要花费时间
- 锁的拥有者(Lock Ownership):如果锁没有拥有者,则当它被某一条线程获取时,其他任意一条线程都可以对它进行解锁;如果锁只能有单一的拥有者,则当它被某一条线程获取时,只有这条线程可以对它进行解锁;如果锁可以有多个拥有者,则它可以同时被某多条线程获取
- 死锁:指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。一般在获得锁的线程中再次进行加锁就会发生死锁
- 饥饿(Starvation):指一个进程一直得不到资源
线程同步方案
要保证线程安全,就必须要线程同步,而在iOS中线程同步的方案有:
- 原子操作
- 信号量
- GCD串行队列
- 锁
原子操作
在 iOS 中,原子操作可以保证属性在单独的 setter
或者 getter
方法中是线程安全的,但是不能保证多个线程对同一个属性进行读写操作时,可以得到预期的值,也就是原子操作不保证线程安全,例如:
// 共享资源name
@property (copy, atomic) NSString *name;
// 初始化
self.name = @"A";
// 线程2进行写操作,是原子操作,不可以分割的
self.name = @"B";
// 线程3进行写操作,是原子操作,不可以分割的
self.name = @"C";
// 线程4进行读操作,是原子操作,不可以分割的,但这时候存在三种可能
self.name == @"A";
self.name == @"B";
self.name == @"C";
Objective-C 的原子操作
在 Objective-C 中,可以在设置属性的时候,使用 atomic
来设置原子属性,保证属性 setter
、getter
的原子性操作,底层是在 getter
和 setter
内部使用 os_unfair_lock
加锁
@property (copy, atomic) NSString *name;
Swift 的原子操作
在 Swift 中,原生没有提供原子操作,可以使用 DispatchQueue
的同步函数来达到同样的效果
class Person {
// 创建一个队列
let queue = DispatchQueue(label: "Person")
// 私有化需要原子操作的属性
private var _name: String = ""
// 向外界暴露的属性,把它的 get 和 set 方法都设置为同步操作,实际上是对 _name 进行操作,这样就可以间接的对 name 进行原子操作
var name: String {
get {
return queue.sync {
_name
}
}
set {
return queue.sync {
_name = newValue
}
}
}
}
信号量(Semaphore)
- 信号量(semaphore)是非负整型变量,在初始化时设置一个值
value
,用来控制线程并发访问的最大数量,当 value == 1 的时候,就可以实现线程同步 - 信号量有两个原子操作:
wait()
、signal()
-
wait()
:当 value > 0,就将 value 减 1 并马上返回;当 value == 0,那当前线程就会睡眠,直到其他线程调用signal()
把 value 加 1,当前线程恢复,然后将 value 减 1 并返回 -
signal()
:将 value 加 1 - 如果初始化的时候 value 为 0, 那么调用
wait()
方法就会马上挂起当前线程,直到别的线程调用了signal()
方法,才会恢复
-
- 被阻塞线程会进入睡眠状态
- 信号量不支持递归
- 信号量没有拥有者(Owner),意味着可以在一条线程进行
wait()
操作,在另外一条线程进行signal()
操作 - 在 iOS 中用
dispatch_semaphore
来使用信号量,也是 GCD 用来同步的一种方式
// 初始化一个值为 5 的信号量,可以同时有 5 条线程访问临界区,其他线程则进入睡眠状态
dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);
// wait
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 临界区...
// signal
dispatch_semaphore_signal(semaphore);
GCD串行队列
- 使用 GCD 串行队列也可以达到同步的效果,配合
sync
函数就是在当前线程执行任务 - GCD 串行队列有单一的拥有者,就是一个串行队列有对应的线程
dispatch_queue_t queue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 临界区...
});
锁
OSSpinLock
-
OSSpinLock
是一种"自旋锁"。自旋锁是一种特殊互斥锁,当一个线程需要获取自旋锁时,如果该锁已经被其他线程占用,那么会一直去请求锁,进入忙等(busy-waiting)
状态,所以会一直占用 CPU - 由于自旋锁在等待锁的时候线程一直处于忙等状态,而不用进入睡眠,所以不用进行上下文切换,自旋锁的效率远高于互斥锁
- 自旋锁适用于
- 预计线程等待锁的时间很短
- 临界区经常访问,但竞争情况很少发生
- 自旋锁不安全,会出现优先级反转问题:如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于
忙等
状态从而占用大量 CPU 时间片。此时低优先级线程无法与高优先级线程争夺 CPU 时间片,从而导致完成任务而无法释放锁 - 在 iOS 10 及以上被废弃
#import
OSSpinLock lock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&lock);
// 临界区...
// 解锁
OSSpinLockUnlock(&lock);
os_unfair_lock
-
os_unfair_lock
用于取代不安全的OSSpinLock
,iOS 10 开始支持,当一条线程等待锁的时候会进入睡眠,不再消耗 CPU 时间,当其他线程解锁以后,操作系统会激活线程 -
os_unfair_lock
有单一的拥有者 - 这是一种不公平锁。在公平锁中,多个线程同时竞争这个锁的时候, 会考虑公平性尽可能的让不同的线程获得锁,这样会频繁进行上下文切换,牺牲性能。而在不公平锁中,系统为了减少上下文切换,当前拥有锁的线程有可能会再次获得锁,但这样做可能会让其他线程等待更长时间,造成饥饿
#import
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 加锁
os_unfair_lock_lock(&lock);
// 临界区...
// 解锁
os_unfair_lock_unlock(&lock);
互斥锁
- 互斥锁是可以看作是一种特殊的信号量,当一条线程等待锁的时候会进入睡眠状态
- 互斥锁阻塞的过程分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入睡眠状态,让出时间片,此时线程就不占用 CPU 时间片,等锁可用的时候,这个线程会立即被唤醒
pthread_mutex
pthread
表示 POSIX thread
,是 POSIX 标准的 unix 多线程库,定义了一组跨平台的线程相关的API。pthread_mutex
是一种用 C 语言实现的互斥锁,有单一的拥有者
#import
// 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 动态初始化
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 加锁
pthread_mutex_lock(&mutex);
// 临界区...
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁锁
pthread_mutex_destroy(&_mutex);
NSLock
-
NSLock
是以 Objective-C 对象的形式对pthread_mutex
的封装,属性为PTHREAD_MUTEX_ERRORCHECK
,它会损失一定性能换来错误提示 -
NSLock
比pthread_mutex
略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响 -
NSLock
有单一的拥有者
NSLock *lock = [[NSLock alloc] init];
// 加锁
[lock lock];
// 临界区...
// 解锁
[lock unlock];
递归锁
递归锁是一种特殊互斥锁。递归锁允许单个线程在释放之前多次获取锁,其他线程保持睡眠状态,直到锁的所有者释放锁的次数与获取它的次数相同。递归锁主要在递归迭代中使用,但也可能在多个方法需要单独获取锁的情况下使用。
pthread_mutex(Recursive)
pthread_mutex
支持递归锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE
即可,它有单一的拥有者
#import
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 加锁
pthread_mutex_lock(&_mutex);
// 临界区...
// 在同一个线程中可以多次获取锁
// 解锁
pthread_mutex_unlock(&_mutex);
// 销毁锁
pthread_mutex_destroy(&_mutex);
NSRecursiveLock
NSRecursiveLock
是以 Objective-C 对象的形式对 pthread_mutex(Recursive)
的封装,它有单一的拥有者
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
// 加锁
[lock lock];
// 临界区...
// 在同一个线程中可以多次获取锁
// 解锁
[lock unlock];
@synchronized
-
@synchronized
是对pthread_mutex(Recursive)
的封装,所以它支持递归加锁 - 需要传入一个 Objective-C 对象,可以理解为把这个对象当做锁来使用
- 实际上它是用
objc_sync_enter(id obj)
和objc_sync_exit(id obj)
来进行加锁和解锁 - 底层实现:在底层存在一个全局用来存放锁的哈希表(可以理解为锁池),对传入的对象地址的哈希值作为key,去查找对应的递归锁
-
@synchronized
额外还会设置异常处理机制,性能消耗较大 -
@synchronized
有单一的拥有者
@synchronized(lock) {
// 临界区...
}
条件锁
条件锁是一种特殊互斥锁,需要条件变量(condition variable) 来配合。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。条件锁是为了解决 生产者-消费者模型
pthread_mutex – 条件锁
pthread_mutex
配合 pthread_cond_t
,可以实现条件锁,其中 pthread_cond_t
没有拥有者
#import
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &NULL);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 初始化条件变量
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
// 消费者
- (void)remove {
// 加锁
pthread_mutex_lock(&mutex);
// 先判断某个条件
if (self.data.count == 0) {
// 如果不满足条件,则等待,具体是释放锁,用条件变量来阻塞当前线程
// 当条件满足的时候,条件变量唤醒线程,再用原来的锁加锁
pthread_cond_wait(&cond, &mutex);
}
[self.data removeLastObject];
// 解锁
pthread_mutex_unlock(&mutex);
}
// 生产者
- (void)add
{
// 加锁
pthread_mutex_lock(&mutex);
[self.data addObject:@"Test"];
// 信号
// 条件变量唤醒阻塞的线程
pthread_cond_signal(&cond);
// 广播
// pthread_cond_broadcast(&cond);
// 解锁
pthread_mutex_unlock(&mutex);
}
// 销毁
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
NSCondition
NSCondition
是以 Objective-C 对象的形式对 pthread_mutex
和 pthread_cond_t
进行了封装,NSCondition
没有拥有者
NSCondition *condition = [[NSCondition alloc] init];
// 消费者
- (void)remove
{
[condition lock];
if (self.data.count == 0) {
// 如果不满足条件,则等待,具体是释放锁,用条件变量来阻塞当前线程
// 当条件满足的时候,条件变量唤醒线程,再用原来的锁加锁
[condition wait];
}
[self.data removeLastObject];
[condition unlock];
}
// 生产者
- (void)add
{
[condition lock];
[self.data addObject:@"Test"];
// 信号
// 条件变量唤醒阻塞的线程
[condition signal];
[condition unlock];
}
NSConditionLock
NSConditionLock
是对 NSCondition
的进一步封装,可以设置条件变量的值。通过改变条件变量的值,可以使任务之间产生依赖关系,达到使任务按照一定的顺序执行,它有单一的拥有者(不确定)
// 初始化设置条件变量的为1,如果不设置则默认为0
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:1];
// 消费者
- (void)remove
{
// 当条件变量为2的时候加锁,否则等待
[lock lockWhenCondition:2];
[self.data removeLastObject];
// 直接解锁
[lock unlock];
}
// 生产者
- (void)add
{
// 直接加锁
[lock lock];
[self.data addObject:@"Test"];
// 解锁并让条件变量为2
[lock unlockWithCondition:2];
}
读写锁
读写锁是一种特殊互斥锁,提供"多读单写"的功能,多个线程可以同时对共享资源进行读取,但是同一时间只能有一条线程对共享资源进行写入
pthread_rwlock
pthread_rwlock
有多个拥有者
#import
// 初始化
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
// 读操作
- (void)read {
pthread_rwlock_rdlock(&lock);
// 临界区...
pthread_rwlock_unlock(&lock);
}
// 写操作
- (void)write
{
pthread_rwlock_wrlock(&lock);
// 临界区...
pthread_rwlock_unlock(&lock);
}
// 销毁
- (void)dealloc
{
pthread_rwlock_destroy(&lock);
}
GCD 的 Barrier函数
- GCD 的 Barrier 函数也可以实现"多读单写"的功能
- Barrier 函数的作用是:等其他任务执行完毕,才会执行任务自己的任务;会执行完毕自己的任务,才会继续执行其他任务
- 这个函数传入的并发队列必须是自己通过
dispatch_queue_cretate
创建的,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于 dispatch_async 函数的效果
dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 读
});
dispatch_async(queue, ^{
// 读
});
dispatch_barrier_async(queue, ^{
// 写
});
dispatch_async(queue, ^{
// 读
});
性能
性能从高到底分别是:
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- GCD 串行队列
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
总结:
-
OSSpinLock
和os_unfair_lock
性能很高,但是一个是已经废弃,一个是低级锁,苹果不建议使用低级锁 -
dispatch_semaphore
和pthread_mutex
也具有不错的性能,NSLock
是pthread_mutex
的封装,性能上接近 - 个人建议在 Objective-C 中直接使用面向对象的
NSLock
,而在 Swif t中使用GCD 串行队列
参考文章
苹果官方文档
白夜追凶,揭开iOS锁的秘密
起底多线程同步锁(iOS)
深入理解 iOS 开发中的锁