ios中线程同步方案
- OSSpinLock 自旋锁
- os_unfair_lock 互斥锁
- phread_mutex
- dispatch_semaphore 信号量
- dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列
- NSLock
- NSRecursiveLock
- NSCondition 条件
- NSConditionLock 条件锁
- @synchronized
OSSpinLock
OSSpinLock 叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用CPU资源
导入头文件 #import <libkern/OSAtomic.h>
查看以下加锁代码:该锁已过期
卖一张票
*/
- (void)saleTicket{
// 初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;
//加锁
OSSpinLockLock(&lock);
int oldTicketsCount = self.ticketsCount;
sleep(.2);//为了更加凸显多线程带来的隐患
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
// self.ticketsCount--;
NSLog(@"还剩%d张票 - %@",oldTicketsCount,[NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&lock);
}
看起来明明加了锁,但是打印结果却是下图,为什么呢
正确代码应该是下文
@property (assign, nonatomic)OSSpinLock lock;
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化锁
_lock = OS_SPINLOCK_INIT;
[self ticketTest];
}
/**
卖一张票
*/
- (void)saleTicket{
//加锁
OSSpinLockLock(&_lock);
int oldTicketsCount = self.ticketsCount;
sleep(.2);//为了更加凸显多线程带来的隐患
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
// self.ticketsCount--;
NSLog(@"还剩%d张票 - %@",oldTicketsCount,[NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_lock);
}
还剩下0张票,这次结果对了!那为什么两次代码的运行结果会不同呢?
加锁的原理:多线程中总会有一条线程先行一步,成功上锁,那当其他线程来到加锁这一步时,发现这把锁已经被加了,那就回乖乖的在原地等待,直到这把锁放开为止,如果是局部变量的话,那么每一把锁都是新锁(新锁肯定没被别人加过啊),大家就各自加自己的锁,并没有达到线程同步的作用。所以大家必须加同一把锁才行。
那存钱取钱是加一把锁还是加两把锁呢?那卖票、存钱、取钱三个行为用几把锁呢?
线程阻塞有两种方案:一种是让线程休眠,直接睡觉(不占用cpu资源),另一种是一个while循环,也就是忙等(一直暂用CPU资源)while(未解锁)
代码摘要:
//导入头文件
#import <libkern/OSAtomic.h>
@property (assign, nonatomic)OSSpinLock lock;
// 初始化锁
_lock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&_lock);
// 解锁
OSSpinLockUnlock(&_lock);
// 尝试加锁 会缓解优先级反转的问题,但是会漏执行啊
if (OSSpinLockTry(_lock)) {
OSSpinLockUnlock(&_lock);
}
Ps:其他内容补充
#define OS_SPINLOCK_INIT 0
static OSSpinLock ticketMoney = OS_SPINLOCK_INIT;
//static 静态初始化,等式右边可以值为数值,但是不可以是函数
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ticketLock = test();
});
test(){
return OS_SPINLOCK_INIT;
}
锁的初始化就是将值赋为为0
目前自旋锁已经不再安全,可能会出现优先级反转问题。(苹果已弃用,在以前是性能比较高的锁,因为休眠状态的锁切换线程也是需要时间,消耗线程的)
如果等待锁的线程优先级较高,它会一直占用CPU资源,优先级低的线程就无法释放锁。
时间片轮转调度算法(进程、线程),如果优先级比较低的线程1先加锁了,而优先级特别高的线程1,想进行西一个加锁,由于CPU会把大量时间分配给线程1,导致线程2一直没走到解锁的代码,就会出现锁一直没被放开的状态。 如果是线程休眠就会解决这个问题,接下来我们来了解os_unfair_lock
os_unfair_lock
- os _ unfair _ lock用于取代不安全的 OSSpinLock ,从iOS10开始才支持
- 从底层调用看,等待os、unfair _ lock锁的线程会处于休眠状态,并非忙等
typedef struct os_unfair_lock_s {
uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
代码摘要
//导入头文件
#import <os/lock.h>
//初始化
os_unfair_lock lock=OS_UNFAIR_LOCK_INIT;
//尝试加锁
os_unfai_lock_trylock(&lock);
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
Ps:如果忘记解锁,或者解错锁,将会永远不能解锁,成为死锁。
pthread_mutex
ps:p开头的锁是跨平台,无论linx、windowns、Mac、ios平台通用,安卓开发不是哟!
mutex叫做“互斥锁”,等待锁的线程会处于休眠状态
代码摘要
//初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_NORMAL);
//初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,&attr);
//尝试加锁
pthread _mutex_trylock(&mutex);
pthread_mutex_lock(&mutex);
//解锁
pthread_mutex unlock(&mutex);
//销毁相关资源
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
如果你是这样初始化你的锁,那么将是不成功的,要注意结构体语法!
#define PTHREAD_MUTEX_INITIALIZER {_PTHREAD_MUTEX_SIG_init, {0}}
PTHREAD_MUTEX_INITIALIZER 是一个宏定义,结构体是不允许直接给属性赋值的因为这相当于直接调用set方法;
代码示例
#import <pthread.h>
@interface MutexDemo ()
@property (nonatomic,assign)pthread_mutex_t ticketMutex;
@property (nonatomic, assign)pthread_mutex_t moneyMutex;
@end
@implementation MutexDemo
- (instancetype)init{
if (self = [super init]) {
[self __initMutex:&_ticketMutex];
[self __initMutex:&_moneyMutex];
}
return self;
}
- (void)__initMutex:(pthread_mutex_t *)mutex{
// 静态初始化
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(&mutex, &attr);
// 属性可以传空,传空就相当于default
pthread_mutex_init(mutex, NULL);
// 销毁属性
pthread_mutexattr_destroy(&attr);
}
- (void)__saleTicket{
pthread_mutex_lock(&_ticketMutex);
[super __saleTicket];
pthread_mutex_unlock(&_ticketMutex);
}
- (void)__saveMoney{
pthread_mutex_lock(&_moneyMutex);
[super __saveMoney];
pthread_mutex_unlock(&_moneyMutex);
}
- (void)__drawMoney{
pthread_mutex_lock(&_moneyMutex);
[super __drawMoney];
pthread_mutex_unlock(&_moneyMutex);
}
- (void)dealloc{
//该锁拥有销毁方法,最好是将锁销毁掉
pthread_mutex_destroy(&_moneyMutex);
pthread_mutex_destroy(&_ticketMutex);
}
@end
另外的情况
以上情况会出现死锁,第一种情况可以用两把锁来解决,第二种情况只能使用递归锁,见下图
递归锁,允许同一个线程,对同一把锁进行加锁
自旋锁、互斥锁汇编分析
改造以下代码
OSSpinLock
进入汇编以后使用si进行追踪
我们会发现汇编一直在上图位置进行一个循环,没有停下来过,进入忙等状态!
mutex 互斥锁
使用c和si进行最终
我们会发现调用玩syscall(系统级别调用)就会弹出模拟器,然后汇编不再执行了l进入休眠状态。
os_unfair_lock执行汇编会有相同的结果。
条件锁
代码示例
- (void)otherTest {
[[[NSThread alloc]initWithTarget:self selector:@selector(__remove) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__add) object:nil]start];
}
//往数组中添加一个元素
- (void)__add{
pthread_mutex_lock(&_mutex);
NSLog(@"__add - begin");
[self.data addObject:@"Test"];
NSLog(@"添加了元素");
// 唤醒 信号
pthread_cond_signal(&_cond);
// 广播
pthread_cond_broadcast(&_cond);
NSLog(@"__add - end");
pthread_mutex_unlock(&_mutex);
}
//删除数组中的一个元素
- (void)__remove{
pthread_mutex_lock(&_mutex);
NSLog(@"__remove - begin");
if (self.data.count == 0) {
// 等待 睡觉的时候会把锁放开
NSLog(@"test");
pthread_cond_wait(&_cond, &_mutex);
printf("execute result:%d",pthread_cond_wait(&_cond, &_mutex));
}
[self.data removeLastObject];
NSLog(@"删除了元素");
// pthread_mutex_unlock(&_mutex);
}
Ps:此处代码出现过一个深渊巨坑关于sleep,花费了好几个小时还求助了外援才解决
从打印结果来看,虽然remove先拿到了锁,通过条件我们先让add方法执行,确保了data中必须有元素。
NSLock、 NSRecursiveLock
NSLock是对mutex普通锁的封装
@interface NSLock:NSObject<NSLocking>{
-(BOOL)tryLock;
-(BOOL) lockBeforeDate :(NSDate*) limit;
@end
@protocol NSLocking
-(void) lock;
-(void) unlock;
@end
代码摘要
//初始化锁
NSlock *lock = [[NSLock alloc]init];
//加锁
[self.moneyLock lock];
//解锁
[self.moneyLock unlock];
//
NSRecursiveLock
NSRecursiveLock 也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition
NSCondition(条件锁) 是对mutex和cond的封装。
代码摘要
@interface NSCondition :NSObject<NSLocking> {
-(void) wait;
-(BOOL) waitUnti1Date :(NSDate*) limit;
-(void) signal;
-(void) broadcast;//广播
@end
NSConditionLock
NSConditionLock 是对 NSCondition 的进一步封装,可以设置具体的条件值
代码摘要
@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 before Date:(NSDate*)limit;
@end
可以利用条件值来设置线程之间的依赖。如果init条件值未设置,那么condition = 0;
dispatch_queue
直接使用GCD的串行队列,也是可以实现线程同步的
代码摘要
self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.moneyQueue, ^{
});
dispatch_semaphore
- semaphore叫做”信号量”
- 信号量的初始值,可以用来控制线程并发访问的最大数量
- 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
代码摘要
// 信号量的初始值
int value =1;
// 初始化信号量
dispatch semaphore_t semaphore=dispatchsemaphore create(value);
// 如果信号量的值<=0,当前线程就会进入休眠等待(直到信号量的值>0)
// 如果信号量的值>0,就减1,然后往下执行后面的代码
dispatch semaphore wait(semaphoreDISPATCH TIME FOREVER);
// 让信号量的值加1
dispatchsemaphoresignal(semaphore);
@synchronized
- @synchronized是对mutex递归锁的封装
- 源码查看:objc4中的objc-sync.mm文件
- @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
苹果不推荐使用,性能较差
iOS线程同步方案性能比较
性能从高到低排序
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
自旋锁、互斥锁比较
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈