iOS--锁的学习
锁的介绍
主要参考了iOS多线程安全-13种线程锁🔒
在iOS开发中,锁是一种用于管理并发访问共享资源的机制。多个线程可以同时访问共享资源,但在某一时刻只允许一个线程对资源进行读取或修改,以避免数据竞争和不一致性。
锁的主要目的是确保在给定时间内只有一个线程可以进入被锁定的代码块或临界区。当一个线程获得了锁并进入临界区时,其他尝试获取锁的线程会被阻塞,直到该线程释放锁。这样可以保证在临界区内的代码只会被一个线程执行,从而避免竞态条件和数据不一致性的问题。
在iOS中,有多种锁机制可供选择,每种锁机制都有其特定的适用场景和性能特征。一些常见的锁机制包括:
互斥锁(Mutex):例如NSLock、pthread_mutex等。互斥锁提供了基本的线程同步机制,通过对临界区加锁和解锁来确保同一时间只有一个线程可以访问。
递归锁(Recursive Lock):例如NSRecursiveLock、pthread_mutex_recursive等。递归锁允许同一线程多次获取锁,避免了死锁情况。
条件锁(Condition Lock):例如NSConditionLock、pthread_cond等。条件锁在某些特定条件下才允许线程继续执行,否则线程会被阻塞。
信号量(Semaphore):例如dispatch_semaphore。信号量是一种更为通用的同步机制,可以用于控制多个线程的并发数量。
自旋锁(Spin Lock):例如os_unfair_lock、OSSpinLock(已被弃用)等。自旋锁在等待锁时不会将线程阻塞,而是通过循环忙等的方式尝试获取锁,适用于短时间内锁的竞争概率较低的情况。
线程安全
线程安全是指在多线程环境下,程序能够正确地处理共享数据并保证数据的一致性和正确性。
在多线程编程中,多个线程可以同时访问和修改共享的数据。如果没有适当的同步机制和线程安全的设计,可能会出现以下问题:
- 竞态条件(Race Condition):多个线程同时对同一数据进行读写操作,导致结果依赖于线程执行的顺序,从而得到不确定的结果。
- 数据竞争(Data Race):多个线程同时访问和修改共享数据,其中至少一个是写操作,导致未定义的行为。
这里用一个比较经典的例子,具体参考iOS多线程安全-13种线程锁🔒
objectivec
//卖票演示
- (void)ticketTest{
self.ticketsCount = 50;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self sellingTickets];
}
});
}
}
//卖票
- (void)sellingTickets{
int oldMoney = self.ticketsCount;
sleep(.2);
oldMoney -= 1;
self.ticketsCount = oldMoney;
NSLog(@"当前剩余票数-> %d", oldMoney);
}
运行结果如图:
这里是一个典型的数据竞争问题,当多个线程同时对一个共享数据进行读写操作是很有可能会发生数据竞争,为了避免这种情况,我们往往通过线程同步来保证线程安全。实现线程同步的方法很多,锁就是一种比较常见的用法 ;
锁的分类
锁是实现线程同步和保证数据访问的一种机制,在多线程编程中起着重要的作用。根据实现方式和特性,锁可以分为几个不同的分类:
- 互斥锁(Mutex
Lock):互斥锁是最常见和基本的锁类型,用于实现互斥访问共享资源。它提供了两个状态:锁定(被一个线程持有)和解锁(无线程持有)。只有一个线程可以持有互斥锁,其他线程在获取锁之前会被阻塞。互斥锁可以防止多个线程同时访问临界区,从而保证数据的一致性。 - 读写锁(Read-Write
Lock):读写锁也称为共享-独占锁,它允许多个线程同时读取共享资源,但只允许一个线程进行写操作。读操作之间不会互斥,而读操作与写操作之间是互斥的。读写锁适用于读多写少的场景,可以提高并发性能。 - 自旋锁(Spin
Lock):自旋锁是一种忙等待锁,它使用循环来反复检查锁是否被释放,而不是将线程阻塞。当一个线程发现自旋锁被其他线程持有时,它会一直循环等待,直到自旋锁可用。自旋锁适用于临界区很小且短时间内会释放的情况。 - 条件变量(Condition
Variable):条件变量用于线程之间的等待和通知机制。它与互斥锁结合使用,允许一个线程等待某个条件成立并在条件满足时通知其他线程。等待条件的线程会释放互斥锁,以便其他线程可以访问临界区。条件变量常用于生产者-消费者问题等场景。 - 屏障(Barrier):屏障用于多个线程在某个点上等待,直到所有线程都到达该点后才继续执行。它可以用于同步多个线程的操作,确保线程在达到某个阶段之前都等待其他线程。
自旋锁和互斥锁
自旋锁(Spin Lock):
自旋锁是一种忙等待锁,它使用循环来反复检查锁的状态,直到获取到锁为止。自旋锁的特点如下:
- 自旋锁适用于临界区很小且短时间内会释放的情况,因为它需要持续占用CPU资源来进行自旋等待。
- 自旋锁不会导致线程的阻塞和切换,所以对于短时间内能够获取到锁的情况,自旋锁的性能较好。
- 自旋锁不会改变线程的调度顺序,因此在高优先级线程和低优先级线程之间,可能会导致优先级反转的问题。
互斥锁是一种常见的线程同步机制,用于实现互斥访问共享资源。在iOS中,常用的互斥锁有NSLock和NSRecursiveLock。互斥锁的特点如下:
- 互斥锁允许一个线程持有锁,其他线程在获取锁之前会被阻塞,从而实现对临界区的互斥访问。
- 互斥锁会导致线程的阻塞和切换,所以对于长时间占用锁或等待时间不确定的情况,互斥锁的性能可能较差。
- 互斥锁可以解决优先级反转的问题,因为它可以改变线程的调度顺序,让优先级高的线程优先获取锁。
互斥锁:线程会从sleep(加锁)------>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
自旋锁:线程一直是running(加锁------>解锁),死循环检测锁的标志位,机制不复杂。
对比
互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。
OSSpinLock
OSSpinLock是iOS旧版本中提供的一种自旋锁(Spin Lock)实现。它通过忙等待的方式来获取锁,并且不会导致线程的阻塞和切换。不过需要注意的是,自旋锁在iOS 10之后已被标记为废弃,因为它存在优先级反转和性能问题。推荐使用更现代的互斥锁实现,如os_unfair_lock和pthread_mutex等。
使用时需要导入头文件#import <libkern/OSAtomic.h>
os_unfair_lock
os_unfair_lock是iOS 10及以上版本引入的一种互斥锁(Mutex Lock)实现,用于实现线程同步和保护共享资源的访问。相比于旧版本中的自旋锁(OSSpinLock),os_unfair_lock采用了更高级的算法来解决优先级反转和性能问题。
os_unfair_lock的特点如下:
- os_unfair_lock是一种互斥锁,只能由一个线程持有,其他线程在获取锁之前会被阻塞。
- 它可以解决优先级反转问题,确保高优先级线程在等待锁时能够优先获取。
- os_unfair_lock的实现采用了更高级的算法,避免了自旋等待,减少了不必要的CPU开销。
- os_unfair_lock的性能通常比旧版自旋锁(OSSpinLock)更好,尤其在高并发情况下。
需要导入头文件#import <os/lock.h>
objectivec
@implementation ViewController {
os_unfair_lock _lock ;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self ticketTest] ;
}
//卖票演示
- (void)ticketTest{
self.ticketsCount = 50;
//os_unfair_lock lock = OS_UNFAIR_LOCK_INIT ;
_lock = OS_UNFAIR_LOCK_INIT ;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self sellingTickets];
}
});
}
}
//卖票
- (void)sellingTickets{
os_unfair_lock_lock(&_lock) ;
int oldMoney = self.ticketsCount;
sleep(.2);
oldMoney -= 1;
self.ticketsCount = oldMoney;
NSLog(@"当前剩余票数-> %d", oldMoney);
os_unfair_lock_unlock(&_lock) ;
}
@end
要注意一下,通常不建议将 os_unfair_lock 直接作为属性来使用。os_unfair_lock 是一个结构体,不具备 Objective-C 对象的特性,因此不适合直接作为属性。
pthread_mutex
pthread_mutex 是 POSIX 线程库中提供的一种互斥锁(mutex)类型,用于实现线程同步和互斥访问共享资源。
主要的 pthread_mutex 函数如下:
- pthread_mutex_init:用于初始化互斥锁,并指定互斥锁的属性。可以使用 NULL 作为第二个参数来使用默认属性。
- pthread_mutex_destroy:用于销毁互斥锁。
- pthread_mutex_lock:加锁操作,将互斥锁加锁,如果互斥锁已经被其他线程锁定,则当前线程被阻塞直到互斥锁可用。
- pthread_mutex_trylock:尝试加锁操作,如果互斥锁已经被其他线程锁定,则该操作会立即返回,而不会阻塞当前线程。
- pthread_mutex_unlock:解锁操作,释放互斥锁,允许其他线程获得锁。
objectivec
@implementation ViewController {
// os_unfair_lock _lock ;
pthread_mutex_t _lock ;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self ticketTest] ;
}
//卖票演示
- (void)ticketTest{
self.ticketsCount = 50;
//os_unfair_lock lock = OS_UNFAIR_LOCK_INIT ;
//初始化锁属性
pthread_mutexattr_t attr ;
pthread_mutexattr_init(&attr) ;
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) ;
pthread_mutex_init(&_lock, &attr) ;//第二个参数可以为NULL,表示使用默认属性PTHREAD_MUTEX_NORMAL
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self sellingTickets];
}
});
}
}
//卖票
- (void)sellingTickets{
// os_unfair_lock_lock(&_lock) ;
pthread_mutex_lock(&_lock) ;
int oldMoney = self.ticketsCount;
sleep(.2);
oldMoney -= 1;
self.ticketsCount = oldMoney;
NSLog(@"当前剩余票数-> %d", oldMoney);
// os_unfair_lock_unlock(&_lock) ;
pthread_mutex_unlock(&_lock) ;
}
@end
pthread_mutex的属性
pthread_mutexattr_settype:设置互斥锁的类型。常见选项有:
- PTHREAD_MUTEX_NORMAL:普通互斥锁,不支持递归。
- PTHREAD_MUTEX_RECURSIVE:递归互斥锁,允许同一线程多次获得锁。
- PTHREAD_MUTEX_ERRORCHECK:错误检查互斥锁,会检测多次锁定同一互斥锁的错误。
- PTHREAD_MUTEX_DEFAULT:根据实现的默认类型设置互斥锁。
NSLock
**NSLock是对mutex普通锁的封装。**pthread_mutex_init(mutex, NULL);
NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
objectivec
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name
@end
objectivec
@implementation ViewController {
// os_unfair_lock _lock ;
// pthread_mutex_t _lock ;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self ticketTest] ;
}
//卖票演示
- (void)ticketTest{
self.ticketsCount = 50;
//os_unfair_lock lock = OS_UNFAIR_LOCK_INIT ;
//初始化锁属性
// pthread_mutexattr_t attr ;
// pthread_mutexattr_init(&attr) ;
// pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) ;
// pthread_mutex_init(&_lock, &attr) ;//第二个参数可以为NULL,表示使用默认属性PTHREAD_MUTEX_NORMAL
self.lock = [[NSLock alloc] init] ;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self sellingTickets];
}
});
}
}
//卖票
- (void)sellingTickets{
// os_unfair_lock_lock(&_lock) ;
// pthread_mutex_lock(&_lock) ;
[self.lock lock] ;
int oldMoney = self.ticketsCount;
sleep(.2);
oldMoney -= 1;
self.ticketsCount = oldMoney;
NSLog(@"当前剩余票数-> %d", oldMoney);
// os_unfair_lock_unlock(&_lock) ;
// pthread_mutex_unlock(&_lock) ;
[self.lock unlock] ;
}
//- (instancetype)init {
// self = [super init];
// if (self) {
// _lock = [[NSLock alloc] init];
// }
// return self;
//}
//- (void)lock {
// [_lock lock];
//}
//
//- (void)unlock {
// [_lock unlock];
//}
@end
NSRecursiveLock
NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致
objectivec
- (void)ticketTest{
self.ticketsCount = 50;
//os_unfair_lock lock = OS_UNFAIR_LOCK_INIT ;
//初始化锁属性
// pthread_mutexattr_t attr ;
// pthread_mutexattr_init(&attr) ;
// pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) ;
// pthread_mutex_init(&_lock, &attr) ;//第二个参数可以为NULL,表示使用默认属性PTHREAD_MUTEX_NORMAL
self.lock = [[NSRecursiveLock alloc] init] ;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self sellingTickets];
}
});
}
}
//卖票
- (void)sellingTickets{
// os_unfair_lock_lock(&_lock) ;
// pthread_mutex_lock(&_lock) ;
[self.lock lock] ;
int oldMoney = self.ticketsCount;
sleep(.2);
oldMoney -= 1;
self.ticketsCount = oldMoney;
NSLog(@"当前剩余票数-> %d", oldMoney);
// os_unfair_lock_unlock(&_lock) ;
// pthread_mutex_unlock(&_lock) ;
[self.lock unlock] ;
}
NSCondition
NSCondition是对mutex和cond的封装,更加面向对象 ;
objectivec
@interface NSCondition : NSObject <NSLocking>
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name;
@end
- NSCondition 类继承自 NSObject 并遵循 NSLocking 协议。
- wait 方法用于使当前线程等待,直到接收到信号。 waitUntilDate: 方法使当前线程等待,直到接收到信号或者指定的日期超时。
- signal 方法用于唤醒一个等待中的线程。
- broadcast 方法用于唤醒所有等待中的线程。
- name 属性是一个可选的字符串,用于标识 NSCondition 对象的名称。
objectivec
// 线程1
// 删除数组中的元素
- (void)__remove
{
[self.condition lock];
if (self.data.count == 0) {
// 等待
[self.condition wait];
}
[self.data removeLastObject];
NSLog(@"删除了元素");
[self.condition unlock];
}
// 线程2
// 往数组中添加元素
- (void)__add
{
[self.condition lock];
sleep(1);
[self.data addObject:@"Test"];
NSLog(@"添加了元素");
// 信号
[self.condition signal];
[self.condition unlock];
}
我的想法是这类对象有两种线程等待的方式,一种是wait--signal方式,一种是lock--unlock方式 ;
NSConditionLock
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
objectivec
@interface NSConditionLock : NSObject <NSLocking> {
- (instancetype)initWithCondition:(NSInteger)condition;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end
dispatch_semaphore
ispatch_semaphore 是 Grand Central Dispatch (GCD) 中的一个信号量机制,用于控制并发访问共享资源的线程数量。这个之前GCD中讲过了 ;
dispatch_queue
使用GCD的串行队列也可以实现线程同步的
@synchronized
@synchronized 是 Objective-C 中用于实现线程安全的关键字之一。它提供了一种简单的方式来保护共享资源,以防止多个线程同时访问和修改该资源。
使用 @synchronized 的基本语法如下:
objectivec
@synchronized (object) {
// 同步的代码块
// ...
}
@synchronized 关键字的工作原理是,在进入 @synchronized 代码块之前,它会获取 object 的互斥锁,以确保同一时间只有一个线程可以进入该代码块。当线程离开 @synchronized 代码块时,它会释放锁,允许其他线程继续进入。
关于@synchronized 的底层原理可以去深入看看,但平时使用只要知道它会在代码块开头加锁,代码块末尾解锁就行了
atomic
在Objective-C中,atomic是一种属性修饰符,用于指定属性的原子性。当一个属性被声明为atomic时,它意味着在对该属性进行读取和写入操作时,将会保证操作的原子性。
原子性是指一个操作要么完全执行,要么完全不执行,不存在执行过程中被中断的情况。在多线程环境下,使用atomic修饰符可以确保对属性的读取和写入操作是线程安全的。
当属性被声明为atomic时,编译器会自动生成相关的同步代码,以确保对属性的访问是原子操作。这样可以防止多个线程同时对同一个属性进行读写操作,避免出现数据竞争和不一致的情况。
需要注意的是,虽然atomic提供了一定程度的线程安全性,但它并不能完全保证线程安全。在高并发的多线程环境中,仍然需要额外的同步机制来确保数据的一致性和正确性。
相比之下,另一个属性修饰符nonatomic则表示属性是非原子的,它不会提供自动的线程安全保护。在多线程环境下,使用nonatomic修饰符可以提高性能,但需要开发人员自行处理线程安全问题。
总之,atomic属性修饰符用于指定属性的原子性,提供一定程度的线程安全性。在多线程环境中,可以使用atomic来确保对属性的读取和写入操作是原子的,但仍需要注意其他的线程安全问题。
pthread_rwlock:读写锁
objectivec
//初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);
//读加锁
pthread_rwlock_rdlock(&_lock);
//读尝试加锁
pthread_rwlock_trywrlock(&_lock)
//写加锁
pthread_rwlock_wrlock(&_lock);
//写尝试加锁
pthread_rwlock_trywrlock(&_lock)
//解锁
pthread_rwlock_unlock(&_lock);
//销毁
pthread_rwlock_destroy(&_lock);
pthread_rwlock 是 POSIX 线程库中的读写锁(read-write lock)机制。它提供了一种多读单写的并发访问控制机制,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。
使用 pthread_rwlock 可以实现以下功能:
- 多读单写控制:多个线程可以同时获取读锁(读取共享资源),但只有一个线程可以获取写锁(修改共享资源)。
- 读写互斥:在写锁被持有期间,其他线程无法获取读锁,以确保数据一致性。
dispatch_barrier_async
GCD中也有 ;
锁的性能比较
性能从高到低排序
1、os_unfair_lock
2、OSSpinLock
3、dispatch_semaphore
4、pthread_mutex
5、dispatch_queue(DISPATCH_QUEUE_SERIAL)
6、NSLock
7、NSCondition
8、pthread_mutex(recursive)
9、NSRecursiveLock
10、NSConditionLock
11、@synchronized