ios中线程同步方案

297 阅读7分钟

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);
}

看起来明明加了锁,但是打印结果却是下图,为什么呢

image-20220426090031558

正确代码应该是下文

@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);
}

image-20220426090346591

还剩下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:其他内容补充

image-20220502150041804

#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);
​

image-20220502155157601

如果你是这样初始化你的锁,那么将是不成功的,要注意结构体语法!

#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
另外的情况

image-20220502162937541

image-20220502162916133

以上情况会出现死锁,第一种情况可以用两把锁来解决,第二种情况只能使用递归锁,见下图

image.png

递归锁,允许同一个线程,对同一把锁进行加锁

image-20220502163859065

自旋锁、互斥锁汇编分析

改造以下代码

OSSpinLock

image-20220502165832995

image-20220502170225965

image-20220502170326992

进入汇编以后使用si进行追踪

image-20220502165714247

我们会发现汇编一直在上图位置进行一个循环,没有停下来过,进入忙等状态!

mutex 互斥锁

image-20220502170902916

使用c和si进行最终

image-20220502174819272

我们会发现调用玩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,花费了好几个小时还求助了外援才解决

image-20220502192203741

从打印结果来看,虽然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

image-20220502215940149

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

image-20220502223310162

可以利用条件值来设置线程之间的依赖。如果init条件值未设置,那么condition = 0;

dispatch_queue

直接使用GCD的串行队列,也是可以实现线程同步的

代码摘要
self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
 dispatch_sync(self.moneyQueue, ^{
   
 });

image-20220503090414610

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);

image-20220503093020613

@synchronized

  • @synchronized是对mutex递归锁的封装
  • 源码查看:objc4中的objc-sync.mm文件
  • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

image-20220503094752153

苹果不推荐使用,性能较差

iOS线程同步方案性能比较

性能从高到低排序

  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

自旋锁、互斥锁比较

什么情况使用自旋锁比较划算?

  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  • CPU资源不紧张
  • 多核处理器

什么情况使用互斥锁比较划算?

  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈