关于“锁”的一些事儿

多线程在日常开发中会时不时遇到。首先APP会有一个主线程(UI线程),处理一些UI相关的逻辑。但是牵扯到网络、数据库等耗时的操作需要新开辟线程处理,避免“卡住”主线程,给用户留下不好的印象。多线程的好处不言而喻:幕后做事,不影响明面上的事儿。但是也有一些需要注意的地方,其中“资源抢夺”就是需要特别注意的一点。

资源抢夺

所谓资源抢夺就是多个线程同时操作一个数据。

下面这段代码很简单,就是往Preferences文件中存一个值,并读取出来输出

    override func viewDidLoad() {
        super.viewDidLoad()

        // 写
        saveData(key: identifier1, value: 1)
        // 读
        let result1 = readData(key: identifier1)
        print(" result1: \(String(describing: result1))")
        
        // 写
        saveData(key: identifier2, value: 2)
        // 读
        print("result2: \(String(describing: result1))")
    }

输出结果毫无疑问是
result1: 1
result2: 2

如果这么写

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // 线程一操作
        let queue1 = DispatchQueue(label: "queue1");
        queue1.async {[weak self] in
            // 写
            self?.saveData(key: identifier, value: 1)
            // 读
            let result = self?.readData(key: identifier) ?? ""
            print("queue1 result: \(String(describing: result))")
        }
        
        // 线程二操作
        let queue2 = DispatchQueue(label: "queue2");
        queue2.async {[weak self] in
            // 写
            self?.saveData(key: identifier, value: 2)
            // 读
            let result = self?.readData(key: identifier) ?? ""
            print("queue2 result: \(String(describing: result))")
        }
    }

通常会认为 queue1 先输出 1, 然后 queue2 再输出 2。 但实际上...
循环打印的结果
queue1 result: 1
queue2 result: 2
queue2 result: 1
queue2 result: 2
queue1 result: 2
queue2 result: 2
queue2 result: 2
queue1 result: 1

刚才代码中的 queue1要读取并写入, 但很有可能 queue2 这时候也运行了, 它在 queue1 的写入操作没有完成之前就做了读取操作。 这时候他们两个读到值都是0, 就会造成两个都输出1。线程的调度是由操作系统来控制的,如果 queue2 调用的时, queue1 正好写入完成,这时就能得到正确的输出结果。 可如果 queue2 调起的时候 queue1 还没写入完成,那么就会出现输出同样结果的现象。 这一切都是由操作系统来控制。

解决

1、NSLock

NSLock 是 iOS 提供给我们的一个 API 封装, 可以很好的解决资源抢夺问题。 NSLock 就是对线程加锁机制的一个封装
使用示例:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let lock = NSLock()
        
        for _ in 0..<100 {
            // 线程一操作
            let queue1 = DispatchQueue(label: "queue1");
            queue1.async {[weak self] in
                lock.lock() // 锁起来
                // 写
                self?.saveData(key: identifier, value: 1)
                
                // 读
                let result = self?.readData(key: identifier) ?? ""
                lock.unlock()  // 解锁
                
                print("queue1 result: \(String(describing: result))")
            }
            
            // 线程二操作
            let queue2 = DispatchQueue(label: "queue2");
            queue2.async {[weak self] in
                lock.lock() // 锁起来
                // 写
                self?.saveData(key: identifier, value: 2)
                
                // 读
                let result = self?.readData(key: identifier) ?? ""
                lock.unlock()  // 解锁
                
                print("queue2 result: \(String(describing: result))")
            }
        }
    }

循环打印的结果
queue1 result: 1
queue2 result: 2
queue1 result: 1
queue2 result: 2
queue1 result: 2
queue2 result: 2
queue1 result: 1
queue2 result: 2

互斥锁(pthread_mutex_lock)

从实现原理上来讲,Mutex(互斥锁)属于sleep-waiting类型的锁。例如在一个多核的机器上有两个线程p1和p2,分别运行在Core1和 Core2上。假设线程p1想要通过pthread_mutex_lock操作去得到一个临界区(Critical Section)的锁,而此时这个锁正被线程p2所持有,那么线程p1就会被阻塞 (blocking),Core1 会在此时进行上下文切换(Context Switch)将线程p1置于等待队列中,此时Core1就可以运行其他的任务(例如另一个线程p3),而不必进行忙等待。

自旋锁(Spin lock)

先插个话题:在OC中定义属性时,很多人会认为如果属性具备 nonatomic 特质,则不使用 “同步锁”。其实在属性设置方法中使用的是自旋锁。

旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。

虽然它的效率比互斥锁高,但是它也有些不足之处:

1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。
因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

总结

这里贴一张ibireme做的测试图,介绍了一些iOS 中的锁的API,及其效率


674591-176434d65ad6f5b6.png

挑几个我们常用且熟悉的啰嗦几句

@synchronized (属:互斥锁)

显然,这是我们最熟悉的加锁方式,因为这是OC层面的为我们封装的,使用起来简单粗暴。使用时 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(也就是锁池),通过对象的哈希值来得到对应的互斥锁。

-(void)criticalMethod  
{  
    @synchronized(self)  
    {  
        //关键代码;  
    }  
}  
NSLock(属:互斥锁)

NSLock 是OC 以对象的形式暴露给开发者的一种锁,它的实现非常简单,通过宏,定义了 lock 方法:
#define MLOCK - (void) lock{\ int err = pthread_mutex_lock(&_mutex);\ // 错误处理 ……}

NSLock只是在内部封装了一个pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,仅仅是内部pthread_mutex互斥锁的类型不同。通过宏定义,可以简化方法的定义。NSLock比pthread_mutex略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。

atomic原子操作(属:自旋锁)

即不可分割开的操作;该操作一定是在同一个cpu时间片中完成,这样即使线程被切换,多个线程也不会看到同一块内存中不完整的数据。如果属性具备 atomic 特质,则在属性设置方法中使用的是“自旋锁”。

什么情况下用什么锁?

1、总的来看,推荐pthread_mutex作为实际项目的首选方案;
2、对于耗时较大又易冲突的读操作,可以使用读写锁代替pthread_mutex;
3、如果确认仅有set/get的访问操作,可以选用原子操作属性;
4、对于性能要求苛刻,可以考虑使用OSSpinLock,需要确保加锁片段的耗时足够小;
5、条件锁基本上使用面向对象的NSCondition和NSConditionLock即可;
6、@synchronized则适用于低频场景如初始化或者紧急修复使用;

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

推荐阅读更多精彩内容

  • 引用自多线程编程指南应用程序里面多个线程的存在引发了多个执行线程安全访问资源的潜在问题。两个线程同时修改同一资源有...
    Mitchell阅读 1,929评论 1 7
  • iOS线程安全的锁与性能对比 一、锁的基本使用方法 1.1、@synchronized 这是我们最熟悉的枷锁方式,...
    Jacky_Yang阅读 2,133评论 0 17
  • 前言 iOS开发中由于各种第三方库的高度封装,对锁的使用很少,刚好之前面试中被问到的关于并发编程锁的问题,都是一知...
    喵渣渣阅读 3,618评论 0 33
  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,458评论 0 6
  • 教授讲,不是认识字的人,都能教语文!这是对自认为谁都能教语文,评价语文课的人错误认识的纠正,同时也是对语文教师提了...
    小水月阅读 433评论 1 8