在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所在的线程被卡死。
还有诸多复杂的场景,以上举例只为了让大家对线程死锁有更好的理解。
其实多线程编程是一个很有艺术性的进行,遇到线程安全问题,只要多加思考,你会发现许许多多很有意思的情景。