iOS各种锁总结

image.png

OSSpinLock

OSSpinLock自旋锁,因为自旋锁一直busy-waiting忙等待占用cpu,且不会像互斥锁、信号量一样会导致线程休眠,进而引发上下文切换,因此短时间持有自旋锁性能最高;但适合多线程处理器及持有时间较短(对于单线程处理器会降低cpu效率),并且存在优先级反转问题(不再安全的 OSSpinLock,对于ios线程存在多个优先级且高优先级不会被低优先级抢占,导致低优先级占用自旋锁后,高优先级被执行获取自旋锁,导致死锁等待,见下)

如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。

对于ios跟踪OSSpinLock汇编,其实现就是while循环,具体如下:

0x104ba98b1 <+12>: cmpl   $-0x1, %eax
0x104ba98b4 <+15>: jne    0x104ba98d1               ; <+44>
0x104ba98b6 <+17>: testl  %ecx, %ecx
0x104ba98b8 <+19>: je     0x104bab0f9               ; _OSSpinLockLockYield
0x104ba98be <+25>: pause
0x104ba98c0 <+27>: incl   %ecx
0x104ba98c2 <+29>: movl   (%rdi), %eax
0x104ba98c4 <+31>: testl  %eax, %eax
0x104ba98c6 <+33>: jne    0x104ba98b1               ; <+12>
0x104ba98c8 <+35>: xorl   %eax, %eax
0x104ba98ca <+37>: lock
0x104ba98cb <+38>: cmpxchgl %edx, (%rdi)
0x104ba98ce <+41>: jne    0x104ba98b1               ; <+12>

对于linux实现(linux2.6内核,)如下:

#define spin_lock(lock)   _spin_lock(lock)    //lock数据类型为*spinlock_t,虽然没有用到-_-。
#define _spin_lock(lock)  __LOCK(lock)       
#define __LOCK(lock) \
        do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)  

linux实现是通过关抢占来避免其他任务抢占来保证原子性(也存在关闭中断抢占),此时若持有自旋锁的线程休眠,引发上下文切换,另一个线程也获取该自旋锁而导致死锁(因关抢占除非主动调度,因此无法释放自旋锁);

spinlock与linux内核调度的关系

使用如下:

OSSpinLock lock = OS_SPINLOCK_INIT;//初始化
OSSpinLockLock(&lock);//上锁
OSSpinLockTry(&lock);//尝试上锁
OSSpinLockUnlock(&lock);//解锁

ios10.0+ mac10.12+已废弃OSSpinLock,替换为os_unfair_lock来解决优先级反转问题;

源码地址:
SpinLocksLoadStoreEx.c

os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10 macos10.12开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,需要导入头文件#import

//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);

跟踪汇编实现如下:

libsystem_kernel.dylib`__ulock_wait:
0x10701f360 <+0>:  movl   $0x2000203, %eax          ; imm = 0x2000203
0x10701f365 <+5>:  movq   %rcx, %r10

0x10701f368 <+8>:  syscall

0x10701f36a <+10>: jae    0x10701f374               ; <+20>
0x10701f36c <+12>: movq   %rax, %rdi
0x10701f36f <+15>: jmp    0x10701ce67               ; cerror_nocancel
0x10701f374 <+20>: retq
0x10701f375 <+21>: nop
0x10701f376 <+22>: nop
0x10701f377 <+23>: nop

其中syscall执行后线程休眠,执行路径:os_unfair_lock_lock -> _os_unfair_lock_lock_slow -> __ulock_wait

pthread_mutex_t pthread_cond_t

互斥锁条件变量POSIX接口,见《unix进程间通信.md》

NSLock NSRecursiveLock

NSLock是对pthread_mutex普通锁的封装。pthread_mutex_init(mutex, NULL);默认属性为PTHREAD_MUTEX_NORMAL

NSLock 遵循 NSLocking 协议,使用方法与pthread_mutex类似,如下:

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject 
- (BOOL)tryLock;//tryLock 是尝试加锁,如果失败的话返回 NO
- (BOOL)lockBeforeDate:(NSDate *)limit;//是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
@end

具体NSLock实现可参考GNUSetup, 搜索NSLock.m,找到 initialize 方法

GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值

 (void) initialize
{
    static BOOL    beenHere = NO;

    if (beenHere == NO)
    {
        beenHere = YES;

        /* Initialise attributes for the different types of mutex.
        * We do it once, since attributes can be shared between multiple
        * mutexes.
        * If we had a pthread_mutexattr_t instance for each mutex, we would
        * either have to store it as an ivar of our NSLock (or similar), or
        * we would potentially leak instances as we couldn't destroy them
        * when destroying the NSLock.  I don't know if any implementation
        * of pthreads actually allocates memory when you call the
        * pthread_mutexattr_init function, but they are allowed to do so
        * (and deallocate the memory in pthread_mutexattr_destroy).
        */
        pthread_mutexattr_init(&attr_normal);
        pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL);
        pthread_mutexattr_init(&attr_reporting);
        pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK);
        pthread_mutexattr_init(&attr_recursive);
        pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);

        /* To emulate OSX behavior, we need to be able both to detect deadlocks
        * (so we can log them), and also hang the thread when one occurs.
        * the simple way to do that is to set up a locked mutex we can
        * force a deadlock on.
        */
        pthread_mutex_init(&deadlock, &attr_normal);
        pthread_mutex_lock(&deadlock);
    }
}

NSRecursiveLock是对pthread_mutex递归属性下的封装,API与NSLock一致;

NSCondition

NSCondtion是对pthread_mutexpthread_cond的封装,具体类源码如下:

+ (void) initialize
{
  [NSLock class];   // Ensure mutex attributes are set up.
}

- (id) init
{
    if (nil != (self = [super init]))
    {
        if (0 != pthread_cond_init(&_condition, NULL))
        {
            DESTROY(self);
        }
        else if (0 != pthread_mutex_init(&_mutex, &attr_reporting))
        {
            pthread_cond_destroy(&_condition);
            DESTROY(self);
        }
    }
    return self;
}

- (void) signal
{
    pthread_cond_signal(&_condition);
}

- (void) wait
{
    pthread_cond_wait(&_condition, &_mutex);
}

- (void) broadcast
{
  pthread_cond_broadcast(&_conditon);
}

具体使用如下:

@interface NSCondition : NSObject  {
- (void)wait;//等待条件
- (BOOL)waitUntilDate:(NSDate *)limit;//超时等待
- (void)signal;//发送信号
- (void)broadcast;//广播信号
@end

需要注意NSCondition遵循NSLock协议,同pthread_cond_t,需要添加条件变量时加锁,避免信号丢失及条件变量无法获取锁导致一直阻塞线程等待;

NSCondtionLock

NSConditionLock是对NScondition的进一步封装,具体如下:

@interface NSConditionLock : NSObject  {
 
- (instancetype)initWithCondition:(NSInteger)condition;//初始化Condition,并且设置状态值

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;//当状态值为condition且收到等待的条件变量信号时返回,否则一直阻塞等待条件变量信号
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;//修改condition条件判断值并广播条件变量信号
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end

NSCondition同pthread_cond一样,存在pthread_cond_wait被虚假唤醒的情况,一般都会while(condition != true)循环判断是否条件为真,否则一直pthread_cond_wait阻塞;NSConditionLock就是提供了condition条件判断的逻辑;

具体类实现如下:

- (id) init
{
  return [self initWithCondition: 0];
}

- (id) initWithCondition: (NSInteger)value
{
  if (nil != (self = [super init]))
    {
      if (nil == (_condition = [NSCondition new]))
    {
      DESTROY(self);
    }
      else
    {
          _condition_value = value;
    }
    }
  return self;
}

- (void) lockWhenCondition: (NSInteger)value
{
  [_condition lock];
  while (value != _condition_value)
    {
      [_condition wait];
    }
}

//条件判断值改变并广播信号解锁
- (void) unlockWithCondition: (NSInteger)value
{
  _condition_value = value;
  [_condition broadcast];
  [_condition unlock];
}

synchronized

@synchronized使用如下:

@synchronized(obj) {
    //code
}

上述代码转化为c++,简化如下:

try {
    objc_sync_enter(obj);
  //code
} finally {
  objc_sync_exit(obj);
}

对于objc_sync_enterobjc_sync_exit源码如下:

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
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;
}

// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    
    return result;
}

其实现使用了递归锁来实现加锁避免多次使用@synchronized导致死锁,并使用缓存技术通过传入的objc地址来查找对应的锁,其中缓存是使用了hash map来缓存;若传入nil则锁不起作用;若@synchronized中的block修改了objc地址也不影响锁结构;并使用try finally来捕获异常,避免锁未释放;

使用注意事项

慎用@synchronized(self)

使用self对于外部可以修改使用的对象地址,容易外部混合使用@synchronized及其他锁导致死锁问题,如:

//class A
@synchronized (self) {
    [_sharedLock lock];
    NSLog(@"code in class A");
    [_sharedLock unlock];
}

//class B
[_sharedLock lock];
@synchronized (objectA) {
    NSLog(@"code in class B");
}
[_sharedLock unlock];

对于类内部数据同步,正确的做法是传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的

减小粗粒度

对于不同的数据锁同步使用不同的objc对象来控制,避免无关的对象锁,且block内部尽量避免函数调用;
正确使用多线程同步锁@synchronized

dispatch_queue(DISPATCH_QUEUE_SERIAL) GCD串行队列

dispatch_queue_t queue = dispatch_queue_create("top.istones.moneyQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.moneyQueue, ^{
    // 任务
});

任务顺序串行同步执行;

dispatch_semaphore

dispatch_semaphore信号量,同unix sem概念相同,具体使用如下:

//表示最多开启5个线程
dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);

实际封装的系统库semaphone_xxx,使用的是基于内存的信号量形式;

但是在资源可用的情况下,使用GCD semaphore将会消耗较少的时间,因为在这种情况下GCD不会调用内核,只有在资源不可用的时候才会调用内核,并且系统需要停在你的线程里,直到线程发出可用信号。

GCD之dispatch_semaphore源码剖析

pthread_rwlock 读写锁

pthread_rwlock经常用于文件等数据的读写操作,需要导入头文件#import

atomic属性

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁(使用了自旋锁)

可以参考源码objc4的objc-accessors.mm;

它并不能保证使用属性的过程是线程安全的(eg.一个属性array,atomic的话只能保证在外面set和get的时候线程安全,但是不能保证array addObject、removeObject线程安全);

atomic属性保证的属性的值修改(包括数值类型及指针类型)线程安全,但不保证指针指向的内存的安全及多部操作的原子性,如上array;见iOS多线程到底不安全在哪里?

image.png

对于property分为三类内存模型:指针、指针指向的内存区域及数值类型;对于指针及指向的内存容易好理解,对于数值类型,64位系统小于等于8位的数值,一个指令周期就可以获取,因此set/get操作atomicnoatomic都可以保证原子性,但对于数值操作,如

self.count = self.count + 1;

排除编译器优化的影响,分为读取(load)、加1(add)、赋值(store)三步操作,store前可能存在多次store操作;
建议:尽量避免多线程设计,若不可避免,则尽量不使用atomic属性,而使用锁机制,保证多条指令执行的原子性;

源码使用了自旋锁来保证属性指针修改或者值修改线程安全,源码见下:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

关键代码如下:

//设置noatomic
if (!atomic) {
    oldValue = *slot;
  *slot = newValue;
} else {//设置atomic
    spinlock_t& slotlock = PropertyLocks[slot];//自旋锁
    slotlock.lock();//自旋锁加锁
    oldValue = *slot;//保留旧值
    *slot = newValue;//修改新值的指针地址
    slotlock.unlock();//自旋锁释放锁
}

dispatch_barrier_async dispatch_barrier_sync

这个函数传入的必须是自己通过dispatch_queue_cretate创建的DISPATCH_QUEUE_CONCURRENT并发队列,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果;

作用是类似栅栏,但相比栅栏能控制汇合的点,即dispatch_barrier_asyn或者dispatch_barrier_sync,在这之前添加到队列的所有block执行完成,才去执行这两个函数中的block,再去执行后续添加的block

主要用于并发读写控制;

dispatch_queue_t queue = dispatch_queue_create("top.istones.rwQueue", DISPATCH_QUEUE_CONCURRENT);

// 读
dispatch_async(queue, ^{

});

// 写
dispatch_barrier_async(queue, ^{

});

两者的相同:

  • 等待前面的任务都执行完成才去执行后面的函数添加的任务;

不同:

  • dispatch_barrier_sync会阻塞调用线程此函数后面的执行(因为同步执行),而dispatch_barrier_async不会阻塞当前调用线程后面的代码执行;
    image.png

    image.png

    dispatch_barrier_sync、dispatch_barrier_async的使用

参考资料

iOS多线程安全-13种线程锁

iOS中的线程同步方案-锁

线程编程指南

并发编程指南

Demo

https://github.com/FengyunSky/notes/blob/master/local/code/threadlock.tar

你可能感兴趣的:(iOS各种锁总结)