五类锁
锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。
不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待
锁总共分为五类:
- 互斥锁
- 自旋锁
- 读写锁
- 条件锁
- 递归锁
互斥锁
在一个多线程环境中,互斥锁可以确保同一时刻只有一个线程能够访问临界区,即共享资源的代码段。
工作原理
-
在访问共享资源之前进行加锁,访问完成后解锁。
-
加锁后,任何其他试图加锁的线程会被阻塞,直到当前线程解锁。
-
解锁时,如果有1个以上的线程阻塞,那么所有该锁上的线程变为就绪状态,第一个就绪的加锁,其他的又进入休眠。
自旋锁
跟互斥类似,只是自旋锁不会引起调用者睡眠, 因为资源被占用的时候, 会一直循环检测锁是否被释放(CPU不能做其他的事情)
节省了唤醒睡眠线程的内核消耗(在加锁时间短暂的情况下会大大提高效率)
在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等
自旋锁和互斥锁的区别
例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而自旋锁则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
- 互斥锁:当一个线程试图获取一个已经被其他线程持有的锁时,这个线程会被阻塞,直到锁被释放。这通常涉及线程的上下文切换,线程被挂起并让出处理器时间给其他线程或进程。
- 自旋锁:当一个线程试图获取一个已经被其他线程持有的锁时,这个线程不会被阻塞,而是会持续检查(“自旋”)锁的状态,直到锁被释放。这避免了上下文切换的开销,但在锁持有时间较长时可能导致浪费CPU资源。
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。
**总结:**自旋锁在等待锁时会持续检查锁状态而不放弃CPU时间,适合短期等待;互斥锁在等待时会让出CPU,进入阻塞状态,直至锁可用,适用于长期等待或需要避免CPU空转的场景。
读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
写的时候:读写都等待;
读的时候:写等待,读无需等待
条件锁
通常与互斥锁一起使用,条件锁允许一个或多个线程等待某个特定条件满足时才继续执行,而互斥锁则用于保护临界区,防止多个线程同时访问共享资源。
工作原理:
- 等待条件:一个线程调用条件锁的
wait()
方法时,它会释放互斥锁并阻塞自己,直到满足某个条件。在wait()
被调用期间,其他线程可以获取互斥锁并访问临界区。 - 唤醒线程:当条件满足时,通常是由另一个线程调用条件锁的
notify_one()
或notify_all()
方法来唤醒一个或所有等待的线程。被唤醒的线程将重新尝试获取互斥锁。 - 检查条件:被唤醒的线程重新获取互斥锁后,需要再次检查条件是否仍然满足,因为可能有其他线程改变了条件状态。如果条件不满足,线程可能需要再次调用
wait()
。
递归锁
跟互斥类似, 但是允许同一个线程在未释放锁前,多次加锁, 不会引发死锁
九种锁
在iOS中有九种锁:
- OSSpinLock
- os_unfair_lock
- Dispatch_semaphore
- pthread_mutex
- NSLock
- NSConditon
- NSRecursiveLock
- NSConditionLock
- @synchronize
下面是9种锁的耗时排行:
OSSpinLock 自旋锁
OSSpinLock是iOS旧版本中提供的一种自旋锁(Spin Lock)实现。它通过忙等待的方式来获取锁,并且不会导致线程的阻塞和切换。自旋锁在iOS 10之后已被标记为废弃,因为它存在优先级反转和性能问题,可以使用os_unfair_lock
替代
os_unfair_lock
是一种轻量级的互斥锁,特别适合于那些锁持有时间较短,且锁竞争不激烈的情况。相比于传统的互斥锁(如 pthread_mutex
),os_unfair_lock
提供了更低的上下文切换开销
当一个线程试图获取一个已经被占用的锁时,它不会排队等待,而是立即返回,这意味着它可能在其他等待的线程之前再次尝试获取锁。这种机制在低竞争环境下可以减少锁的获取时间,但在高竞争环境下可能导致某些线程长期无法获取锁。
注意⚠️:尽管它在某些方面表现出类似自旋锁的行为(例如,在锁未被其他线程持有时,尝试获取锁的线程不会立即被阻塞),但它本质上仍然是一种互斥锁,因为它确保了同一时刻只有一个线程可以拥有锁。
dispatch_semaphore
- semaphore叫做”信号量”
- 信号量的初始值,可以用来控制线程并发访问的最大数量
- 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
//表示最多开启5个线程
dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
运行下面代码:
@interface dispatch_semaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation dispatch_semaphoreDemo
- (instancetype)init
{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)otherTest
{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
- (void)test
{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@end
每隔一秒出现一次打印。虽然我们同时开启20个线程,但是一次只能访问一条线程的资源
pthread_mutex
mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
- 1、初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
- 2、初始化锁
// 初始化锁
pthread_mutex_init(mutex, &attr);
- 3、初始化锁结束以后,销毁属性
// 销毁属性
pthread_mutexattr_destroy(&attr);
- 4、加锁解锁
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);
- 5、销毁锁
pthread_mutex_destroy(&_mutex);
可以不初始化属性,在传属性的时候直接传
NULL
,表示使用默认属性PTHREAD_MUTEX_NORMAL
。pthread_mutex_init(mutex, NULL);
确保在调用 pthread_mutex_lock()
后总是调用相应的 pthread_mutex_unlock()
,并且在解锁前不要释放调用线程。否则,可能会导致死锁
NSLock
NSLock是对mutex
普通锁的封装
NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
@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
使用方法:
@interface NSLockDemo()
@property (nonatomic,strong) NSLock *lock;
@end- (void)test{
[self.lock lock];
[self doSomeThing];
[self.lock unlock];
}
NSRecursiveLock
NSRecursiveLock是对mutex
递归锁的封装,API跟NSLock基本一致
@interface RecursiveLockDemo()
@property (nonatomic,strong) NSRecursiveLock *lock;
@end
- (void)test{
[self.lock lock];
[self doSomeThing];
[self.lock unlock];
}
NSCondition
NSCondition是对mutex
和cond
的封装,更加面向对象
@interface NSCondition : NSObject <NSLocking> {
- (void)wait;//用于使当前线程等待,直到接收到信号。
- (BOOL)waitUntilDate:(NSDate *)limit;//使当前线程等待,直到接收到信号或者指定的日期超时。
- (void)signal;//唤醒一个等待中的线程。
- (void)broadcast;//唤醒所有等待中的线程。
@property (nullable, copy) NSString *name //用于标识 NSCondition 对象的名称
@end
NSCondition
的 wait
、signal
和 broadcast
方法会自动获取锁,所以你应该在调用这些方法之前先释放锁。这是因为,当你调用 wait
方法时,线程会释放锁并进入等待状态;当你调用 signal
或 broadcast
方法时,被唤醒的线程会在继续执行前自动获取锁。
#import <Foundation/Foundation.h>NSCondition *queueCondition = [[NSCondition alloc] init];
NSMutableArray *queue = [NSMutableArray array];void producer() {for (int i = 0; i < 10; i++) {[NSThread sleepForTimeInterval:1]; // 模拟耗时操作[queueCondition lock];[queue addObject:[NSNumber numberWithInt:i]];NSLog(@"Producer added item: %d", i);[queueCondition signal]; // 唤醒一个等待的消费者[queueCondition unlock];}
}void consumer() {while (TRUE) {[queueCondition lock];while ([queue count] == 0) {[queueCondition wait]; // 等待生产者添加项目}NSNumber *item = [queue objectAtIndex:0];[queue removeObjectAtIndex:0];NSLog(@"Consumer got item: %@", item);[queueCondition unlock];}
}int main(int argc, const char * argv[]) {@autoreleasepool {NSThread *producerThread = [[NSThread alloc] initWithTarget:self selector:@selector(producer) object:nil];NSThread *consumerThread = [[NSThread alloc] initWithTarget:self selector:@selector(consumer) object:nil];[producerThread start];[consumerThread start];[producerThread join];[consumerThread join];}return 0;
}
- 一个生产者线程(
producer
)生成项目并将其添加到队列中,然后调用signal
方法唤醒一个等待的消费者线程。 - 一个消费者线程(
consumer
)等待队列中有项目可用,一旦有项目可用,它就会从队列中取出项目并处理。
NSCondition
本身并不提供锁的功能,所以需要结合使用 NSLock
或 NSRecursiveLock
来保护临界区
NSConditionLock
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。它结合了互斥锁和条件变量的功能。NSConditionLock
提供了一种高级的同步机制,允许线程在等待特定条件满足时阻塞自己,而同时保持对共享资源的锁定。
@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
initWithCondition:
初始化Condition
,并且设置状态值-[NSConditionLock lock]
:获取锁,阻止其他线程访问受保护的资源。-[NSConditionLock unlock]
:释放锁,允许其他线程访问受保护的资源。-[NSConditionLock lockWhenCondition:]
:如果条件不满足,则释放锁并使当前线程等待,直到条件变为真(true),然后再次获取锁并继续执行。-[NSConditionLock lockWhenCondition:withTimeout:]
:与lockWhenCondition:
相同,但增加了一个超时值,如果超时时间内条件未满足,线程将恢复执行而不等待。
#import <Foundation/Foundation.h>@interface ConditionDemo : NSObject
@property (nonatomic, strong) NSConditionLock *lock;
@property (nonatomic, assign) int counter;
@end@implementation ConditionDemo- (instancetype)init {self = [super init];if (self) {_lock = [[NSConditionLock alloc] initWithCondition:1];_counter = 0;}return self;
}- (void)incrementCounter {[self.lock lock];if (_counter >= 10) {[self.lock lockWhenCondition:1]; // 等待条件满足}_counter++;NSLog(@"Counter incremented to: %d", _counter);[self.lock unlock];
}- (void)waitForCounterToReachTen {[self.lock lock];while (_counter < 10) {[self.lock lockWhenCondition:0]; // 等待条件满足}NSLog(@"Counter reached ten.");[self.lock unlock];
}@endint main(int argc, const char * argv[]) {@autoreleasepool {ConditionDemo *demo = [[ConditionDemo alloc] init];dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);for (int i = 0; i < 20; i++) {dispatch_async(queue, ^{[demo incrementCounter];});}[demo waitForCounterToReachTen];}return 0;
}
注意:
使用 NSConditionLock
的关键点在于正确管理条件的真伪值。通常,你可以使用条件锁的构造函数初始化一个条件值,然后在代码中根据需要更新这个值。当一个线程调用 lockWhenCondition:
或 lockWhenCondition:withTimeout:
方法时,它会检查条件值,如果条件为假(false),则线程会释放锁并等待,直到另一个线程改变了条件值使其为真(true)。
@synchronized
@synchronized
是一个编译器指令,对mutex
递归锁的封装, @synchronized(obj)
内部会生成obj对应的递归锁,然后进行加锁、解锁操作
@synchronized([obj]) {// 临界区代码// 这里的代码将受到保护,一次只允许一个线程执行
}
这里的 [object]
是任何 Objective-C 对象,@synchronized
将使用该对象的地址作为锁的标识。这意味着每次调用 @synchronized
并传入相同的对象时,都会使用同一个锁。如果传入不同的对象,则会使用不同的锁。
工作原理:
当一个线程进入由 @synchronized
保护的代码块时,它会尝试获取与传入对象关联的锁。如果锁当前未被其他线程持有,那么该线程将获得锁并执行临界区的代码。如果锁已被其他线程持有,那么当前线程将等待,直到锁被释放。当线程完成临界区的代码执行时,它会自动释放锁。
@synchronized
的底层实现就是在开始和结束的时候调用了objc_sync_enter
&objc_sync_exit
方法。
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}return result;
}
就是根据id2data
方法找到一个data
对象,然后在对data
对象进行mutex.lock()
加锁操作。我们点击进入id2data
方法继续查找
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
发现获取data
对象的方法其实就是根据sDataLists[obj].data
这个方法来实现的,也就是一个哈希表。
总之就是@synchronized在底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁