iOS之线程同步(锁)

531 阅读3分钟

近期要写一个多线程工具,把之前学习的多线程,以及线程同步复习一下

多线程的本质:有多条线程,但是只能执行一条,如果间隔时间设置的足够小,就给人的感觉是多条线程是同时进行的,时间片轮转调度算法

iOS中线程同步方案

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • Dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

OSSpinLock - 自旋锁

等待锁🔐的线程会处于忙等状态,一直占用CPU资源

会出现优先级翻转的问题,如果线程之间的优先级不同,如果低优先级的锁先进来,把锁锁住,那么高优先级的线程进来就会一直忙等,但是系统又会分配时间资源给线程高的,从而导致低优先级的线程无法执行完自己的代码,从而导致优先级低的锁无法释放。

/// 初始化锁
self.lock = OS_SPINLOCK_INIT;
   
- (void)saleTicket{
    /// 加锁
  OSSpinLockLock(&_lock);
  //BOOL isLock =  OSSpinLockTry(&_lock); 
  int oldTicketsCount = self.ticketsCount;
  sleep(.2);
  oldTicketsCount--;
  self.ticketsCount = oldTicketsCount;
  NSLog(@"还剩下%d张票 = %@",self.ticketsCount,[NSThread currentThread]);
  /// 解锁
  OSSpinLockUnlock(&_lock);
}
   

os_unfair_lock - iOS10后支持 (本质是互斥锁)

用于替代OSSpinLock 的锁,使用的技术不再是忙等,而是休眠等待唤醒

/// 初始化锁
self.lock = OS_UNFAIR_LOCK_INIT;
- (void)saleTicket{   
    /// 加锁
    os_unfair_lock_lock(&_lock);
//  BOOL isLock = os_unfair_lock_trylock(&lock);
    // 卖票
    [self sale];
    /// 解锁
    os_unfair_lock_unlock(&_lock);
}

pthread_mutex

mutex 叫做"互斥锁",等待锁的线程会处于休眠状态

/// 初始化属性    
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
/// 初始化锁
pthread_mutex_init(&_lock, &attr);
​
//销毁属性 pthread_mutexattr_destroy(&attr);
//销毁锁   pthread_mutex_destroy(&_lock);
​
- (void)saleTicket{ 
    /// 加锁
    pthread_mutex_lock(&_lock);
    ///尝试 加锁
    //BOOL islock = pthread_mutex_trylock(&_lock);
    // 卖票
    [self sale];
    /// 解锁
    pthread_mutex_unlock(&_lock);
}
/*
 * 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
递归锁

递归锁:允许同一个线程对一把锁重复加锁

线程一 : 调用saleTicket (+🔐)

  • 调用saleTicket (+🔐)

    • 调用saleTicket (+🔐)

线程二:调用saleTicket (发现已经被加锁了,等待)

当锁中间的代码遇到递归调用,打印的结果永远只有一条saleTicket,因为没有人能够走到解锁的那一步。

- (void)saleTicket{
    /// 加锁
    pthread_mutex_lock(&_lock);
    ///尝试 加锁
    //BOOL islock = pthread_mutex_trylock(&_lock);
    NSLog(@"%s",__func__);
    // 卖票 递归调用
    [self saleTicket];
    /// 解锁
    pthread_mutex_unlock(&_lock);
 
}

pthread是支持递归锁的,只需要把初始化属性改为递归锁

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

条件锁
@property (nonatomic, assign) pthread_cond_t cond;
​
/// 初始化条件
pthread_mutex_init(&_lock, &attr);
​
// 销毁条件
pthread_cond_destroy(&_cond);
​
[[[NSThread alloc]initWithTarget:self selector:@selector(__remove) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__add) object:nil]start];
​
- (void)__remove{
   
    pthread_mutex_lock(&_lock);
    
    if (self.dataArray.count == 0) {
        //一旦进入等待状态,就会放开这把锁,直到别人发送信号唤醒
        pthread_cond_wait(&_cond, &_lock);
    }
    [self.dataArray removeLastObject];
    NSLog(@"删除了元素");
    pthread_mutex_unlock(&_lock);
}
​
- (void)__add{
    
    pthread_mutex_lock(&_lock);
    
    [self.dataArray addObject:@"123"];
    NSLog(@"添加了元素");
     
    pthread_cond_signal(&_cond);
    pthread_mutex_unlock(&_lock);
    // 这里需要保证pthread_cond_signal在pthread_mutex_unlock之前
    // 如果在之后的话,锁解开了,发送信号的这个过程中有可能被别的锁抢先进来了
}
​

NSLock/NSRecursiveLock/NSCondition

  • NSLock是对mutex普通锁的封装
  • NSRecursiveLock 是对mutex递归锁的封装
  • NSCondition 是对mutexcont的封装
@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 API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end

可以查看GNUStep看到NSLock的实现

@interface NSConditionLock : NSObject <NSLocking> {
@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 API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
​
@end

Dispatch_queue(DISPATCH_QUEUE_SERIAL) - 串行队列

  • 串行队列,也可以实现线程同步,保证了每一条线程的操作都是按顺序的
  @property (nonatomic, strong) dispatch_queue_t  queen;
  
  self.queen =  dispatch_queue_create("123", DISPATCH_QUEUE_SERIAL);
  
  - (void)test{
    self.ticketsCount = 20;
    
    dispatch_async(self.queen, ^{
        for (int i = 0; i<5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(self.queen, ^{
        for (int i = 0; i<5; i++) {
            [self saleTicket];
        }
    });
}

dispatch_semaphore - 信号量

  • 信号量的初始值,可以用来控制线程的并发访问的最大数量
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
  // 设置最大并发数     
 self.semaphore = dispatch_semaphore_create(1);
      
  for (int i = 0; i<20; i++) {
     [[[NSThread alloc]initWithTarget:self selector:@selector(test) object:nil]start];
  }
  
- (void)test{    
    // 如果信号量的值>0,就让信号量减1,然后继续执行下面的代码
    // 直到信号量的值<=0的时候,就会休眠等待
    // DISPATCH_TIME_FOREVER永远 或者 设置成 DISPATCH_TIME_NOW现在立即
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    self.ticketsCount--;
    sleep(2);
    NSLog(@"剩下的票为 %d",self.ticketsCount);
    //让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
}

@synchronized

  • @synchronized是对mutex递归锁的封装
    @synchronized (self) { //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 方法内部

image.png

image.png 内部是一个哈希表,把obj当做keydata->mutex.lock(); 拿到唯一的mutex锁,来加锁


自旋锁和互斥锁对比

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

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

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

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

\