锁是用来保证线程安全的一种机制,也是保持数据同步的一种必要手段。是确保一段代码在同一个时间只能允许被一个线程访问,例如,线程A进入一段被锁Lock加锁的代码,另外一个线程B,就不能在此时访问,只能等待线程A执行完成后,B线程才可以访问该段加锁的代码。
Advise:不要将过多的操作放到加锁的代码里,而让另外一个线程长时间的等待,这样不利于发挥多线程应有的功能。
锁的类型
1. NSLock
NSLock
是一个互斥锁,实现了NSLocking
<待查询该协议的内容>协议。
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
-
lock
进行加锁; -
unlock
解锁; -
tryLock
尝试加锁,如果失败,并不会阻塞线程,只是立即返回,不会执行加锁代码; -
lockBeforeDate:
在指定时间date之前阻塞线程,如果到期没有获取到锁,则线程被唤醒,函数立即返回NO。
2. 条件锁NSCondition
NSCondition
条件锁,它也实现了NSLocking
协议,所以也有lock
和unlock
方法。当然NSCondition
还有更高级的用法,有wait和signal。
NSCondition
可以给每个分线程加锁,加锁后不影响其他线程进入临界区域。这种分别加锁的方式,wait并加锁后不能真正解决资源竞争。
@interface NSCondition : NSObject {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
-(void)wait;
进入等待状态
-(BOOL)waitUnitilDate:(NSDate *)date;
线程等待一定时间
-(void)signal;
随机唤醒一个线程取消等待继续执行
-(void)broadcast;
唤醒所有线程取消等待继续执行
NSConditionLock
也实现了NSLocking
协议,可以像NSCondition
一样做多线程之间的任务等待调用,且线程安全
@interface NSConditionLock : NSObject {
@private
void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@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 NS_AVAILABLE(10_5, 2_0);
@end
3. 递归锁NSRecursiveLock
在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。
@interface NSRecursiveLock : NSObject {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
4. NSDistributedLock
没仔细研究过
NSDistributedLock是MAC开发中的跨进程的分布式锁,底层是用文件系统实现的互斥锁。NSDistributedLock没有实现NSLocking协议,所以没有lock方法,取而代之的是非阻塞的tryLock方法。当执行到do something时程序退出,程序再次启动之后tryLock就再也不能成功了,陷入死锁状态.其他应用也不能访问受保护的共享资源。在这种情况下,你可以使用breadLock方法来打破现存的锁以便你可以获取它。但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不可能再释放该锁。
. 5@synchronized
代码块
- (void)TeseSynchronizedMethod{
@synchronized(self){
//加锁的代码块
}
}
以上加锁的方式,把对应的代码块加到对应的块{}里边,如果某个线程没有执行完,其他的线程需要执行就得等待。类似互斥锁,保证此时没有其他线程对self对象进行修改。
@synchronized
是objective-c的一个锁令牌,防止self对象在同一时间被其他线程访问,起到线程的保护作用。一般在公有变量的时候作用,例如单例模式或者操作类的static变量中使用。
指令@synchronized()需要一个参数。该参数可以使任何的Objective-C对象,包括self。这个对象就是互斥信号量。针对程序中的不同的关键代码段,分别使用不同的信号量。只有在应用程序编程执行多线程之前就创建好所有需要的互斥信号量对象来避免线程间的竞争才是最安全的。
Objective-C中的同步特性是支持递归的。一个线程是可以以递归的方式多次使用同一个信号量的;其他的线程会被阻塞知道这个线程释放了自己所有的和该信号量相关的锁,也就是说通过正常执行或者是通过异常处理的方式退出了所有的@synchronized()
代码块。
当在@synchronized()
代码块中抛出异常的时候, Objective-C运行时会捕获到该异常,并释放信号量,并把该异常重新抛出给下一个异常处理者。
. 6dispatch_semaphore
信号量
dispatch_semaphore
是GCD中的信号量,它可以解决资源竞争和信号的通知和等待。当发送一个信号通知,则信号量+1;当等待一个信号时信号量-1;如果信号量为0,则信号会处于等待的状态,直到信号量大于0开始执行。
来个例子:
dispatch_semaphore_t signal = dispatch_semaphore_create(1); //传入值必须 >=0, 若传入为0则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 等待ing");
dispatch_semaphore_wait(signal, overTime); //signal 值 -1
NSLog(@"线程1");
dispatch_semaphore_signal(signal); //signal 值 +1
NSLog(@"线程1 发送信号");
NSLog(@"--------------------------------------------------------");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 等待ing");
dispatch_semaphore_wait(signal, overTime);
NSLog(@"线程2");
dispatch_semaphore_signal(signal);
NSLog(@"线程2 发送信号");
});
7. 互斥锁POSIX
POSIX是Unix/Linux平台上提供的一套条件互斥锁的API。
新建一个简单的POSIX互斥锁,引入头文件#import
声明并初始化一个pthread_mutex_t
的结构。使用pthread_mutex_lock
和pthread_mutex_unlock
函数。调用pthread_mutex_destroy
来释放该锁的数据结构。
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
int pthread_mutex_lock(pthread_mutex_t *);
int pthread_mutex_trylock(pthread_mutex_t *);
int pthread_mutex_unlock(pthread_mutex_t *);
int pthread_mutex_destroy(pthread_mutex_t *);
int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
int * __restrict);
int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
int * __restrict);
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
首先是第一个方法,这是初始化一个锁,__restrict 为互斥锁的类型,传 NULL 为默认类型,一共有 4 类型。
PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
OSIX还可以创建条件锁,提供了和NSCondition
一样的条件控制,初始化互斥锁同时使用pthread_cond_init
t来初始化条件数据结构.
// 初始化
int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
// 等待(会阻塞)
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
// 定时等待
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
// 唤醒
int pthread_cond_signal (pthread_cond_t *cond);
// 广播唤醒
int pthread_cond_broadcast (pthread_cond_t *cond);
// 销毁
int pthread_cond_destroy (pthread_cond_t *cond);
POSIX还提供了很多函数,有一套完整的API,包含Pthreads线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。推荐一篇详细文章,但不是基于iOS的,是基于Linux的,但是介绍的非常详细 Linux 线程锁详解
8. 自旋锁OSSpinLock
typedef int32_t OSSpinLock;
bool OSSpinLockTry( volatile OSSpinLock *__lock );
void OSSpinLockLock( volatile OSSpinLock *__lock );
void OSSpinLockUnlock( volatile OSSpinLock *__lock );
首先要提的是OSSpinLock
已经出现了BUG,导致并不能完全保证是线程安全的。
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。
OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
-摘自ibireme
所以说不建议再继续使用,不过可以拿来玩耍一下,导入头文件#import
#import
@interface MYOSSpinLockViewController ()
{
OSSpinLock spinlock; //声明pthread_mutex_t的结构
}
@end
@implementation MYOSSpinLockViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
spinlock = OS_SPINLOCK_INIT;
/**
* 初始化
*
*/
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
/**
* 加锁
*/
OSSpinLockLock(&spinlock);
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
}
/**
* 解锁
*/
OSSpinLockUnlock(&spinlock);
}
@end
OSSpinLock的性能真的很卓越,可惜啦
9. GCD线程阻断dispatch_barrier_async
/dispatch_barrier_sync
dispatch_barrier_async
/dispatch_barrier_sync
在一定的基础上也可以做线程同步,会在线程队列中打断其他线程执行当前任务,也就是说只有用在并发的线程队列中才会有效,因为串行队列本来就是一个一个的执行的,你打断执行一个和插入一个是一样的效果。两个的区别是是否等待任务执行完成。
注意:如果在当前线程调用dispatch_barrier_sync
打断会发生死锁。
@interface MYdispatch_barrier_syncViewController ()
{
__block double then, now;
}
@end
@implementation MYdispatch_barrier_syncViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
}else{
now = CFAbsoluteTimeGetCurrent();
printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
}
}
- (void)getImageNameWithMultiThread{
NSMutableArray *imageNames = [NSMutableArray new];
int count = 1024*11;
for (int i=0; i
参考
iOS 开发中的八种锁(Lock)
iOS多线程-各种线程锁的简单介绍
ibireme的不再安全的 OSSpinLock