swift 多线程锁(一) NSLock

发布时间:2023年12月18日

在多线程的相关的开发中,必定会有锁的应用,这是因为如果多个线程极有可能会同时读取或者修改一个对象的值,那这时候很可能会出问题,比如读取的数值不对,或者出现之前对象的值已经被释放而引发野指针的问题

卖票问题

我们先看已经经典的售票问题,假设我们总共有500张票,有4个网络渠道同步售卖

不加锁

先看不加锁的情况,并发4个线程来同时卖票,代码如下:

class ViewController: UIViewController {
    var num = 10

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        DispatchQueue.global().async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "1",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "2",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        for _ in 0...100 {
            self.buy()
        }
        print("main thread完成任务")

    }

    func buy(){
        if num > 0{
            num -= 1
            print("出票一张: 剩余: \(num)")
        }
    }

}

打印结果如下:

出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 7
出票一张: 剩余: 6
出票一张: 剩余: 5
出票一张: 剩余: 4
出票一张: 剩余: 3
出票一张: 剩余: 2
出票一张: 剩余: 1
出票一张: 剩余: 0

注意看,这里的剩余票数不对,且只有10张票,出票了12次。

另外需要说明的是,因为是多线程的问题,所以随着并发次数增多,那错误会更多,比如10000张票,分10个渠道,分别尝试出票1000次,那结果会更加混乱,感兴趣的读者可以尝试

锁的介绍

这里介绍下锁的概念,所谓的锁,就是当你操作这个变量的时候,把权限门锁上,等你开了锁之后,其他人才能操作这个变量,即保证同一时间只有一个操作行为

我们先试用比较基础的NSLock来加锁,NSLock是互斥锁,对应的是自旋锁。

互斥锁是指当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。

自旋锁是指线程在这一过程中保持执行,忙等待可以操作,比如每1ms来查看是否可以操作对象,该线程不会挂起,会一直占用cpu切片,但是没有互斥锁的线程切换行为

这两种锁各有利弊,但是一般还是推荐使用互斥锁,因为自旋锁用不好非常容易占用CPU资源

NSLock

我们使用NSLock来进行加锁,首先我们定义一个锁 ,然后在出票前我们加锁,出票结束后我们解锁,代码如下:

class ViewController: UIViewController {
    var num = 10

    let lockLock = NSLock()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        DispatchQueue.global().async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "1",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        DispatchQueue(label: "2",attributes: .concurrent).async {
            for _ in 0...10 {
                self.buy()
            }
        }

        for _ in 0...100 {
            self.buy()
        }
        print("main thread完成任务")

    }

    func buy(){
        lockLock.lock()
        if num > 0{
            num -= 1
            print("出票一张: 剩余: \(num)")
        }
        lockLock.unlock()
    }

}

输出日志如下:

出票一张: 剩余: 9
出票一张: 剩余: 8
出票一张: 剩余: 7
出票一张: 剩余: 6
出票一张: 剩余: 5
出票一张: 剩余: 4
出票一张: 剩余: 3
出票一张: 剩余: 2
出票一张: 剩余: 1
出票一张: 剩余: 0

这里可以看到,顺序和剩余票数都对了

NSLock源码

open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

    public override init() {
#if os(Windows)
        InitializeSRWLock(mutex)
        InitializeConditionVariable(timeoutCond)
        InitializeSRWLock(timeoutMutex)
#else
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
    }
    
    deinit {
#if os(Windows)
        // SRWLocks do not need to be explicitly destroyed
#else
        pthread_mutex_destroy(mutex)
#endif
        mutex.deinitialize(count: 1)
        mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
    }
    
    open func lock() {
#if os(Windows)
        AcquireSRWLockExclusive(mutex)
#else
        pthread_mutex_lock(mutex)
#endif
    }

    open func unlock() {
#if os(Windows)
        ReleaseSRWLockExclusive(mutex)
        AcquireSRWLockExclusive(timeoutMutex)
        WakeAllConditionVariable(timeoutCond)
        ReleaseSRWLockExclusive(timeoutMutex)
#else
        pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
#endif
#endif
    }

    open func `try`() -> Bool {
#if os(Windows)
        return TryAcquireSRWLockExclusive(mutex) != 0
#else
        return pthread_mutex_trylock(mutex) == 0
#endif
    }
    
    open func lock(before limit: Date) -> Bool {
#if os(Windows)
        if TryAcquireSRWLockExclusive(mutex) != 0 {
          return true
        }
#else
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
#endif

#if os(macOS) || os(iOS) || os(Windows)
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
        guard var endTime = timeSpecFrom(date: limit) else {
            return false
        }
#if os(WASI)
        return true
#else
        return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
#endif
    }

    open var name: String?
}

这里可以看到,NSLock其实就是对pthread_mutex的封装,后面讲递归锁的时候,我们还会再仔细看这里的代码

NSLock注意事项

lock会挂起线程,要注意配对使用

刚才讲到NSLock是互斥锁,会挂起线程,那么如果在上面的代码最后,增加两次lock,那么主线程就会被挂起,增加代码如下:

lockLock.lock()

lockLock.lock()

print("main thread完成任务")

这里的main thread完成任务永远不会执行了,非常恐怖

一个办法可以避免造成死锁,使用try函数,尤其是在主线程中加锁的时候,try函数的介绍如下:
Attempts to acquire a lock and immediately returns a Boolean value that indicates whether the attempt was successful.

if lockLock.try() {
    lockLock.lock()
}
if lockLock.try() {
    lockLock.lock()
}
if lockLock.try() {
    lockLock.lock()
}
print("main thread完成任务")

这样子就不会发生死锁

NSLock是非递归锁

所谓递归非递归,是指是否支持嵌套,比如Lock中如果还有这个lock,那是否会出问题,我们尝试下,比如用户1,他因为各种原因,会在买票行为过程前后加锁解锁,我们试下

DispatchQueue(label: "1",attributes: .concurrent).async {
            print("用户1开始买票")
            self.lockLock.lock()
            for _ in 0...10 {
                self.buy()
            }
            self.lockLock.unlock()
            print("用户1结束买票")
        }

我这里打印的结果是这样:

用户1开始买票
出票一张: 剩余: 9
出票一张: 剩余: 8
出票一张: 剩余: 7

这里就发生了死锁,用户1永远不会结束买票,因为它执行买票需要锁解除,而刚开始已经锁了,后续的解锁只能等购买行为结束才能解锁,购买行为需要等这个锁解除,就死循环了

怎么解决呢? 递归锁来解决,后续再增加这部分内容,所以这里要强调,lock一定要针对最小颗粒来进行解锁,避免死锁问题。 什么是最小颗粒?就是操作的最小函数,把操作相关的都封装函数,然后进行操作,不要在外部使用锁

参考文章和源码资料

  • https://juejin.cn/post/7000298226855182349
  • NSLock源码 https://github.com/apple/swift-corelibs-foundation
  • https://www.jianshu.com/p/777c28eface5
文章来源:https://blog.csdn.net/njafei/article/details/135011091
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。