一、线程安全场景

多个线程中一起拜访同一块资源,也便是资源共享。多牵扯到对同一块数据的读写操作,或许引发数据紊乱问题。

比较经典的线程安全问题有购票和存钱取钱问题,为了阐明读写操作引发的数据紊乱问题,以存钱取钱问题来做个阐明。

1. 购票事例

iOS 线程安全和锁机制

用代码示例如下:

@IBAction func ticketSale() {
    tickets = 30
    let queue = DispatchQueue.global()
    queue.async {
      for _ in 0..<10 {
        self.sellTicket()
      }
    }
    queue.async {
      for _ in 0..<10 {
        self.sellTicket()
      }
    }
    queue.async {
      for _ in 0..<10 {
        self.sellTicket()
      }
    }
  }
  //卖票
  func sellTicket() {
    var oldTicket = tickets
    sleep(UInt32(0.2))
    oldTicket -= 1
    tickets = oldTicket
    print("还剩\(tickets)张票 ---- \(Thread.current)")
  }

一起有三条线程进行卖票,对余票进行减操作,三条线程每条卖10次,原定票数30,应该剩余票数为0,下面看下打印成果:

iOS 线程安全和锁机制

能够看到打印票数不为0

2. 存钱取钱事例

先用个图阐明

iOS 线程安全和锁机制

上图能够看出,存钱和取钱之后的余额理论上应该是500,但由于存钱取钱一起拜访并修改了余额,导致数据紊乱,终究余额或许变成了400,下面用代码做一下验证阐明:

//存钱取钱
  @IBAction func remainTest() {
    remain = 500
    let queue = DispatchQueue.global()
    queue.async {
      for _ in 0..<5 {
        self.saveMoney()
      }
    }
    queue.async {
      for _ in 0..<5 {
        self.drawMoney()
      }
    }
  }
  //存钱
  func saveMoney() {
   var oldRemain = remain
    sleep(2)
    oldRemain += 100
    remain = oldRemain
    print("存款100元,账户余额还剩\(remain)元 ----\(Thread.current)")
  }
 
  //取钱
  func drawMoney() {
    var oldRemain = remain
    sleep(2)
    oldRemain -= 50
    remain = oldRemain
    print("取款50元,账户余额还剩\(remain)元 ---- \(Thread.current)")
  }

上述代码存款5次100,取款5次50,终究的余额应该是 500 + 5 * 100 – 5 * 50 = 750

iOS 线程安全和锁机制

如图所示,能够看到在存款取款之间现已呈现紊乱了

上述两个事例之所以呈现数据紊乱问题,便是因为有多个线程一起操作了同一资源,导致数据不安全而呈现的。

那么遇到这个问题该怎样处理呢?自然而然的,咱们想到了对资源进行加锁处理,以此来确保线程安全,在同一时间,只答应一条线程拜访资源。

加锁的办法大概有以下几种:

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock

1. OSSpinLock 自旋锁

iOS 线程安全和锁机制

OSSpinLock 是自旋锁,在体系框架 libkern/OSAtomic

iOS 线程安全和锁机制

如图,体系供给了以下几个API

  • 定义lock let osspinlock = OSSpinLock()
  • OSSpinLockTry

官方给定的解说如下

Locks a spinlock if it would not block
return false, if the lock was already held by another thread,
return true, if it took the lock successfully.

测验加锁,加锁成功则持续,加锁失利则直接回来,不会堵塞线程

  • OSSpinLockLock
Although the lock operation spins, it employs various strategies to back
off if the lock is held. 

加锁成功则持续,加锁失利,则会堵塞线程,处于忙等状况

运用
@IBAction func ticketSale() {
    osspinlock = OSSpinLock()
    tickets = 30
    let queue = DispatchQueue.global()
    queue.async {
      for _ in 0..<10 {
        self.sellTicket()
      }
    }
    queue.async {
      for _ in 0..<10 {
        self.sellTicket()
      }
    }
    queue.async {
      for _ in 0..<10 {
        self.sellTicket()
      }
    }
  }
  //卖票
  func sellTicket() {
    OSSpinLockLock(&osspinlock)
    var oldTicket = tickets
    sleep(UInt32(0.2))
    oldTicket -= 1
    tickets = oldTicket
    print("还剩\(tickets)张票 ---- \(Thread.current)")
    OSSpinLockUnlock(&osspinlock)
  }

iOS 线程安全和锁机制

能够看到,终究的余票数量现已是正确的了,这儿要注意的是osspinlock需要做成全局变量或者特点,多个线程要用这同一把锁去加锁和解锁,假如每个线程各自生成锁,则达不到要加锁的意图了

那么自旋锁是怎样样做到加锁确保线程安全的呢? 先来介绍下让线程堵塞的两种办法:

  • 忙等:也便是自旋锁的原理,它本质上便是个while循环,不停地去判断加锁条件,自旋锁没有让线程真实的堵塞,只是将线程处在while循环中,体系CPU仍是会不停地分配资源来处理while循环指令。
  • 真实堵塞线程: 这是让线程休眠,类似于Runloop里的match_msg() 实现的作用,它借助体系内核指令,让线程真实停下来处于休眠状况,体系的CPU不再分配资源给线程,也不会再履行任何指令。体系内核用的是symcall指令来让线程进入休眠

它的原理便是,自旋锁在加锁失利时,让线程处于忙等状况,让线程停留在临界区之外,一旦加锁成功,就能够进入临界区对资源进行操作。

iOS 线程安全和锁机制

经过这个能够看到,苹果在iOS10之后就弃用了OSSpinLock,官方主张用 os_unfair_lock来代替,那么为什么要弃用呢?因为在iOS10之后线程能够设置优先级,在优先级配置下,能够发生优先级反转,使自旋锁卡住,自旋锁本身现已不再安全。

2. os_unfair_lock

os_unfair_lock 是苹果官方推荐的,自iOS10之后用来代替 OSSpinLock 的一种锁

  • os_unfair_lock_trylock: 测验加锁,加锁成功回来true,持续履行。加锁失利,则回来false,不会堵塞线程。
  • os_unfair_lock_lock: 加锁,加锁失利,堵塞线程持续等候。加锁成功,持续履行。
  • os_unfair_lock_unlock : 解锁

运用:

//卖票
  func sellTicket() {
    os_unfair_lock_lock(&unfairlock)
    var oldTicket = tickets
    sleep(UInt32(0.2))
    oldTicket -= 1
    tickets = oldTicket
    print("还剩\(tickets)张票 ---- \(Thread.current)")
    os_unfair_lock_unlock(&unfairlock)
  }

打印成果和OSSpinLock一样,os_unfair_lock摒弃了OSSpinLock的while循环实现的忙等状况,而是采用了真实让线程休眠,从而防止了优先级反转问题。

3. pthread_mutex

pthread_mutexpthread跨平台的一种处理方案,mutex 为互斥锁,等候锁的线程会处于休眠状况。 互斥锁的初始化比较费事,首要为以下办法:

  1. var ticketMutexLock = pthread_mutex_t()
  2. 初始化特点:
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)
  1. 初始化锁:pthread_mutex_init(&ticketMutexLock, &attr)

关于互斥锁的运用,首要供给了以下办法:

  1. 测验加锁:pthread_mutex_trylock(&ticketMutexLock)
  2. 加锁:pthread_mutex_lock(&ticketMutexLock)
  3. 解锁:pthread_mutex_unlock(&ticketMutexLock)
  4. 毁掉相关资源:pthread_mutexattr_destory(&attr), pthread_mutex_destory(&ticketMutexLock)

运用办法如下:

iOS 线程安全和锁机制

要注意,在析构函数中要将锁进行毁掉开释掉 在初始化特点中,第二个参数有以下几种办法:

iOS 线程安全和锁机制

PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL,代表一般的互斥锁 PTHREAD_MUTEX_ERRORCHECK 代表查看过错锁 PTHREAD_MUTEX_RECURSIVE 代表递归互斥锁

互斥锁的底层原理实现也是经过堵塞线程,等候锁的线程处于休眠状况,CPU不再给等候的线程分配资源,和上面讲到的os_unfair_lock原理类似,都是经过内核调用symcall办法来休眠线程,经过这个比照也能推测出,os_unfair_lock实际上也能够归属于互斥锁

3.1 递归互斥锁

iOS 线程安全和锁机制

如图所示,假如是上述场景,办法1里面嵌套办法2,正常调用时,输出应该为:

iOS 线程安全和锁机制

若要对上述场景确保线程安全,先用一般互斥锁添加锁试下

iOS 线程安全和锁机制

成果打印如下:

iOS 线程安全和锁机制

和料想中的不一样,假如懂得锁机制便会理解,图中所示的rsmText2中加锁失利,需要等候rsmText1中的锁开释后才可加锁,所以rsmText2办法开端等候并堵塞线程,程序无法再履行下去,那么rsmText1中锁开释的逻辑就无法履行,就这样造成了死锁,所以只能打印rsmText1中的输出内容。 处理这个问题,只需要给两个办法用两个不同的锁目标进行加锁就能够了,可是假如是针对于同一个办法递归调用,那么就无法经过不同的目标去加锁,这时候应该怎样办呢?递归互斥锁就该用上了。

iOS 线程安全和锁机制

iOS 线程安全和锁机制

iOS 线程安全和锁机制

如上,现已能够正常调用并加锁 那么递归锁是如何防止死锁的呢?简而言之便是答应对同一个目标进行重复加锁,重复解锁,加锁和解锁的次数相等,调用结束时一切的锁都会被解开

3.2 互斥锁条件 pthread_cond_t

互斥锁条件所用到的常见办法如下:

  1. 定义一个锁: var condMutexLock = pthread_mutex_t()
  2. 初始化锁目标:pthread_mutex_init(&condMutexLock)
  3. 定义条件目标:var condMutex = pthread_cond_t()
  4. 初始化条件目标:pthread_cond_init(&condMutex, nil)
  5. 等候条件:pthread_cond_wait(&condMutex, &condMutexLock) 等候进程中会堵塞线程,知道有激活条件的信号宣布,才会持续履行
  6. 激活一个等候该条件的线程:pthread_cond_signal(&condMutex)
  7. 激活一切等候该条件的线程pthread_cond_broadcast(&condMutex)
  8. 解锁:pthread_mutex_unlock(&condMutexLock)
  9. 毁掉锁目标和毁掉条件目标:pthread_mutex_destroy(&condMutexLock) pthread_cond_destroy(&condMutex)

下面设计一个场景:

  • 在一个线程里对dataArr做remove操作,另一个线程里做add操作
  • dataArr为0时,不能进行删去操作
@IBAction func mutexCondTest(_ sender: Any) {
    initMutextCond()
  }
  func initMutextCond() {
    //初始化特点
    var attr = pthread_mutexattr_t()
    pthread_mutexattr_init(&attr)
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)
    //初始化锁
    pthread_mutex_init(&condMutexLock, &attr)
    //开释特点
    pthread_mutexattr_destroy(&attr)
    //初始化cond
    pthread_cond_init(&condMutex, nil)
    _testDataArr()
   
  }
  func _testDataArr() {
    let threadRemove = Thread(target: self, selector: #selector(_remove), object: nil)
    threadRemove.name = "remove 线程"
    threadRemove.start()
   
    sleep(UInt32(1))
    let threadAdd = Thread(target: self, selector: #selector(_add), object: nil)
    threadAdd.name = "add 线程"
    threadAdd.start()
   
  }
  @objc func _add() {
    //加锁
    pthread_mutex_lock(&condMutexLock)
    print("add 加锁成功---->\(Thread.current.name!)开端")
    sleep(UInt32(2))
    dataArr.append("test")
    print("add成功,发送条件信号 ------ 数组元素个数为\(dataArr.count)")
    pthread_cond_signal(&condMutex)
    //解锁
    pthread_mutex_unlock(&condMutexLock)
    print("解锁成功,\(Thread.current.name!)线程结束")
  }
  @objc func _remove() {
    //加锁
    pthread_mutex_lock(&condMutexLock)
    print("remove 加锁成功,\(Thread.current.name!)线程敞开")
    if(dataArr.count == 0) {
      print("数组内没有元素,开端等候,数组元素为\(dataArr.count)")
      pthread_cond_wait(&condMutex, &condMutexLock)
      print("接收到条件更新信号,dataArr元素个数为\(dataArr.count),持续向下履行")
    }
    dataArr.removeLast()
    print("remove成功,dataArr数组元素个数为\(dataArr.count)")
   
    //解锁
    pthread_mutex_unlock(&condMutexLock)
    print("remove解锁成功,\(Thread.current.name!)线程结束")
  }
 
  deinit {
//    pthread_mutex_destroy(&ticketMutexLock)
    pthread_mutex_destroy(&condMutexLock)
    pthread_cond_destroy(&condMutex)
  }

输出成果为:

iOS 线程安全和锁机制

从打印成果来看,假如不满足条件时进行条件等候 pthread_cond_wati,remove 线程是解锁,此时线程是休眠状况,然后等候的add 线程进行加锁成功,处理add的逻辑。

当add 操作结束时,经过 pthread_cond_signal宣布信号,remove线程收到信号后被唤醒,然后remove线程会等候add线程解锁后,再进行加锁处理后续的逻辑.

整个进程中一共用到了三次加锁,三次解锁,这种锁能够处理线程依靠的场景.

4. NSLock, NSRecursiveLock, NSCondition

上文中提到了mutex一般互斥锁 mutex递归互斥锁mutext条件互斥锁,这几种锁都是基于C语言的API,苹果在此基础上做了面向目标的封装,分别对应如下:

  • NSLock 封装了 pthread_mutex_t的 attr类型为 PTHREAD_MUTEX_DEFAULT 或者 PTHREAD_MUTEX_NORMAL 一般锁
  • NSRecursiveLock 封装了 pthread_mutex 的 attr类型为PTHREAD_MUTEX_RECURSIVE递归锁
  • NSCondition 封装了 pthread_mutex_tpthread_cond_t

底层实现和 pthread_mutex_t一样,这儿只看下运用办法即可:

4.1 NSLock
//一般锁 
let lock = NSLock() 
lock.lock()
lock.unlock()
4.2 NSRecursiveLock
let lock = NSRecursiveLock()
lock.lock()
lock.unlock()
4.3 NSCondition
let condition = NSCondition()
condition.lock()
condition.wait()
condition.signal()//condition.broadcast()
condition.unlock()
4.4 NSConditionLock

这个是NSCondition 的进一步封装,该锁答应咱们在锁中设定条件具体条件值,有了这个功能,咱们能够愈加便利的多条线程的依靠关系和前后履行次序

下面用一个场景来模拟下次序操控的功能,有三条线程履行A,B,C三个办法,要求按A,C,B的次序履行

@IBAction func conditionLockTest(_ sender: Any) {
   let threadA = Thread(target: self, selector: #selector(A), object: nil)
    threadA.name = "ThreadA"
    threadA.start()
   let threadB = Thread(target: self, selector: #selector(B), object: nil)
    threadB.name = "ThreadB"
    threadB.start()
   let threadC = Thread(target: self, selector: #selector(C), object: nil)
    threadC.name = "ThreadC"
    threadC.start()
  }
  @objc func A() {
    conditionLock.lock()
    print("A")
    sleep(UInt32(1))
    conditionLock.unlock(withCondition: 3)
  }
  @objc func B() {
    conditionLock.lock(whenCondition: 2)
    print("B")
    sleep(UInt32(1))
    conditionLock.unlock()
  }
  @objc func C() {
    conditionLock.lock(whenCondition: 3)
    print("C")
    conditionLock.unlock(withCondition: 2)
  }

输出成果为:

A
C
B

5. dispatch_semaphore

信号量 的初始值能够用来操控线程并发拜访的最大数量,初始值为1,表示一起答应一条线程拜访资源,这样能够到达线程同步的意图

  • 创建信号量: dispatch_semaphore_create(value)

  • 等候:dispatch_semaphore_wait(semaphore, 等候时间) 信号量的值 <= 0,线程就休眠等候,直到信号量 > 0,假如信号量的值 > 0,则就将信号量的值递减1,持续履行下面的程序

  • 信号量值+1: dispatch_semaphore_signal(semaphore)