第十四篇:iOS锁

自旋锁,互斥锁,读写锁
自旋锁就是一个忙等状态 do--while
互斥锁就是一个闲等,可以使得CPU进行休眠去做别的处理

下面是iOS中所用到的锁:执行10万次所用的时间

OSSpinLock:                   1.70 ms  自旋锁  iOS10后被移除,因为其有优先级占用,苹果用os_unfair_lock这个互斥锁来代替被废除的OSSpinLock锁

dispatch_semaphore:           3.19 ms
pthread_mutex:                2.18 ms
NSCondition:                  3.33 ms
NSLock:                       2.93 ms
pthread_mutex(recursive):     3.08 ms
NSRecursiveLock:              4.66 ms
NSConditionLock:             10.82 ms
@synchronized:                9.09 ms

下面代码打印后,我们发现其并不是以49依次递减的,这说明self.count这个是线程不安全的。如果改成atomic原子属性,其也不会保证线程安全的。这个是因为 self.count --这句代码的执行,其实就是执行了set和get方法,这样同时执行原子属性无法同时满足线程安全,只满足了一边的线程安全。

解决这个问题就需要用到锁功能。

@property (nonatomic ,assign) int count;

 self.count = 50;

- (void)test {
    for (int i = 0; i < 10; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.count --;
            NSLog(@"%d",self.count);
        });
    }
}
2022-05-31 15:11:04.921282+0800 iOSLockDemo[4510:100720] 49
2022-05-31 15:11:04.921301+0800 iOSLockDemo[4510:100719] 47
2022-05-31 15:11:04.921291+0800 iOSLockDemo[4510:100732] 48
2022-05-31 15:11:04.921311+0800 iOSLockDemo[4510:100727] 46
2022-05-31 15:11:04.921334+0800 iOSLockDemo[4510:100733] 45
2022-05-31 15:11:04.921342+0800 iOSLockDemo[4510:100720] 44
2022-05-31 15:11:04.921355+0800 iOSLockDemo[4510:100734] 43
2022-05-31 15:11:04.921397+0800 iOSLockDemo[4510:100732] 42
2022-05-31 15:11:04.921410+0800 iOSLockDemo[4510:100719] 41
2022-05-31 15:11:04.921458+0800 iOSLockDemo[4510:100724] 40

下面是set属性源码:

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];//如果是原子操作就加一把spinlock_t锁
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

下面是get属性方法源码:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;//如果是非原子的直接返回
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];//是原子就会加spinlock_t把锁
    slotlock.lock();//加锁
    id value = objc_retain(*slot);
    slotlock.unlock();//解锁
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

os_unfair_lock互斥锁

当加了os_unfair_lock锁后发现其打印就是按顺序进行打印了

- (void)test {
    for (int i = 0; i < 10; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self unfairLock_test];
        });
    }
}

-(void)unfairLock_test {
    os_unfair_lock_lock(&_unfairLock);
    self.count --;
    NSLog(@"%d",self.count);
    os_unfair_lock_unlock(&_unfairLock);
}

2022-05-31 16:38:19.268185+0800 iOSLockDemo[6256:161713] 49
2022-05-31 16:38:19.268249+0800 iOSLockDemo[6256:161718] 48
2022-05-31 16:38:19.268320+0800 iOSLockDemo[6256:161718] 47
2022-05-31 16:38:19.268413+0800 iOSLockDemo[6256:161713] 46
2022-05-31 16:38:19.268449+0800 iOSLockDemo[6256:161715] 45
2022-05-31 16:38:19.268492+0800 iOSLockDemo[6256:161712] 44
2022-05-31 16:38:19.268539+0800 iOSLockDemo[6256:161726] 43
2022-05-31 16:38:19.268621+0800 iOSLockDemo[6256:161717] 42
2022-05-31 16:38:19.268733+0800 iOSLockDemo[6256:161725] 41
2022-05-31 16:38:19.268998+0800 iOSLockDemo[6256:161719] 40
WechatIMG2017.jpeg

NSLock锁

其是对Pthreds的封装

- (void)test {
    for (int i = 0; i < 10; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self nslock_test];
        });
    }
}

-(void)nslock_test {
    [self.iLock lock];
    self.count --;
    NSLog(@"%d",self.count);
    [self.iLock unlock];
}
WechatIMG2018.jpeg

NSConditon锁

其是遵循了NSLocking协议,所以其有lock和unlock方法。

下面代码可以模拟一个抢票的机制

- (void)nscondition_test {
    for (int i = 0; i < 50; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self lg_production];
        });
    }
    for (int i = 0; i < 100; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self lg_consumption];
        });
    }
}

- (void)lg_production {
    [self.iCondition lock];
    self.count ++;
    NSLog(@"生产了一个产品,现有产品 : %d个",self.count);
    [self.iCondition signal];//唤醒
    [self.iCondition unlock];
}

- (void)lg_consumption {//这里依然会有负数打印,这个是因为是系统的bug,虚假唤醒
    [self.iCondition lock];
    while (self.count == 0) {//等待,如果没有这个会打印负数,用while就会避免虚假唤醒
        [self.iCondition wait];
    }
    self.count --;
    NSLog(@"消费了一个产品,现有产品: %d个",self.count);
    [self.iCondition unlock];
}

WechatIMG2019.jpeg

NSConditonLock锁

其是对NSConditon的又一次封装,通过下面的锁,可以使得线程按1,2,3顺序打印

- (void)lg_testConditonLock{
    
    self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3];//3个线程锁

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:3];//加锁
        NSLog(@"线程 1");
        [self.iConditionLock unlockWithCondition:2];//解锁变成2
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:2];
        NSLog(@"线程 2");
        [self.iConditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:1];
        NSLog(@"线程 3");
        [self.iConditionLock unlockWithCondition:0];
    });
}
WechatIMG2020.jpeg

NSRecursiveLock锁

这个是个递归锁,这个只能保证在同一个线程里操作

锁在同一时刻只能被一条线程拥有
递归锁同一时刻能被多条线程锁拥有

-(void)recursiveLock_test {
    
    [self.iRecursiveLock lock];
    self.count --;
    NSLog(@"%d",self.count);
    [self.iRecursiveLock unlock];
}

- (void)test {
    for (int i = 0; i < 10; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self recursiveLock_test];
        });
    }
}

- (void)recursiveTest {
 
如果这里加了for循环进行几次,每次调用下面的代码就会出现奔溃。这里是因为加了for就会有多个线程,递归锁同一时刻能被多条线程锁拥有,但是在解锁的时候没有按顺序就会奔溃,如果要解决这个问题可以添加 @synchronized这个锁就可以解决。


    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^recursiveMethod)(int);
        recursiveMethod = ^(int value){//block的递归调用,递归操作需要用到self.iRecursiveLock这种递归锁
            if (value > 0) {
                [self.iRecursiveLock lock];
                NSLog(@"%d",value);
                recursiveMethod(value - 1);
                [self.iRecursiveLock unlock];
            }
        };
        recursiveMethod(10);
    });
}

WechatIMG2021.jpeg

pthread_mutex锁

其是c语言底层写的

- (void)lg_pthread_mutex {
    
    //非递归
    pthread_mutex_t lock0;
    pthread_mutex_init(&lock0, NULL);
    pthread_mutex_lock(&lock0);
    pthread_mutex_unlock(&lock0);
    pthread_mutex_destroy(&lock0);
    
    //递归
    pthread_mutex_t lock;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_lock(&lock);
    pthread_mutex_unlock(&lock);
    pthread_mutex_destroy(&lock);
}
WechatIMG2022.jpeg

读写锁

读和写互斥,多读单写,下面我们用GCD来实现

- (NSString *)lg_read {
    // 异步读取
    __block NSString *ret;
    dispatch_sync(self.iQueue, ^{
        // 读取的代码
        ret = self.dataDic[@"name"];
    });
    NSLog(@"%@",ret);
    return ret;
}

- (void)lg_write: (NSString *)name {
    // 写操作
    dispatch_barrier_async(self.iQueue, ^{
        [self.dataDic setObject:name forKey:@"name"];
    });
}

@synchronized锁

首先在main函数里添加 @synchronized (obj) 这个方法,然后用clang命令可以打印出main.cpp文件。

#import <Cocoa/Cocoa.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        NSObject *obj = [NSObject alloc];
        @synchronized (obj) {
            
        }
    }
    return NSApplicationMain(argc, argv);
}

下面是得到的cpp

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
        { id _rethrow = 0; id _sync_obj = (id)obj; objc_sync_enter(_sync_obj);
try {
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}//~_SYNC_EXIT() 是一个析构函数
    id sync_exit;
    }
    
    _sync_exit(_sync_obj);//结构体_SYNC_EXIT的构造函数

        } catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
    ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
    id rethrow;
    } _fin_force_rethow(_rethrow);}
}

    }
    return NSApplicationMain(argc, argv);
}

其实在synchronized里有用到下面两个函数,objc_sync_exit和objc_sync_enter。这里的SyncData是一个单向的链表。其中其里面有个spinlock_t,这个其实就是 os_unfair_lock锁。

WechatIMG2027.jpeg
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
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
         

下面的StripedMap是用来缓存带spinlock锁能力的类或者结构体,如果一张表存储一个那么会浪费内存,如果一张表存储多个那么效率会很低,所以就执行设置特定的个数StripeCount = 8(非模拟器上) 或者StripeCount = 64 (模拟器上)。其实这个SyncList里是存着sDataLists这样的数据形式。

在线程的局部存储空间里也就是TLS,其里面缓存有synchronized的信息.也就是线程里有单独开辟了一小部分空间让其存储一些关于线程的数据。

static StripedMap<SyncList> sDataLists;

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
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;
        }
    }

下面是_objc_pthread_data 这个数据结构体里存syncCache信息,这里就是关于synchronize的信息。


typedef struct {
    struct _objc_initializing_classes *initializingClasses; // for +initialize
    struct SyncCache *syncCache;  // for @synchronize
    struct alt_handler_list *handlerList;  // for exception alt handlers
    char *printableNames[4];  // temporary demangled names for logging
    const char **classNameLookups;  // for objc_getClass() hooks
    unsigned classNameLookupsAllocated;
    unsigned classNameLookupsUsed;

    // If you add new fields here, don't forget to update 
    // _objc_pthread_destroyspecific()

} _objc_pthread_data;

下面SyncCache里是存着SyncCacheItem,在SyncCacheItem里有lockCount,这个是记录当前线程的加锁次数

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

下面是加锁的时候lockCount会加1,解锁的时候lockCount会减1,下面的源码也就是在线程的缓存里去找和我们这个 加锁的对象SyncData,找到的话就会返回。这里有快速的缓存,去查找拿取,没有的话就去objc_pthresd_data里正常查找。如果还没有就会去表里找(StripeCount数量表中),如果还没有那就会创建一个添加到表里。

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
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

下面用一个图示来显示整个过程:
其中lockcount是指当前线程的加锁数,thredcount是指当前锁被几条线程所拥有。

WechatIMG2033.jpeg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • iOS之武功秘籍 文章汇总[https://www.jianshu.com/p/07991e5b1c30] 写在前...
    長茳阅读 573评论 0 2
  • 苹果官方资源opensource[https://opensource.apple.com/releases/]o...
    顶级蜗牛阅读 795评论 0 7
  • 写在前面 多线程在日常开发中能起到性能优化的作用,但是一旦没用好就会造成线程不安全,本文就来讲讲如何保证线程安全 ...
    M_慕宸阅读 529评论 0 5
  • 前言 多线程开发是性能优化常用的技术,在多线程开发中,线程安全是绕不开的一个话题。线程安全的定义,在之前的文章中也...
    虎啦吧唧的猴阅读 1,060评论 0 2
  • 本文主要介绍常见的锁,以及synchronized、NSLock、递归锁、条件锁的底层分析 锁 先看一张大家都非常...
    北京_小海阅读 494评论 0 0