iOS 中的锁(1)

2020-11-10  本文已影响0人  just东东

iOS 中的锁(1)

本文主要通过Objective-C语言进行体现,其实跟Swift也差不多。

本文从锁的基本概念NSLock@synchronized三个方面做了介绍。

1. 基本概念

锁的存在主要就是解决资源抢夺的问题,在iOS中的锁基本分为两种,分别是互斥锁自旋锁,其实读写锁也可以算一种,但是读写锁也是一种特殊的自旋锁。另外对于条件锁递归锁信号量基本都是上层的封装实现。

1.1 互斥锁

自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。

1.2 自旋锁

1.3 互斥锁和自旋锁的区别

1.4 使用场景

1.4 死锁

死锁就是字面意思,锁上了解不开,不解锁就不能继续执行,基本就是两个线程的相互等待,最后谁也等不到,这里说明一下阻塞和死锁的理解误区,阻塞就是不能继续执行了是线程内的等待,死锁是线程间的等待,本质上是不一样的。

1.5 其他锁

2. NSLock

NSLock在分类中属于互斥锁,是我们在使用Objective-C进行开发时常用的一种锁。看了好多文章说NSLock非递归锁,确实NSLock的递归上会引起阻塞或者崩溃,但是在同一线程内NSLock也可以再次加锁,所以在这一点也不绝对。

2.1 NSLock 定义

我们点击跳转到NSLock的定义处,源码如下:

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}bv

2.2 NSLocking 协议

从上一节中我们可以看到NSLock遵守一个NSLocking的协议,协议定义如下:

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

我们可以看到协议中的lockunlock方法就是我们常用的加锁解锁的方法。值得注意的是-lock-unlock必须在相同的线程中成对调用,否则就会产生未知的结果。

2.3 NSLock的其他方法

对于NSLock还有另外两个方法和一个属性,定义在源码的下面,代码如下:


// 尝试获取锁,获取到返回YES,获取不到返回NO
- (BOOL)tryLock;

// 在指定时间前获取锁,能够获取到返回YES,获取不到返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;

// 锁名称,如果使用锁出现异常,输出的log中会有锁的名称打印
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

2.4 NSLock 使用示例

这里我们模拟一个售票系统,如果不加锁的话就会导致一张票被卖多次的情况;加锁后才能保证票数的准确。

2.4.1 基本用法示例


- (void)testNSLock3 {
    
    self.lock = [[NSLock alloc] init];
    NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
    thread1.name = @"1号窗口";
    [thread1 start];

    NSThread *therad2 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
    therad2.name = @"2号窗口" ;
//    therad2.threadPriority = 0.8;
    [therad2 start];

    NSThread *therad3 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
    therad3.name = @"3号窗口" ;
//    therad3.threadPriority = 1 ;
    [therad3 start];
}

//模拟售票
-(void)therad:(id)object{

    //票数100张
    static int number = 100 ;

    while (1) {
        // 线程加锁,提高数据访问的安全性
        [self.lock lock];
        number--;
        NSLog(@"%@ %d",[[NSThread currentThread]name],number);
          //模拟等待
//        sleep(1);

        if (number == 0) { break ; }
        [self.lock unlock] ;
    }
}

2.4.2 tryLock

- (void)testNSLock5 {
    //主线程中
    NSLock *lock = [[NSLock alloc] init];
    
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"线程1");
        sleep(10);
        NSLog(@"睡醒了");
        [lock unlock];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        if ([lock tryLock]) {
            NSLog(@"线程2");
            [lock unlock];
        } else {
            NSLog(@"尝试加锁失败");
        }
    });
}

打印结果:

打印结果.jpg

根据打印结果我们可以看到,tryLock返回NO后也不会阻塞线程,还继续执行下面的代码。

2.4.3 lockBeforeDate

如果将2.4.2中的tryLock换成[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]就会阻塞线程,它将在Date前尝试加锁,如果在指定时间前都不能加锁则返回NO,加锁失败后跟上面打印是一致的。如果加锁成功打印结果如下:

打印结果:

打印结果.jpg

2.4.4 死锁


- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = [[NSLock alloc] init];
//    [NSThread detachNewThreadSelector:@selector(testLock1) toTarget:self withObject:nil];
    [self testLock1];
}

- (void)testLock1 {
    [self.lock lock];
    NSLog(@"testLock1: lock");
    [self testLock2];
    [self.lock unlock];
    NSLog(@"testLock1: unlock");
}

- (void)testLock2 {
    [self.lock lock];
    NSLog(@"testLock2: lock");
    [self.lock unlock];
    NSLog(@"testLock2: unlock");
}

这里只会打印testLock1: lock,在同一线程内如果没有解锁就再次加锁的话就会造成死锁。这里就是testLock2等待testLock1解锁,而testLock1也在等testLock2解锁。

2.5 NSLock 底层实现

通过上面的NSLock定义我们可以知道NSLock是在Foundation库中实现的,但是Foundation的开源代码只在Swift中有,本着有就比没有强的思想我们下载一个Swift CoreLibs Foundation源码一探究竟。

open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

    public override init() {
#if os(Windows)
        InitializeSRWLock(mutex)
        InitializeConditionVariable(timeoutCond)
        InitializeSRWLock(timeoutMutex)
#else
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
    }
    
    deinit {
#if os(Windows)
        // SRWLocks do not need to be explicitly destroyed
#else
        pthread_mutex_destroy(mutex)
#endif
        mutex.deinitialize(count: 1)
        mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
    }
    
    open func lock() {
#if os(Windows)
        AcquireSRWLockExclusive(mutex)
#else
        pthread_mutex_lock(mutex)
#endif
    }

    open func unlock() {
#if os(Windows)
        ReleaseSRWLockExclusive(mutex)
        AcquireSRWLockExclusive(timeoutMutex)
        WakeAllConditionVariable(timeoutCond)
        ReleaseSRWLockExclusive(timeoutMutex)
#else
        pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
#endif
#endif
    }

    open func `try`() -> Bool {
#if os(Windows)
        return TryAcquireSRWLockExclusive(mutex) != 0
#else
        return pthread_mutex_trylock(mutex) == 0
#endif
    }
    
    open func lock(before limit: Date) -> Bool {
#if os(Windows)
        if TryAcquireSRWLockExclusive(mutex) != 0 {
          return true
        }
#else
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
#endif

#if os(macOS) || os(iOS) || os(Windows)
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
        guard var endTime = timeSpecFrom(date: limit) else {
            return false
        }
        return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
    }

    open var name: String?
}

根据源码我们可以看到NSLock是对pthread互斥锁(mutex)的封装,我们可以看到在lockBbeforeLimit方法中会调用timedLock这个方法,这也是在Date前实现加锁的真正实现,我们跳转到该方法(PS:在源码中有Windows平台的实现,这里我们就不看了,直接看else的部分)进行查看:

timedLock 源码:

private func timedLock(mutex: _MutexPointer, endTime: Date,
                       using timeoutCond: _ConditionVariablePointer,
                       with timeoutMutex: _MutexPointer) -> Bool {
    var timeSpec = timeSpecFrom(date: endTime)
    while var ts = timeSpec {
        let lockval = pthread_mutex_lock(timeoutMutex)
        precondition(lockval == 0)
        let waitval = pthread_cond_timedwait(timeoutCond, timeoutMutex, &ts)
        precondition(waitval == 0 || waitval == ETIMEDOUT)
        let unlockval = pthread_mutex_unlock(timeoutMutex)
        precondition(unlockval == 0)

        if waitval == ETIMEDOUT {
            return false
        }
        let tryval = pthread_mutex_trylock(mutex)
        precondition(tryval == 0 || tryval == EBUSY)
        if tryval == 0 { // The lock was obtained.
            return true
        }
        // pthread_cond_timedwait didn't timeout so wait some more.
        timeSpec = timeSpecFrom(date: endTime)
    }
    return false
}

2.6 小结

至此我们对NSLock的分析就完毕了,总结如下:

3. @synchronized

@synchronized是我们在使用Objective-C开发时使用最多的一把锁了由于代码简单且方便实用深得广大开发者喜欢。但是很多人并不知道@synchronized底层实现是个递归锁,不会产生死锁,且不需要程序猿手动去加锁解锁。下面我们就慢慢揭开@synchronized的面纱。

3.1 @synchronized 实现探索

由于@synchronized是关键字,我们并不能直接查看它的具体实现。此时我们编写如下代码:并添加断点,通过汇编进行初步探索

16045596538778.jpg

要想查看汇编则需要在Xcode->Debug->Debug Workflow->Always Show Disassembly选中。

汇编代码.jpg

由上面的汇编代码我们可以看到在NSLog的上下分别有objc_sync_enterobjc_sync_exit的调用(bl)。其实这里objc_sync_enter就是加锁,objc_sync_exit就是解锁。一般objc开头的方法,基本都是在objc源码中,下面我们打开objc源码一探究竟。此处使用的是objc4-779.1

3.2 objc_sync_enter 探索

3.2.1 objc_sync_enter源码分析

objc_sync_enter源码:

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

通过注释我们可以看出该函数:

其实代码上也跟上面说的一致,只是还有些细微的处理:

3.2.2 关于加锁(id2data)进一步探索

id2data有两个参数,第一个是锁定对象obj,第二个ACQUIRE这里是个枚举值ACQUIRE的意思是加锁,其实还有两个分别是RELEASE意思是解锁和CHECK检查锁的状态。

我们在来看看SyncData是个什么东西?

SyncData源码:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

通过SyncData源码我们可以看到SyncData是一个结构体,拥有4个成员:

id2data函数分析

id2data.jpg

由于id2data代码比较多,这里我们通过折叠代码先来简单看看


下面我们来一步一步的分析,首先看看前三行代码

第一行就是获取一个锁,这是个局部变量,在本函数内需要使用的锁,看名字spinlock_t是个自旋锁,那么我们来看看它的实现的,源码如下:

using spinlock_t = mutex_tt<LOCKDEBUG>;

class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
    // 此处省略80多行代码...    
}

os_unfair_lock

/*!
 * @typedef os_unfair_lock
 *
 * @abstract
 * Low-level lock that allows waiters to block efficiently on contention.
 *
 * In general, higher level synchronization primitives such as those provided by
 * the pthread or dispatch subsystems should be preferred.
 *
 * The values stored in the lock should be considered opaque and implementation
 * defined, they contain thread ownership information that the system may use
 * to attempt to resolve priority inversions.
 *
 * This lock must be unlocked from the same thread that locked it, attempts to
 * unlock from a different thread will cause an assertion aborting the process.
 *
 * This lock must not be accessed from multiple processes or threads via shared
 * or multiply-mapped memory, the lock implementation relies on the address of
 * the lock value and owning process.
 *
 * Must be initialized with OS_UNFAIR_LOCK_INIT
 *
 * @discussion
 * Replacement for the deprecated OSSpinLock. Does not spin on contention but
 * waits in the kernel to be woken up by an unlock.
 *
 * As with OSSpinLock there is no attempt at fairness or lock ordering, e.g. an
 * unlocker can potentially immediately reacquire the lock before a woken up
 * waiter gets an opportunity to attempt to acquire the lock. This may be
 * advantageous for performance reasons, but also makes starvation of waiters a
 * possibility.
 */
OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
    uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;

根据上面的注释,也就是下面这句,os_unfair_lock是用来替代OSSpinLock这个自旋锁的互斥锁,不会自旋,在内核中等待被唤醒。所以说spinlock_t并不是如它的名字一般,而是个互斥锁。

Replacement for the deprecated OSSpinLock. Does not spin on contention but waits in the kernel to be woken up by an unlock.

第二行代码获取了一个SyncData类型的二重指针,我们通过查看SyncData的定义知道它是一个链表结构,所以说这个listp就是链表的头指针。对于宏LIST_FOR_OBJ代码如下:

#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

可以看到这个宏是个全局静态变量sDataLists,以obj为索引获取值,获取到的对象类型为StripedMap<SyncList>,同时对其取data取地址进行返回。对于StripedMap是一个类,如下图:

Class StripedMap.jpg

第三行代码就是定义了一个result值为NULL,是id2data()需要返回的结果,就没啥说的了。


接下来是在单线程中查找,代码如下:

单线程缓存查找.jpg

在单线程缓存中查找不到后,就会来到下面的全局缓存中进行查找

全局缓存查找.jpg

SyncCache 和 SyncCacheItem 结构体:

两个结构体实现如下,详见注释,对于SyncCacheItem就一看就明了了。

typedef struct SyncCache {
    unsigned int allocated; // 保存`SyncCacheItem`的总数
    unsigned int used;  // 保存使用的数量
    SyncCacheItem list[0]; // 缓存链表头结点地址
} SyncCache;

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

fetch_cache 源码:

static SyncCache *fetch_cache(bool create)
{
    _objc_pthread_data *data;
    
    data = _objc_fetch_pthread_data(create);
    if (!data) return NULL;

    if (!data->syncCache) {
        if (!create) {
            return NULL;
        } else {
            int count = 4;
            data->syncCache = (SyncCache *)
                calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
            data->syncCache->allocated = count;
        }
    }

    // Make sure there's at least one open slot in the list.
    if (data->syncCache->allocated == data->syncCache->used) {
        data->syncCache->allocated *= 2;
        data->syncCache = (SyncCache *)
            realloc(data->syncCache, sizeof(SyncCache) 
                    + data->syncCache->allocated * sizeof(SyncCacheItem));
    }

    return data->syncCache;
}

如果来到下面这段代码就说明我们在任何缓存中都没有找到当前对象的锁,说明是第一次给这个对象加锁。

16046316987295.jpg

最后我们goto done

done.jpg

在done这个模块主要是对result的一些处理

关于快速缓存:前面分析的时候无论在线程缓存中是否找到被锁的对象(前提是线程快速缓存存在)fastCacheOccupied都会被置为YES,也就是说线程私有数据的快速缓存只缓存一次,且只保存第一次的这一个节点指针。我觉得就是你锁了一次,下次在锁的概率很大,使用频率也会超级高,因为在锁定一个对象的时候大多情况都是多线程操作这个对象,在短时间内操作频率足够高,如果不高的话可能也不至于用锁,还有可能该对象的同步锁已经被其他线程缓存到其他线程的私有数据了,当前线程又无法访问其他线程的私有数据,如果替换的话,会重复缓存。

3.2.3 objc_sync_exit

关于解锁我们也是直接看源码了,代码如下:

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

解锁的代码就很简单了,几步判断,核心步骤也是通过id2data进行处理的,在加锁代码分析的时候多提到过。

3.3 @synchronized 总结

3.3.1 注意事项

3.3.2 @synchronized

上一篇下一篇

猜你喜欢

热点阅读