Swift 中有这么多锁,你都学会了吗?

2,957 阅读6分钟

引言

锁是线程编程中基本的同步工具。锁使你能够轻松地保护大段代码,以确保这些代码的正确性。OS X 和 iOS 为所有应用程序类型提供了基本的互斥锁,Foundation 框架还为特定情况定义了一些互斥锁的变体。

  • 互斥锁

  1. POSIX Mutex Lock

POSIX 互斥锁在任何应用程序中都非常容易使用。要创建互斥锁,你需要声明并初始化一个 pthread_mutex_t 结构体。要锁定和解锁互斥锁,你需要使用 pthread_mutex_lockpthread_mutex_unlock 函数。当你不再需要该锁时,只需调用 pthread_mutex_destroy 来释放锁的数据结构。

class Counter {
    private var value = 0
    private var mutex = pthread_mutex_t()
    init() {
      	//初始化
        pthread_mutex_init(&mutex, nil)
    }
    func increment() {
      	//获取锁
        pthread_mutex_lock(&mutex)
        value += 1
      	//释放锁
        pthread_mutex_unlock(&mutex)
    }
    deinit {
      	//销毁锁
        pthread_mutex_destroy(&mutex)
    }
}
let counter = Counter()
for _ in 0..<6 {
    DispatchQueue.global().async {
        counter.increment()
    }
}
  1. NSLock

    NSLock 对象为 Cocoa 应用程序实现了一个基本的互斥锁。所有锁(包括 NSLock)的接口实际上是由 NSLocking 协议定义的,该协议定义了 lock 和 unlock 方法。你可以像使用任何互斥锁一样使用这些方法来获取和释放锁。

//改写上个案例
class Counter {
    private var value = 0
    private var lock = NSLock()
    func increment() {
      	//尝试获取锁,阻塞线程的执行直到锁能够被获取。
        lock.lock()
        value += 1
        lock.unlock()
    }
}

let counter = Counter()
for _ in 0..<6 {
    DispatchQueue.global().async {
        counter.increment()
    }
}

除了标准的锁定行为外,NSLock 类还增加了 tryLock 和 lockBeforeDate: 方法。tryLock 方法尝试获取锁,但如果锁不可用则不会阻塞;相反,该方法只是简单地返回 NO。lockBeforeDate: 方法尝试获取锁,但如果在指定时间内未能获取锁,则会解除线程阻塞并返回 NO。

class DataHandler {
    private var sharedData = [Int]()
    private let lock = NSLock()
    
    // 读取数据的方法
    func readData() -> [Int]? {
        if lock.try() {
            // 成功获取锁
            let data = sharedData
            lock.unlock()
            return data
        } else {
            // 未能获取锁,返回 nil
            return nil
        }
    }
    
    // 写入数据的方法
    func writeData(_ data: Int) {
        // 尝试获取锁
        if lock.try() {
            // 成功获取锁
            sharedData.append(data)
            print("Data written: \(data) by \(Thread.current)")
            lock.unlock()
        } else {
            // 未能获取锁,进行其他操作或稍后再试
            print("Write lock not acquired by \(Thread.current), will try again later.")
        }
    }
}

let dataHandler = DataHandler()

// 读取线程
let readThread = Thread {
    while true {
        if let data = dataHandler.readData() {
            print("Data read by \(Thread.current): \(data)")
        } else {
            print("Read lock not acquired by \(Thread.current)")
        }
        // 模拟其他工作
        Thread.sleep(forTimeInterval: 1)
    }
}

// 写入线程
let writeThread = Thread {
    var counter = 0
    while true {
        dataHandler.writeData(counter)
        counter += 1
        // 模拟其他工作
        Thread.sleep(forTimeInterval: 0.5)
    }
}

// 启动线程
readThread.start()
writeThread.start()
  • 递归锁

    • NSRecursiveLock

NSRecursiveLock 类定义了一种锁,这种锁可以被同一个线程多次获取而不会导致线程死锁。递归锁会记录成功获取锁的次数。每次成功获取锁都必须通过相应的解锁调用来平衡。只有当所有的锁定和解锁调用平衡时,锁才真正被释放,以便其他线程可以获取它。

顾名思义,这种类型的锁通常用于递归函数内部,以防止递归阻塞线程。你也可以在非递归情况下使用它,来调用需要获取锁的函数。这是一个简单递归函数的例子,该函数通过递归获取锁。如果不使用 NSRecursiveLock 对象,此代码在函数再次调用时会导致线程死锁。

let recursiveLock = NSRecursiveLock()
var value = 15
func recuriveFunction() {
    recursiveLock.lock()
    if value > 0 {
        print("value: \(value) \(Thread.current)")
        value -= 1
        recuriveFunction()
    }
    recursiveLock.unlock()
}

func testFunction() {
    recursiveLock.lock()
    value -= 1
    print("value: \(value) \(Thread.current)")
    recursiveLock.unlock()
}

DispatchQueue.global().async {
    recuriveFunction()
}

DispatchQueue.global().async {
    testFunction()
}

条件锁

  • NSConditionLock

NSConditionLock 对象定义了一种可以使用特定值进行加锁和解锁的互斥锁。通常,当线程需要按特定顺序执行任务时,你会使用 NSConditionLock 对象,例如一个线程生成数据,另一个线程消费数据。当生产者线程在执行时,消费者线程使用一个特定于你的程序的条件来获取锁(条件本身只是一个你定义的整数值)。当生产者完成时,它会解锁并将锁的条件设置为适当的整数值以唤醒消费者线程,然后消费者线程继续处理数据。

// 定义条件
enum BufferCondition: Int {
    case empty = 0
    case full = 1
}

// 创建一个 NSConditionLock 实例,初始条件为 0
let conditionLock = NSConditionLock(condition: BufferCondition.empty.rawValue)
var buffer: [Int] = []

// 定义缓冲区最大容量
let bufferSize = 5

// 生产者线程
DispatchQueue.global().async {
    var item = 0
    while (true) {
        // 当传入条件是 0, 获取锁,其他情况阻塞线程
        conditionLock.lock(whenCondition: BufferCondition.empty.rawValue)
        Thread.sleep(forTimeInterval: 1.0)
        print("produce: \(item)")
        buffer.append(item)
        item += 1
        let newCondition = buffer.count == bufferSize ? BufferCondition.full.rawValue: BufferCondition.empty.rawValue
        // 解锁,当buffer满了之后,设置条件为1,其他情况设置条件为 0
        conditionLock.unlock(withCondition: newCondition)
    }
}

// 消费者线程
DispatchQueue.global().async {
    while (true) {
        // 当传入条件是 1, 获取锁,其他情况阻塞线程
        conditionLock.lock(whenCondition: BufferCondition.full.rawValue)
        Thread.sleep(forTimeInterval: 0.5)
        let item = buffer.removeFirst()
        print("consume: \(item)")
        let newCondition = buffer.count == 0 ? BufferCondition.empty.rawValue: BufferCondition.full.rawValue
        
        // 解锁,当buffer为空时,设置条件为 0,其他情况设置条件为 1
        conditionLock.unlock(withCondition: newCondition)
    }
}
  • NSCondition

Conditions是一种特殊类型的锁,可以用于同步操作执行的顺序。他们与互斥锁在细微之处有不同,等待条件的线程会保持阻塞状态,直到该条件被另一个线程显示信号通知

let condition = NSCondition()
var buffer: [Int] = []
// 定义缓冲区最大容量
let bufferSize = 10

let producer = Thread {
    var item = 0
    while true {
        // 获取锁,如果没有成功阻塞线程
        condition.lock()
        Thread.sleep(forTimeInterval: 1.0)
        print("Produced: \(item)")
        buffer.append(item)
        // 当buffer充满时,唤醒消费者线程,然后调用`wait()`阻塞线程
        if buffer.count == bufferSize {
            condition.signal()
            condition.wait()
        }
        item += 1
        // 释放锁
        condition.unlock()
    }
}

let consumer = Thread {
    while true {
        // 获取锁,如果没有成功阻塞线程
        condition.lock()
        if buffer.isEmpty {
            // 当buffer为空时,唤醒生产者线程,然后调用`wait()`阻塞线程
            condition.signal()
            condition.wait()
        }
        let value = buffer.removeFirst()
        Thread.sleep(forTimeInterval: 1.0)
        print("Consumed: \(value)")
        // 释放锁
        condition.unlock()
    }
}

consumer.start()
producer.start()

  • 其他锁

    • Dispatch Semaphore
    class Counter {
        private var value = 0
    
        let semaphore = DispatchSemaphore(value: 1)
    
        func increment() {
            semaphore.wait()
            value += 1
            print("value: \(value)")
            semaphore.signal()
        }
    }
    
    let counter = Counter()
    for _ in 0..<6 {
        DispatchQueue.global().async {
            counter.increment()
        }
    }
    
    • Dispatch 串行队列
    class Counter {
        private var value = 0
        let serialQueue = DispatchQueue(label: "searialQueue")
        func increment() {
            serialQueue.async {
                self.value += 1
                print("vlaue: \(self.value)")
            }
         }  
    }
    let counter = Counter()
    for _ in 0..<6 {
        DispatchQueue.global().async {
            counter.increment()
        }
    }
    
    • os_unfair_lock

    os_unfair_lock 是 Apple 提供的一种轻量级、高性能的锁,用于替代不推荐使用的 OSSpinLock。它主要用于多线程编程中,确保共享资源的安全访问。os_unfair_lock 的设计目标是提供一种低开销的锁机制,同时避免 OSSpinLock 可能出现的优先级反转问题。

    class Counter {
        private var value = 0
    
        private var lock = os_unfair_lock_s()
    
        func increment() {
            os_unfair_lock_lock(&lock)
            value += 1
            print("value: \(value)")
            os_unfair_lock_unlock(&lock)
        }
    }
    
    let counter = Counter()
    for _ in 0..<6 {
        DispatchQueue.global().async {
            counter.increment()
        }
    }
    

结语

Swift 提供了丰富的锁机制来帮助开发者处理多线程编程中的同步问题。每种锁都有其独特的特点和适用场景:

  • NSLock和NSRecursiveLock适合一般的线程同步需求,递归锁则更适用于递归调用的场景。
  • NSCondition 和NSConditionLock 提供了更高级的条件同步机制,适用于复杂的线程等待和通知场景。
  • DispatchSemaphore 和 DispatchQueue (串行队列) 提供了基于 GCD 的高效并发控制方式。
  • os_unfair_lock 提供了高性能的锁机制,适合低开销、避免优先级反转的问题。
  • pthread_mutex 提供了底层的 POSIX 线程锁,用于更细粒度的控制。

通过学习和掌握这些不同的锁机制,你可以根据具体的应用场景选择最合适的工具,确保程序的正确性和性能。多线程编程虽然复杂,但合理使用这些锁机制可以帮助你有效地管理并发和同步,编写出高效、可靠的 Swift 应用程序。希望本文的介绍能帮助你更好地理解和使用 Swift 中的各种锁,实现稳定和高效的多线程编程。

如果有任何问题,请不吝赐教。