GCD多线程安全

在iOS中,多线程编程用得最多的是GCD。
与其他平台或语言不同的是,GCD管理了线程的创建、执行和销毁,使用者无需维护线程资源,减少了许多冗余的线程维护代码。
而GCD暴露了一些任务接口,允许我们通过函数式编程方式,简单直接快速地执行自己的任务,而这个任务直接关联的事GCD 中的队列Queue。队列有串行也有并发,串行的支持FIFO,并发的支持多任务同步执行,对于iOS编程人员,需要了解的就是队列这个概念。
虽然我们已经无需过多关注线程,但是在开发过程中,我们常遇到数据安全,死锁等问题,而这些问题都是线程处理业务的时候产生的,所以我们还是要对GCD多线程的基本原理有所了解。

队列&线程

如果你接触过数据结构编程,你大概会了解诸如链表、栈、树、队列等基本的结构模型。它们都有各自的特性,对于数据的插入、查询、删除等操作都有特定的方式,能够适配各种需求条件。
而队列就是以 先进先出(FIFO) 为特点,很好支撑我们日常中涉及的“排队”业务。那么在GCD中,队列也是有这个特点,不过只是业务的存取罢了,对于业务的执行,不一定是FIFO。CGD队列有两种,串行队列,并发队列。

串行队列 的业务执行是严格遵循FIFO的。我们知道执行业务是在dispatch中发生,而dispatch常见的有两种,dispatch_sync(同步执行)和dispatch_async (异步执行)。同步和异步是相对于当前的线程来说的,同步的任务将在当前线程执行,不管存取任务的队列是串行还是并发的。而 异步的任务会在非当前线程执行(也可以在当前线程执行,不过需要当前线程完成上一个任务之后才可能执行当前任务),不过在串行队列的任务异步执行的时候,只会开辟一条新的线程,因为要满足串行队列的业务执行的FIFO特性。

并发队列 的业务执行不会遵循FIFO。并发队列在同步执行的时候,会将任务放到当前线程执行。在执行异步任务的时候,在需要的条件下会开辟多条线程,而且任务执行顺序也得不到保证。

特别说明:iOS中GCD创建的线程是有限的,在线程数量达到极限的时候,队列任务就会等待其他线程释放之后才能够继续执行。

数据安全

我们在编程过程中,一直要注意一个问题,就是数据读写安全。而这个问题,大多出现在多线程业务执行的过程中。
考虑一下当你在线程A中执行业务的时候,读取变量q;而与此同时线程B在业务执行过程中正在修改变量q,此时就会产生读写冲突,很有可能会导致程序奔溃。
为了解决读写冲突问题,我们常会用到 。由于不同业务要求,产生了许多种类型的锁,在iOS中有普通锁,条件锁,递归锁等等,还有信号量等其他一些辅助策略。
在OC中,我们常会用到 @synchronized 来达到代码块安全执行的效果。要强调的是,@synchronized本质上是一个递归锁,在同个线程中无效(同线程嵌套调用,不会有阻隔作用),在不同的线程中则有效。
而在swift中,没有@synchronized , 但有替代的objc_sync_enter/objc_sync_exit组合。如果你常用该组合,建议你重新封装一下:

public class func syncExecute<T>(_ object: Any , function : () -> T) ->T{
        objc_sync_enter(object)
        defer {
            objc_sync_exit(object)
        }
        return function()
    }

然后在使用的地方调用syncExecute方法就好了。

当然,在这个组合中,大家也可以使用NSLock的lock()/unlock().但是要注意同线程操作,很大风险会导致死锁问题,要更加格外小心。

数组和字典

在日常开发过程中,数组和字典这两种基本类型是比较常用的,不可变(OC说法)或者常量(swift说法)的数组和字典是线程安全(thread-safety)的,但是可变的数组和字典都是非线程安全的。那么我们在操作可变数组或字典的读写时,要注意数据读写冲突,此时可以在数组或字典的读取,插入,删除等操作加锁,但是往往在程序多线程中会使用到许多的可变的数组和字典,假如对每个数组或字典维护一把锁,很容易想象到,你需要维护很多锁,在程序业务没有过多交叉的时候,这些锁的维护都很简单,一旦业务变得复杂,维护成本就会剧升。
为了解决维护难得问题,我们使用自定义的线程安全数组和字典类。

以下是线程安全数组:

import Foundation

@objc
class TSArray:NSObject {
    static let DEFAULT_QUEUE_LABEL = "SyncArrayAccess"
    private var array = [Any]()
    private var accessQueue : DispatchQueue
    
    @objc
    init(with queueName:String?){
        guard let nonilQueueName = queueName else{
            self.accessQueue = DispatchQueue(label: TSArray.DEFAULT_QUEUE_LABEL, attributes: .concurrent)
            return
        }
        self.accessQueue = DispatchQueue(label: nonilQueueName, attributes: .concurrent)
    }
    
    @objc
    public func append(_ newElement:Any){
        //queue flag is barrier, so that current work item should be execute after the other work items finish in the queue
        self.accessQueue.async(flags:.barrier){
            self.array.append(newElement)
        }
    }
    @objc
    public func update(_ elements:[Any]){
        //
        self.accessQueue.async(flags:.barrier){
            self.array.removeAll()
            self.array.append(contentsOf: elements)
        }
    }
    @objc
    public func remove(at index: Int) {
        //queue flag is barrier, so that current work item should be execute after the other work items finish in the queue
        self.accessQueue.async(flags:.barrier) {
            self.array.remove(at: index)
        }
    }
    @objc
    public func removeAll(){
        self.accessQueue.async(flags:.barrier){
            self.array.removeAll()
        }
    }
    @objc
    public var count: Int {
        var count = 0
        
        self.accessQueue.sync {
            count = self.array.count
        }
        
        return count
    }
    @objc
    public subscript(index: Int) -> Any? {
        set {
            self.accessQueue.async(flags:.barrier) {
                if newValue != nil{
                    self.array[index] = newValue!
                }
            }
        }
        get {
            var element : Any? = nil
            self.accessQueue.sync {
                if index < self.array.count{
                    element = self.array[index]
                }
            }
            
            return element
        }
    }
    @objc
    public func first() -> Any? {
        var element: Any?
        
        self.accessQueue.sync {
            if !self.array.isEmpty {
                element = self.array[0]
            }
        }
        
        return element
    }
    public func first<T>(where predicate: (T) -> Bool) -> T?{
        var element: T?
        self.accessQueue.sync {
            element = self.array.first(where: { (value) -> Bool in
                guard let tValue = value as? T else{
                    return false
                }
                return predicate(tValue)
            }) as? T
        }
        return element
    }
    public func index<T>(where predicate: (T) -> Bool) -> Int?{
        var index: Int?
        self.accessQueue.sync {
            index = self.array.firstIndex(where: { (value) -> Bool in
                guard let tValue = value as? T else{
                    return false
                }
                return predicate(tValue)
            })
        }
        return index
    }
    
    
    //MARK: -Deep copy value.
    //It means value copy, not referrence copy.
    @objc public var deepCopyValues:[Any]{
        get{
            var allValues = [Any]()
            self.accessQueue.sync {
                for element in self.array{
                    guard let object = element as? NSCopying else{
                        allValues.append(element)
                        continue
                    }
                    allValues.append(object.copy())
                }
            }
            return allValues
        }
    }
    //MARK: - Refferrence copy.
    @objc public var refCopyValues:[Any]{
        get{
            var allValues:[Any]!
            self.accessQueue.sync {
                allValues = self.array
            }
            return allValues
        }
    }
    
}

以下是线程安全字典:

import Foundation

@objc
class TSDictionary: NSObject {
    private var dict = [AnyHashable:Any]()
    private let accessQueue = DispatchQueue(label: "SyncDictAccess", attributes: DispatchQueue.Attributes.concurrent)
    
    public func update(_ newDict:[AnyHashable:Any], valueReplaceFilter:((_ oldValue:Any , _ newValue:Any) -> Void)? = nil){
        self.accessQueue.async(flags:.barrier) {
            guard let notNilFilter = valueReplaceFilter else{
                //If filter is nil, we should update dict directly
                self.dict.removeAll()
                for (key,value) in newDict {
                    self.dict[key] = value
                }
                return
            }
            //Else we should change the value by filter
            var newMergeDict = [AnyHashable:Any]()
            for(key,newValue) in newDict{
                guard let oldDict = self.dict.first(where: { $0.key == key}) else{
                    //Cannot find in the old dict, so we add it into the merge-dict
                    newMergeDict[key] = newValue
                    continue
                }
                //Found in the old-dict, we should update with value filter,and then add into the merge-dict
                notNilFilter(oldDict.value,newValue)
                newMergeDict[key] = oldDict.value
            }
            
            //Finally we should replace the old-dict with the merge-dict
            self.dict = newMergeDict
        }
    }
    
    @objc
    public subscript(key:AnyHashable ) -> Any? {
        set{
            self.accessQueue.async(flags:.barrier){
                self.dict[key] = newValue
            }
        }
        get{
            var value:Any?
            self.accessQueue.sync {
                value = self.dict[key]
            }
            return value
        }
    }
    public func removeAll(){
        self.accessQueue.async(flags:.barrier){
            self.dict.removeAll()
        }
    }
    public func removeValue(forKey key: AnyHashable) -> Any?{
        var value:Any?
        self.accessQueue.async(flags:.barrier) {
            value = self.dict.removeValue(forKey: key)
        }
        return value
    }
    
    public func filter<K:Hashable,T>(_ isIncluded: (Dictionary<K, T>.Element) -> Bool)  -> [Dictionary<K, T>.Key : Dictionary<K, T>.Value]{
        var filterValue = Dictionary<K, T>()
        self.accessQueue.sync {
            let value = self.dict.filter{ (element) -> Bool in
                guard let guardElement = element as? Dictionary<K, T>.Element else{
                    return false
                }
                return isIncluded(guardElement)
            }
            guard let guardValue = value as? Dictionary<K, T> else{
                return
            }
            filterValue = guardValue
        }
        return filterValue
    }
    
    public func first<K:Hashable,T>(where predicate: ((key: K, value: T))  -> Bool)  -> (key: K, value: T)?{
        var firstValue:(key: K, value: T)?
        self.accessQueue.sync {
            firstValue = self.dict.first(where: { (element) -> Bool in
                guard let guardElement = element as? (key: K, value: T) else{
                    return false
                }
                return predicate(guardElement)
            }) as? (key: K, value: T)
        }
        return firstValue
    }
    
    //MARK: -Deep copy dict
    public func deepCopyDict() -> [AnyHashable:Any]{
        var newDict = [AnyHashable:Any]()
        self.accessQueue.sync {
            for (key,value) in self.dict{
                guard let objectValue = value as? NSCopying else{
                    newDict[key] = value
                    continue
                }
                newDict[key] = objectValue.copy()
            }
        }
        return newDict
    }
    //MARK: - Referrence copy Dict.
    public func refCopyDict() -> [AnyHashable:Any]{
        var newDict:[AnyHashable:Any]!
        self.accessQueue.sync {
            newDict = self.dict
        }
        return newDict
    }
    //MARK: -Deep copy values.
    public var deepCopyValues:[Any]{
        get{
            var newValues = [Any]()
            self.accessQueue.sync {
                for value in self.dict.values {
                    guard let objectValue = value as? NSCopying else{
                        newValues.append(value)
                        continue
                    }
                    newValues.append(objectValue.copy())
                    
                }
            }
            return newValues
        }
    }
    //MARK: -Referrence cop values
    public var values:[Any]{
        get{
            var newValues = [Any]()
            self.accessQueue.sync {
                for value in self.dict.values{
                    newValues.append(value)
                }
            }
            return newValues
        }
    }

}

代码中使用到GCD栅栏来写如数据,使用sync同步读取数据。当然大家也可以使用NSLock来替代的操作。
然后在需要用到安全数组或字典的地方使用如下代码:

TSArray array = TSArray()
或
TSDictionary dict = TSDictionary()

你可以先不可变数组或字典那样,只关心操作,无需关心数据安全。

数据同步

多线程数据同步在我看来也是数据安全的一个话题。这个场景可以是这样的:
数据A更新的同时,更新数据B,只有等到更新数据B之后,才可以更新数据C。 A和B是同步的。
面临这样的问题的时候,我们最直接的办法是给A、B加锁,例如代码如下:

lock(a)
update A{
lock(b)
update B{
....
....
....
}
unlock(b)
}
unlock(a)
update C

此时看似完美的一段代码逻辑,却会隐藏着很大的风险。假如一不小心,你在其他地方犯了以下错误:

lock(b)
...
...
lock(a)
...
...
unlock(a)
...
...
unlock(b)

那就很可能导致死锁了,这个死锁问题还不是一定会重现,所以发现问题也纯靠运气了。

除了以上这种数据同步会导致一些很隐晦的死锁问题之外,还有许多其他的出现死锁的情况。

线程死锁

除了数据同步过程中不注意嵌套锁会导致死锁外,还有可能在GCD队列任务调度的时候发生。诸如以下情况:

情景一

//queue是个串行队列

queue.async{
//业务A
...
...
queue.sync{
//业务B
...
...
}
...
..
}

在串行队列中,业务是一个个有顺序执行的,所以这里业务A还没执行完的时候执行业务B,就会被卡死。但如果queue是并发队列的话,就没有问题。

情景二

//queue1,queue2是个串行队列

queue1.async{
//业务A
...
...
queue2.sync{
//业务B
...
...
queue1.sync{
//业务C
...
..
}
...
..
}

这里是一种比较简单的队列嵌套调用的情景,原理很简单:业务A等待业务B完成后继续完成余下工作,而业务B又必须等待业务C完成后才能继续进行,最后业务C也要等待队列queue1的业务A完成后才可以进行,就进入了一个业务死循环,业务A,业务B和业务C所在的线程被卡死。

还有诸多复杂的场景,以上举例只为了让大家对线程死锁有更好的理解。

其实多线程编程是一个很有艺术性的进行,遇到线程安全问题,只要多加思考,你会发现许许多多很有意思的情景。

如有问题,请留言...

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

推荐阅读更多精彩内容