iOS中的锁——由属性atomic想到的线程安全

本文不介绍各种锁的高级用法,只是整理锁相关的知识点,帮助理解。

锁的作用

防止在多线程(多任务)的情况下对共享资源(临界资源)的脏读或者脏写。

自旋锁和互斥锁

共同点:都能保证同一时刻只能有一个线程操作锁住的代码。都能保证线程安全。
不同点

  • 互斥锁(mutex):当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕(sleep-waiting),当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
  • 自旋锁(Spin lock):当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(busy-waiting),当上一个线程的任务执行完毕,下一个线程会立即执行。
  • 由于自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁
  • 自旋锁会一直占用CPU,也可能会造成死锁

自旋锁有bug! ibireme大神的文章《不再安全的 OSSpinLock》
不同优先级线程调度算法会有优先级反转问题,比如低优先级获锁访问资源,高优先级尝试访问时会等待,这时低优先级又没法争过高优先级导致任务无法完成lock释放不了。

1. 原子操作

  • nonatomic:非原子属性,非线程安全,适合小内存移动设备
  • atomic:原子属性,default,线程安全(内部使用自旋锁),消耗大量资源
    • 单写多读,只为setter方法加锁,不影响getter

    • 相关代码如下:

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

很容易理解的代码,可变拷贝和不可变拷贝会开辟新的空间,两者皆不是则持有(引用计数+1),相比nonatomic只是多了一步锁操作。

2. synchronized 条件锁

使用最简单,性能也最差。

@synchronized(obj) {
    // 内部会添加异常处理,所以耗时
    NSLog(@"自动加锁,自动解锁,自动销毁");
}

obj为该锁的唯一标识,只有当标识相同时,才为满足互斥

3. dispatch_semaphore 信号量

用于线程同步,有序访问。不支持递归。

  • 创建信号:dispatch_semaphore_create(long value) 传入值必须 >=0, 若传入为 0 则阻塞线程并等待timeout,时间到后会执行其后的语句
  • 等待信号到达:dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) 使得signal-1
  • 发送信号,解除等待状态:dispatch_semaphore_signal(dispatch_semaphore_t deem)使得signal+1

这里顺便写一下dispatch_barrier,一个dispatch_barrier允许在一个并发队列(如果是串行队列或全局队列相当于dispatch_(a)sync)中创建一个同步点。当在并发队列中遇到一个barrier,他会延迟执行barrier的block,等待所有在barrier之前提交的blocks执行结束。 这时,barrier block自己开始执行。 之后, 队列继续正常的执行操作。
dispatch_barrier_asyncdispatch_barrier_sync的区别在于是否会等待自己的block完成再执行后面的任务。

一个同步访问网络的例子
- (void)syncRequestWithUrl:(NSURL*)url {
    NSMutableURLRequest *req = [[NSMutableURLRequest alloc]initWithURL:url];
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
    __block NSURLSessionTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (!error && data) {

        } else {
            NSLog(@"error: %@",[error description]);
        }
        dispatch_semaphore_signal(semaphore);
    }];
    [dataTask resume];
        
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

4. pthread_mutex 互斥锁

Facebook的 KVOController 有使用到。(先使用的是OSSpinLock,由于自旋锁的优先级反转问题后改用pthread_mutex)

使用需导入头文件:
#import <pthread.h>

  • 声明:pthread_mutex_t _mutex;
  • 初始化:pthread_mutex_init(&_mutex, NULL);
  • 加锁:pthread_mutex_lock(&_mutex);
  • 解锁:pthread_mutex_unlock(&_mutex);
  • 销毁:pthread_mutex_destroy(&_mutex);

5. pthread_mutex(recursive) 递归锁

递归锁允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。不会造成死锁。

static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
pthread_mutex_init(&pLock, &attr); // 初始化的时候带入参数
pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用

6. OSSpinLock 自旋锁

是性能最佳的锁,但由于线程调度算法(高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰)优先级反转的原因逐渐被其他锁取代。不支持递归。

使用需导入头文件
#import <libkern/OSAtomic.h>

  • OSSpinLock oslock = OS_SPINLOCK_INIT;:默认值为 0,在 locked 状态时就会大于 0,unlocked状态下为 0
  • OSSpinLockLock(&oslock);:上锁
  • OSSpinLockUnlock(&oslock);:解锁
  • OSSpinLockTry(&oslock):尝试加锁,可以加锁则立即加锁并返回YES,反之返回NO

提一下os_unfair_lock,苹果解决优先级反转的问题整出的。。
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);

7. NSLock

Cocoa提供的最基本的锁对象,实际是在内部封装了pthread_mutex。对象锁均实现了NSLocking协议。

@protocol NSLocking

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

@end
  • trylock:能加锁返回YES并执行加锁操作,相当于lock,反之返回NO
  • lockBeforeDate:表示会在传入的时间内尝试加锁,若能加锁则执行加锁操作并返回YES,反之返回NO

8. NSRecursiveLock 递归锁

NSLock的递归版本,解决在循环或递归时造成的死锁问题。使用同上。

9. NSCondition

最基本的条件锁,底层是通过条件变量(condition variable) pthread_cond_t 来实现,实际上封装了一个互斥锁和条件变量。手动控制线程waitsignal

  • wait:进入等待状态
  • waitUntilDate::让一个线程等待一定的时间
  • signal:唤醒一个等待的线程
  • broadcast:唤醒所有等待的线程

10. NSConditionLock 条件锁

API名说明一切...

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

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

性能相对较差,但使用NSConditionLock可以处理任务间的依赖关系。

Additional

11. pthread_rwlock 读写锁

读写锁是用来解决文件读写问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。

  • 当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行
  • 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 读模式
pthread_rwlock_wrlock(&lock);
// 写模式
pthread_rwlock_rdlock(&lock);
// 读模式或者写模式的解锁
pthread_rwlock_unlock(&lock);

对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。

12. NSDistributedLock 分布式锁

Mac开发使用,mark

引用

更详细的资料参考:

iOS 开发中的八种锁
iOS中保证线程安全的几种方式与性能对比
深入理解iOS开发中的锁

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,512评论 0 6
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 656评论 0 0
  • iOS线程安全的锁与性能对比 一、锁的基本使用方法 1.1、@synchronized 这是我们最熟悉的枷锁方式,...
    Jacky_Yang阅读 2,212评论 0 17
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 789评论 0 2
  • 前言 iOS开发中由于各种第三方库的高度封装,对锁的使用很少,刚好之前面试中被问到的关于并发编程锁的问题,都是一知...
    喵渣渣阅读 3,690评论 0 33