Object-C各种锁的实现

工作中我们常常会遇到多线程的问题,比如滑动界面的同时播放音频、子线程请求网络数据并处理等等。当不同的线程同时处理一部分数据,或者不同的线程处理任务有顺序要求时,就需要用到各种锁来保证安全。

总的来说, 安全锁分为两类: 自旋锁和互斥锁。 它们的详细分类如下图:

安全锁.jpg

自旋锁的实现原理是建立公用的临界区, 类似全局变量, 当一个线程在访问临界区内的资源时, 其它线程就需要等待, 直到临界区内的线程处理完毕离开, 才可以进入。 互斥锁也是建立公用临界区, 但是在访问已经占用的临界区时,访问线程会被睡眠, 直到公共区内的线程处理完才被唤醒。 两者的区别就像同样抢着去已经被占用的公共厕所, 自旋锁的处理方式是在厕所内把门锁上; 互斥锁厕所门口蹲着门卫, 直接把来的人干倒。 从实现方式上可以看出自旋锁的效率相对较高。下面介绍下各种锁的实现方式。

自旋锁

OSSpinLock

 __block OSSpinLock theLock = OS_SPINLOCK_INIT;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    OSSpinLockLock(&theLock);
    NSLog(@"需要线程同步的操作1");
    sleep(3);
    OSSpinLockUnlock(&theLock);
});

查询API文档看到OSSpinLock在iOS10.0已经废除, 换成了os_unfair_lock。 查询相关资料了解到是由于OSSpinLock会由于线程优先级反转, 可能会不安全.

os_unfair_lock

__block os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    os_unfair_lock_lock(&unfairLock);
    NSLog(@"需要线程同步的操作1");
    sleep(3);
    os_unfair_lock_unlock(&unfairLock);
    
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    os_unfair_lock_lock(&unfairLock);
    NSLog(@"需要线程同步的操作2");
    sleep(1);
    os_unfair_lock_unlock(&unfairLock);
});

打印的结果为:

2017-09-05 10:34:36.895281+0800 TimeTest[810:810984] 需要线程同步的操作2
2017-09-05 10:34:37.900904+0800 TimeTest[810:810977] 需要线程同步的操作1

可以看出优先级较高的线程确实会抢先申请锁资源。 但是在实际使用中, OSSpinLock不安全的情况却不多(暂时没有找到实际应用中不安全的场景)。

互斥锁

dispatch_semaphore_t

dispatch_semaphore_t signal;
signal = dispatch_semaphore_create(2);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"run task 1");
    sleep(1);
    NSLog(@"end task 1");
    dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"run task 2");
    sleep(2);
    NSLog(@"end task 2");
    dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"run task 3");
    sleep(3);
    NSLog(@"end task 3");
    dispatch_semaphore_signal(signal);
});

dispatch_semaphore_create设置线程并发数signal, 上面的代码将并发数设置为2。 dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)将设置的并发数signal减1,接收dsema和timeout两个参数。 当signal的信号为0时,该线程会等待timeout的时间后执行后面的代码。 dispatch_semaphore_signal将并发数signal加1, 一般 和dispatch_semaphore_signal成对出现。 上面的例子中, task1和task2会在signal分配的2个线程中执行, task3会在task1或者task2执行完成后再进入分配的两个线程执行。

pthread_mutex_lock

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    pthread_mutex_lock(&mutex);
    sleep(2);
    pthread_mutex_unlock(&mutex);
});

pthread_mutex_lock是NSLock, NSRecursiveLock的底层实现。 可以通过pthread_mutexattr_settype来设置创建锁的类型。

NSLock

NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    static void (^RecursiveMethod)(int);
    
    RecursiveMethod = ^(int value){
        [lock lock];
        if (value > 0) {
            NSLog(@"value = %zd", value);
            sleep(2);
            RecursiveMethod(value - 1);
        }
        [lock unlock];
    };
    RecursiveMethod(5);
});

上面的线程中的递归块RecursiveMethod,在调用lock方法后, 会在递归中再次调用lock方法申请锁资源,造成该线程睡眠而死锁。

*** -[NSLock lock]: deadlock (<NSLock: 0x1740dcee0> '(null)')
*** Break on _NSLockError() to debug.

采用NSRecursiveLock可以解决。

NSRecursiveLock

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    static void (^RecursiveMethod)(int);
    
    RecursiveMethod = ^(int value){
        [lock lock];
        if (value > 0) {
            NSLog(@"value = %zd", value);
            sleep(2);
            RecursiveMethod(value - 1);
        }
        [lock unlock];
    };
    RecursiveMethod(5);
});

NSConditionLock

NSConditionLock conditionClock = [[NSConditionLock alloc] initWithCondition:1];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [conditionClock lock];
    NSLog(@"Task1 begin");
    sleep(2);
    NSLog(@"Task1 end");
    [conditionClock unlockWithCondition:10];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [conditionClock lockWhenCondition:10];
    NSLog(@"Task2 begin");
    sleep(2);
    NSLog(@"Task2 end");
    [conditionClock unlockWithCondition:20];
});

conditionClock可以调用lock方法锁定线程, 或者调用lockWhenCondition方法, 在满足规定条件值时锁定线程, 用于不同线程间有数据交互的场景中。

时间对比
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);

CFAbsoluteTime startTime_pthread = CFAbsoluteTimeGetCurrent();
NSUInteger count = 10000 * 100;
for (int i = 0; i < count; i++) {    
    pthread_mutex_lock(&mutex);
    pthread_mutex_unlock(&mutex);    
}
CFAbsoluteTime endTime_pthread = CFAbsoluteTimeGetCurrent();
NSLog(@"time_pthread: %f", endTime_pthread - startTime_pthread);
CFAbsoluteTime startTime_syn = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < count; i++) {
    @synchronized (self) {
        
    }
}
CFAbsoluteTime endTime_syn = CFAbsoluteTimeGetCurrent();
NSLog(@"time_syn: %f", endTime_syn - startTime_syn);
__block os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT;
CFAbsoluteTime startTime_unfairLock = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < count; i++) {
    os_unfair_lock_lock(&unfairLock);
    os_unfair_lock_unlock(&unfairLock);
}
CFAbsoluteTime endTime_unfairLock = CFAbsoluteTimeGetCurrent();
NSLog(@"time_unfairLock: %f", endTime_unfairLock - startTime_unfairLock);

结果:

time_pthread: 0.233672
time_syn: 0.591735
time_unfairLock: 0.101001

将pthread_mutex_lock、@ synchronized、 os_unfair_lock分别加锁、解锁100万次, 可以看到它们占用的时间@ synchronized要大大高于pthread_mutex_lock和os_unfair_lock, 也印证了开始对自旋锁性能高于互斥锁的结论。

实际使用中, 简单场景优先推荐使用自旋锁, 用到互斥锁的地方可以根据具体应用场景来选择。

2020.04.22 更新
pthread_mutex_t 是互斥锁,通过 pthread_mutexattr_settype 的属性设置, 可以获得默认锁、递归锁和条件锁。OC中的NSLock、NSRecursiveLock、NSConditionLock 底层是通过 pthread_mutex_t 设置不同 type 实现的。

自旋锁使用时,线程并没有休眠,处于循环等待状态,会一直占用CPU资源,当线程等待时间过长时不适合使用。 OSSpinLock 线程优先级反转时不安全是因为低优先级的线程A获得互斥锁,使用公共区资源时,一个高优先级的线程B到来,无法获取锁。但是B并不会休眠,有因为其现场优先级较高,会继续抢占CPU资源,导致A中的任务不能顺利执行,这样会造成循环等待,最终死锁。

综上,自旋锁效率高,但是用在线程等待时间很短的场景;互斥锁效率低,会让线程等待,适用在线程等待时间长,或者等待时间不确定的场景中。

除了自旋锁和互斥锁,还有一种读写锁pthread_rwlock_rdlock, 可以实现读时并发,写时互斥,使用如下(采用swift5.0):

var rwlock = pthread_rwlock_t()
{ 
  pthread_rwlock_init(&rwlock, nil)
   testRWLock()
}
func testRWLock() {
        let queue = DispatchQueue(label: "aaa", attributes: .concurrent)
        for _ in 0 ..< 5 {
            queue.async {
                self.write()
            }
        }
        for _ in 0 ..< 5 {
            queue.async {
                self.read()
            }
        }
    }
    
    func read() {
        pthread_rwlock_rdlock(&rwlock)
        print("\(#function) \(Thread.current)")
        pthread_rwlock_unlock(&rwlock)
    }
    
    func write() {
        pthread_rwlock_wrlock(&rwlock)
        print("\(#function) \(Thread.current)")
        pthread_rwlock_unlock(&rwlock)
    }

喜欢和关注都是对我的鼓励和支持~

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,495评论 0 6
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 636评论 0 0
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 764评论 0 2
  • 在平时的开发中经常使用到多线程,在使用多线程的过程中,难免会遇到资源竞争的问题,那我们怎么来避免出现这种问题那? ...
    IAMCJ阅读 3,069评论 2 25
  • 前言 iOS开发中由于各种第三方库的高度封装,对锁的使用很少,刚好之前面试中被问到的关于并发编程锁的问题,都是一知...
    喵渣渣阅读 3,664评论 0 33