iOS中锁

315 阅读7分钟

最底层的锁有两周互斥锁自旋锁,有很多高级的锁都是基于它们实现的 加锁的目的是保证共享资源的任意时间里,只有一个线程访问,这样就可以避免由于多线程导致的数据错乱的问题。

互斥锁 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞

对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。

自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。

如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。

自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对

互斥锁中又分为递归锁和非递归锁 下面我们分析常见的几种锁

@synchronized

我们一共有20张票,

截屏2021-08-17 上午11.37.53.png

然后有好几个窗口在同时卖票,

截屏2021-08-17 上午11.38.25.png

截屏2021-08-17 上午11.39.14.png 好了,现在开始卖票

截屏2021-08-17 上午11.39.48.png

但是我们发现,在票为17张的时候下面显示出18张,这样这个票数便不准确了。 这是因为在同一时刻有多个线程同时操作同一块内存区域,导致数据不准确,我们加一个锁,在同一时刻只有一个线程操作这个票数。 截屏2021-08-17 下午12.06.08.png

截屏2021-08-17 下午12.06.26.png 在上面的例子中我们用到了@synchronized锁,它起到一个加锁的效果。有时候我们还会看到下面这种结构。@synchronized是可以实现递归可重入的。那么@synchronized的结构是怎么样子的,是怎么实现加锁的效果的❓。我们来分析源码

@synchronized (p) {

              @synchronized (p1) {

                   
              }
}

加个断点,看到汇编走到了

截屏2021-08-17 下午1.37.44.png

截屏2021-08-17 下午1.48.02.png

找到objc的源码,搜索objc_sync_enter

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;

}

从else看出,如果obj为空,则does nothing

看下objc_sync_exit

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;

}

两者代码很相似,在obj不为空的情况下,分别进行了data->mutex.lock();bool okay = data->mutex.tryUnlock();操作。

我们看下SyncData* data = id2data(obj, ACQUIRE);主要流程代码


static SyncData* id2data(id object, enum usage why)
{

spinlock_t *lockp = &LOCK_FOR_OBJ(object);

SyncData **listp = &LIST_FOR_OBJ(object);

SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS

bool fastCacheOccupied = NO;
//#define __PTK_FRAMEWORK_OBJC_KEY1 41
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);

//如果从线程里面找到有data则走if里面,线程lockCount++
if(data){...}

SyncCache *cache = fetch_cache(NO);
//Check per-thread cache of already-owned locks for matching object
//在锁退出的时候,如果cache中有,进行lockCount--,threadCount--操作
if (cache) {...}

lockp->lock();
//如果data里面没有listp里面的object==object则threadCount,线程数++
{...}

//创建一个SyncData添加到list
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));

result->object = (objc_object *)object;

result->threadCount = 1;

new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);

result->nextData = *listp;

*listp = result;

done:

    lockp->unlock();
    if (result) {
    //存入cache 等别的操作
    ...
    }
return result;    
数据结构
spinlock_t *lockp = &LOCK_FOR_OBJ(object);

SyncData **listp = &LIST_FOR_OBJ(object);
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock

#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;

我们看下SyncList, StripedMap

struct SyncList {

SyncData *data;

spinlock_t lock;

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

};
template<typename T>

class StripedMap {

#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR

enum { StripeCount = 8 };

#else

enum { StripeCount = 64 };

#endif
...
}

//里面有递归锁,还有多线程,所以比recursive_mutex_t更强大,用的更多一点
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;



可以看到sDataLists是一个全局的静态哈希表 打印一下,由class StripedMap我们使用的模拟器,所以StripeCount = 64,是下面的结构

(StripedMap<SyncList>::PaddedT [64]) array = {

  [0] = {

    value = {

      data = NULL

      lock = {

        mLock = (_os_unfair_lock_opaque = 0)

      }

    }

  }

  [1] = {

    value = {

      data = NULL

      lock = {

        mLock = (_os_unfair_lock_opaque = 0)

      }

    }

  }

  [2] = {

    value = {

      data = NULL

      lock = {

        mLock = (_os_unfair_lock_opaque = 0)

      }

    }

  }

  ...
  [63] = {

    value = {

      data = NULL

      lock = {

        mLock = (_os_unfair_lock_opaque = 0)

      }

    }

  }

}
    
小结

objc_sync_enter/exit 是对称出现的。封装的是底层的递归锁 支持两种存储:TLS、cache 第一次会创建一个syncData,会进行一个头插法,创建一个链表结构,标记threadCount=1、 第二次进来会判断当前是不是同一个对象进来,如果不是,重新创建,重新标记 如果是同一个对象,TLS可以找到之后,就会多lockCount进行加加,如果TLS找不到,listp里面有这个object则threadCount++ 如果exit,则进行一个lockCount--和threadCount--

为什么Synchronized:具备可重入、递归和多线程 1.有线程空间的保障(TLS),保障了threadCount,有多少条线程对这个锁进行加锁 2.lockCount++进行了多少次

NSLock、NSRecursiveLock

for (int i = 0; i < 200000; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            _testArray = [NSMutableArray array];

        });

    }

在这种多线程同时对一块资源进行操作的时候,会有线程不安全的情况,上面会出现崩溃,我们给  _testArray = [NSMutableArray array];进行加锁,可以解决这个问题。

   NSLock *lock = [[NSLock alloc] init];

   for (int i = 0; i < 200000; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [lock lock];

            _testArray = [NSMutableArray array];

            [lock unlock];

        });

    }

我们点进去,看下NSLock,发现它遵循NSLocking协议,这个协议里面有两个方法lock和unlock 截屏2021-08-20 下午2.19.32.png

for (int i= 0; i<10; i++) {

dispatch_async(dispatch_get_global_queue(0, 0), ^{

static void (^testMethod)(int);

testMethod = ^(int value){

if (value > 0) {

NSLog(@"current value = %d",value);

testMethod(value - 1);

}

};

testMethod(10);

});

}

对于这个例子我们要怎么加锁呢,如果不加锁会发现输出的数据会存在紊乱的情况。

NSLock *lock = [[NSLock alloc] init];

for (int i= 0; i<10; i++) {

dispatch_async(dispatch_get_global_queue(0, 0), ^{

[lock lock];

static void (^testMethod)(int);

testMethod = ^(int value){

if (value > 0) {

NSLog(@"current value = %d",value);

testMethod(value - 1);

}

};

testMethod(10);

[lock unlock];

});

}

如果我们把锁加如下的位置

NSLock *lock = [[NSLock alloc] init];

for (int i= 0; i<10; i++) {

dispatch_async(dispatch_get_global_queue(0, 0), ^{

static void (^testMethod)(int);

testMethod = ^(int value){

[lock lock];

if (value > 0) {

NSLog(@"current value = %d",value);

testMethod(value - 1);

}

[lock unlock];

};

testMethod(10);

});

}

则只输出10,每次进去进行一个lock,但是没有unlock,所以只输出10 我们知道NSRecursiveLock是递归锁,我们把NSRecursiveLock换成递归锁 输出10 9 8 7 6 5 4 3 2 1 然后崩溃了,NSRecursiveLock只是单线程的递归,多线程会出现问题。我们知道前面的@synchronized支持多线程,那么我们换成@synchronized 则输出正常

for (int i= 0; i<10; i++) {


dispatch_async(dispatch_get_global_queue(0, 0), ^{

static void (^testMethod)(int);

testMethod = ^(int value){

@synchronized (self) {

if (value > 0) {

NSLog(@"current value = %d",value);

testMethod(value - 1);

}

}

};

testMethod(10);

});

}

截屏2021-08-20 下午2.38.35.png 我们看到NSRecursiveLock也是遵循协议

NSCondition

有时候会有一些生产消费问题,消费依赖生产,先生产了才能消费,如果生产为0此时消费应该等着生产,当生产为正,则发送一个消息给消费,可以消费了,然后开始消费,这种情况下,我们可以使用NSCondition

截屏2021-08-20 下午2.47.43.png api文档也比较简单, eg:

- (void)lg_testConditon{

_testCondition = [[NSCondition alloc] init];

//创建生产-消费者

for (int i = 0; i < 50; i++) {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

[self lg_producer];

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

[self lg_consumer];

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

[self lg_consumer];

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

[self lg_producer];

});

}

}



- (void)lg_producer{

[_testCondition lock]; // 操作的多线程影响

self.ticketCount = self.ticketCount + 1;

NSLog(@"生产一个 现有 count %zd",self.ticketCount);

[_testCondition signal]; // 信号

[_testCondition unlock];

}


- (void)lg_consumer{

[_testCondition lock]; // 操作的多线程影响

if (self.ticketCount == 0) {

NSLog(@"等待 count %zd",self.ticketCount);

[_testCondition wait];

}

//注意消费行为,要在等待条件判断之后

self.ticketCount -= 1;

NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);

[_testCondition unlock];

}

NSConditionLock

api中一些简单的使用

NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

[conditionLock lockWhenCondition:1];

NSLog(@"线程 1");

[conditionLock unlockWithCondition:0];

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{

[conditionLock lockWhenCondition:2];

sleep(0.1);

NSLog(@"线程 2");

[conditionLock unlockWithCondition:1];

});

dispatch_async(dispatch_get_global_queue(0, 0), ^{

[conditionLock lock];

NSLog(@"线程 3");

[conditionLock unlock];

});

小结

这些锁底层都是对 pthread_mutex_lock的封装, 对于NSLock的源码

open class NSLock: NSObject, NSLocking {
...
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
...
}

对于NSRecursiveLock的源码

open class NSRecursiveLock: NSObject, NSLocking {

public override init() {

super.init()

...

withUnsafeMutablePointer(to: &attrib) { attrs in

pthread_mutexattr_init(attrs)
//比NSLock多一个标记
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))

pthread_mutex_init(mutex, attrs)

}

...

}
open func lock() {

pthread_mutex_lock(mutex)

}

open func unlock() {

pthread_mutex_unlock(mutex)

}
}

NSCondition源码

open class NSCondition: NSObject, NSLocking {

internal var mutex = _MutexPointer.allocate(capacity: 1)

internal var cond = _ConditionVariablePointer.allocate(capacity: 1)

public override init() {


pthread_mutex_init(mutex, nil)


}

deinit {


pthread_mutex_destroy(mutex)

pthread_cond_destroy(cond)

}

open func lock() {

pthread_mutex_lock(mutex)

}

open func unlock() {

pthread_mutex_unlock(mutex)

}

open func wait() {

pthread_cond_wait(cond, mutex)

}

open func wait(until limit: Date) -> Bool {

guard var timeout = timeSpecFrom(date: limit) else {

return false

}

return pthread_cond_timedwait(cond, mutex, &timeout) == 0

}

open func signal() {

pthread_cond_signal(cond)

}

open func broadcast() {

pthread_cond_broadcast(cond) // wait signal

}

open var name: String?

}

NSConditionLock源码

open class NSConditionLock : NSObject, NSLocking {

internal var _cond = NSCondition()

internal var _value: Int

internal var _thread: _swift_CFThreadRef?

public convenience override init() {

self.init(condition: 0)

}

public init(condition: Int) {

_value = condition

}

open func lock() {

let _ = lock(before: Date.distantFuture)

}

open func unlock() {

_cond.lock()

_thread = nil

}

open var condition: Int {

return _value

}

open func lock(whenCondition condition: Int) {

let _ = lock(whenCondition: condition, before: Date.distantFuture)

}

open func `try`() -> Bool {

return lock(before: Date.distantPast)

}

open func tryLock(whenCondition condition: Int) -> Bool {

return lock(whenCondition: condition, before: Date.distantPast)

}

open func unlock(withCondition condition: Int) {

_cond.lock()

_thread = nil

_value = condition

_cond.broadcast()

_cond.unlock()

}

open func lock(before limit: Date) -> Bool {

_cond.lock()

while _thread != nil {

if !_cond.wait(until: limit) {

_cond.unlock()

return false

}

}

_thread = pthread_self()

_cond.unlock()

return true

}

open func lock(whenCondition condition: Int, before limit: Date) -> Bool {

_cond.lock()

while _thread != nil || _value != condition {

if !_cond.wait(until: limit) {

_cond.unlock()

return false

}

}

_thread = pthread_self()

_cond.unlock()

return true

}

open var name: String?

}

可见NSConditionLock中有一个value和NSCondition

锁的底层都是对pthread的封装

读写锁

有时候一些场景,比如对文件的操作,我们会要求

  • 读写互斥
  • 写写互斥
  • 可以同时多个读操作
  • 写的时候不能阻塞流程 这种情况下要怎么设计呢 对于写我们可以使用dispatch_barrier_async函数,由多线程的知识知道,dispatch_barrier_async起到在同一个队列上的同步的效果,但是并不阻塞线程,所以可以满足写的时候不能阻塞流程写写互斥
- (void)setAryWithValue:(NSString *)value time:(int)time {

sleep(time);

dispatch_barrier_async(self.read_write_queue, ^{

[self.dataDic setValue:value forKey:@"name"];

NSLog(@"写情况--%@--%@",self.dataDic[@"name"], [NSThread currentThread]);

});

}

对于读,我们使用的是dispatch_sync函数,这是一个同步函数,保证在这个队列上同步,同时写也是在这个队列上,所以读和写互斥,dispatch_sync虽然是一个同步函数,但是读的时候我们使用多线程来读,dispatch_sync只是在当前线程同步,所以可以实现多读。

有人会疑问,那读使用dispatch_async不就好了,dispatch_async的话异步函数,会导致return value;value = self.dataDic[@"name"];执行的更快,从而返回的为空

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

for (int i = 0; i < 10; i++) {

dispatch_async(dispatch_get_global_queue(0, 0 ), ^{

NSLog(@"现在这个值是--%@", [self valueForDataDicWithKey:@"name"]);

});

}

}

- (NSString *)valueForDataDic{

__block NSString *value;

dispatch_sync(self.read_write_queue, ^{

value = self.dataDic[@"name"];

NSLog(@"读情况--%@--%@",self.dataDic[@"name"], [NSThread currentThread]);

});

return value;

}